refactor(e2b): drop speculative cleanup retry state
This commit is contained in:
@@ -29,7 +29,6 @@ export class E2BSubprocessService extends SubprocessService {
|
||||
private readonly live = new Set<E2BSubprocessHandle>()
|
||||
private readonly terminals = new Set<SubprocessTerminalHandle>()
|
||||
private readonly terminalSetups = new Map<Promise<void>, AbortController>()
|
||||
private readonly failedTerminalSetupCleanups = new Set<() => Promise<void>>()
|
||||
private disposing = false
|
||||
|
||||
/** @inheritdoc */
|
||||
@@ -51,7 +50,6 @@ export class E2BSubprocessService extends SubprocessService {
|
||||
await Promise.all([...this.terminalSetups.keys()])
|
||||
const handles = [...this.live]
|
||||
const terminals = [...this.terminals]
|
||||
const failedTerminalSetupCleanups = [...this.failedTerminalSetupCleanups]
|
||||
const pending: Promise<unknown>[] = []
|
||||
for (const handle of handles) {
|
||||
handle.terminate()
|
||||
@@ -63,9 +61,6 @@ export class E2BSubprocessService extends SubprocessService {
|
||||
for (const terminal of terminals) {
|
||||
pending.push(terminal.terminate().then(() => { this.terminals.delete(terminal) }))
|
||||
}
|
||||
for (const cleanup of failedTerminalSetupCleanups) {
|
||||
pending.push(cleanup().then(() => { this.failedTerminalSetupCleanups.delete(cleanup) }))
|
||||
}
|
||||
const outcomes = await Promise.allSettled(pending)
|
||||
for (const outcome of outcomes) {
|
||||
if (outcome.status === 'rejected') throw outcome.reason
|
||||
@@ -155,7 +150,6 @@ export class E2BSubprocessService extends SubprocessService {
|
||||
this.ctx.e2b,
|
||||
{ ...spec, signal: setupSignal },
|
||||
stateDir,
|
||||
(cleanup) => { this.failedTerminalSetupCleanups.add(cleanup) },
|
||||
)
|
||||
this.terminals.add(terminal)
|
||||
// oxlint-disable-next-line typescript/no-unnecessary-condition -- Remote allocation yields to disposal.
|
||||
|
||||
@@ -636,35 +636,18 @@ export class E2BSubprocessHandle implements SubprocessHandle {
|
||||
}
|
||||
|
||||
private async forceKillGroup(sandbox: Sandbox, handle: CommandHandle, processGroupId: number): Promise<void> {
|
||||
let groupFailure: unknown
|
||||
try {
|
||||
if (!await this.signalGroup(sandbox, processGroupId, 'KILL')) {
|
||||
groupFailure = new Error('process-group KILL did not report delivery')
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
groupFailure = error
|
||||
await this.signalGroup(sandbox, processGroupId, 'KILL')
|
||||
} catch (_processGroupKillFailure) {
|
||||
// SDK kill and the final liveness probe remain independent cleanup paths.
|
||||
}
|
||||
let handleFailure: unknown
|
||||
try {
|
||||
if (!await handle.kill()) handleFailure = new Error('E2B SDK kill did not report command termination')
|
||||
} catch (error: unknown) {
|
||||
handleFailure = error
|
||||
await handle.kill()
|
||||
} catch (_sdkKillFailure) {
|
||||
// The final liveness probe, not either transport's self-report, proves cleanup.
|
||||
}
|
||||
let proofFailure: unknown
|
||||
try {
|
||||
if (await this.waitForGroupExit(sandbox, processGroupId)) return
|
||||
proofFailure = new Error(`remote process group ${processGroupId} remained live after force termination`)
|
||||
} catch (error: unknown) {
|
||||
proofFailure = error
|
||||
}
|
||||
throw new AggregateError(
|
||||
[
|
||||
...(groupFailure === undefined ? [] : [groupFailure]),
|
||||
...(handleFailure === undefined ? [] : [handleFailure]),
|
||||
proofFailure,
|
||||
],
|
||||
'subprocess-e2b: force termination failed through both process-group and SDK transports',
|
||||
)
|
||||
if (await this.waitForGroupExit(sandbox, processGroupId)) return
|
||||
throw new Error(`subprocess-e2b: remote process group ${processGroupId} remained live after force termination`)
|
||||
}
|
||||
|
||||
private async waitForGroupExit(sandbox: Sandbox, processGroupId: number): Promise<boolean> {
|
||||
|
||||
@@ -485,14 +485,12 @@ export class E2BTerminalHandle implements SubprocessTerminalHandle {
|
||||
* @param runtime - Shared E2B sandbox owner.
|
||||
* @param spec - Fully specified terminal-process request.
|
||||
* @param stateDir - Private remote directory for one startup transaction.
|
||||
* @param retainFailedCleanup - Optional owner for retrying a cleanup transaction that could not prove quiescence.
|
||||
* @returns The live subprocess terminal handle.
|
||||
*/
|
||||
export async function spawnE2BTerminal(
|
||||
runtime: E2BSandboxService,
|
||||
spec: SubprocessTerminalSpawnSpec,
|
||||
stateDir: string,
|
||||
retainFailedCleanup?: (cleanup: () => Promise<void>) => void,
|
||||
): Promise<E2BTerminalHandle> {
|
||||
const sandbox = await runtime.getSandbox()
|
||||
spec.signal?.throwIfAborted()
|
||||
@@ -562,7 +560,7 @@ export async function spawnE2BTerminal(
|
||||
output.destroy()
|
||||
let terminalQuiescent = handle === undefined
|
||||
let stateRemoved = !stateDirectoryCreated
|
||||
const retryCleanup = async (): Promise<void> => {
|
||||
const cleanup = async (): Promise<void> => {
|
||||
const failures: Error[] = []
|
||||
if (!terminalQuiescent && handle !== undefined) {
|
||||
try {
|
||||
@@ -588,9 +586,10 @@ export async function spawnE2BTerminal(
|
||||
}
|
||||
}
|
||||
try {
|
||||
await retryCleanup()
|
||||
await cleanup()
|
||||
} catch (cleanupError: unknown) {
|
||||
retainFailedCleanup?.(retryCleanup)
|
||||
// TODO(e2b-terminal-setup-rollback): Retain retry state only if a real
|
||||
// double failure must be recovered before sandbox disposal or timeout.
|
||||
throw new AggregateError([asError(error), asError(cleanupError)], asError(error).message)
|
||||
}
|
||||
throw error
|
||||
|
||||
Reference in New Issue
Block a user