refactor(e2b): drop speculative cleanup retry state

This commit is contained in:
Tianyi Cui
2026-07-30 06:02:10 +08:00
parent de77310c6f
commit b1b2d63785
15 changed files with 48 additions and 111 deletions

View File

@@ -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.

View File

@@ -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> {

View File

@@ -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