refactor(e2b): drop speculative cleanup retry state
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/e2b/e2b/README.md
|
||||
README.md: 6045b313a91edafc25361eba3f3c4dacf56b4eb0
|
||||
README.zh.md: dc157d9b5167488530c7d63b59aed793ab6aa4c7
|
||||
README.md: 7ade7c3d6522d8fa6d54d7011766238451b17c9a
|
||||
README.zh.md: b683f33d2211106cf422e780b2b37904b0c640ad
|
||||
|
||||
@@ -26,7 +26,7 @@ Shared lifecycle owner for one E2B sandbox. The filesystem and subprocess adapte
|
||||
|
||||
Construction starts one sandbox creation. Before resolving `getSandbox()`, the service creates `cwd` and the private `cwd/.dsh-e2b` adapter-state directory, verifies that the reserved path is a real directory rather than a symlink or another file type, then sets it to mode `0700`. Each adapter-internal E2B command shell receives a fresh randomized root-level `HOME`, so the SDK's fixed login shell does not resolve profile files from the mutable user home before the control command.
|
||||
|
||||
Disposal first prevents new handle acquisition, then awaits setup and deletes the sandbox. A `SandboxNotFoundError` means expiry or another owner already deleted it and is accepted as quiescence. Initial directory setup failure also deletes the newly created sandbox; if that rollback fails, disposal retries it before releasing ownership. Provider plugins must load after this owner and dispose before it.
|
||||
Disposal first prevents new handle acquisition, then awaits setup and deletes the sandbox. A `SandboxNotFoundError` means expiry or another owner already deleted it and is accepted as quiescence. Initial directory setup failure makes one deletion attempt; the configured E2B timeout bounds a second failure. Provider plugins must load after this owner and dispose before it.
|
||||
|
||||
## Model Experience
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
构造阶段会启动一次沙箱创建。服务在 `getSandbox()` 结算前创建 `cwd` 和私有的 `cwd/.dsh-e2b` 适配器状态目录,验证该预留路径是真实目录而非符号链接或其他文件类型,再把该目录的 mode 设为 `0700`。每个适配器内部的 E2B 命令 shell 都会获得一个位于根目录下、全新随机生成的 `HOME`,因此 SDK 固定使用的登录 shell 不会在控制命令之前解析可变用户主目录中的配置文件。
|
||||
|
||||
资源释放会先阻止继续获取新句柄,再等待初始化完成,然后删除沙箱。`SandboxNotFoundError` 表示沙箱已因超时或被另一个所有者删除,因此可视为完全停稳。初始目录设置失败时也会删除新建沙箱;如果该回滚失败,资源释放会在解除所有权前重试。提供方插件必须在该所有者之后加载,并在其之前 dispose(资源释放)。
|
||||
资源释放会先阻止继续获取新句柄,再等待初始化完成,然后删除沙箱。`SandboxNotFoundError` 表示沙箱已因超时或被另一个所有者删除,因此可视为完全停稳。初始目录设置失败时会尝试删除一次;若该尝试也失败,则由已配置的 E2B 超时约束沙箱的存活时间。提供方插件必须在该所有者之后加载,并在其之前 dispose(资源释放)。
|
||||
|
||||
## 模型体验
|
||||
|
||||
|
||||
@@ -85,7 +85,6 @@ export class E2BSandboxService extends Service {
|
||||
|
||||
private readonly config: ResolvedConfig
|
||||
private readonly ready: Promise<Sandbox>
|
||||
private failedSetupSandbox: Sandbox | undefined
|
||||
private disposed = false
|
||||
|
||||
constructor(ctx: Context, config: Config) {
|
||||
@@ -108,21 +107,18 @@ export class E2BSandboxService extends Service {
|
||||
|
||||
ctx.effect(() => async () => {
|
||||
this.disposed = true
|
||||
let sandbox = this.failedSetupSandbox
|
||||
if (sandbox === undefined) {
|
||||
try {
|
||||
sandbox = await this.ready
|
||||
} catch {
|
||||
sandbox = this.failedSetupSandbox
|
||||
}
|
||||
let sandbox: Sandbox
|
||||
try {
|
||||
sandbox = await this.ready
|
||||
} catch (_sandboxSetupFailure) {
|
||||
// open() either acquired no sandbox or already made the POC's one rollback attempt.
|
||||
return
|
||||
}
|
||||
if (sandbox === undefined) return
|
||||
try {
|
||||
await sandbox.kill()
|
||||
} catch (error: unknown) {
|
||||
if (!(error instanceof SandboxNotFoundError)) throw error
|
||||
}
|
||||
this.failedSetupSandbox = undefined
|
||||
}, 'e2b sandbox teardown')
|
||||
}
|
||||
|
||||
@@ -174,10 +170,9 @@ export class E2BSandboxService extends Service {
|
||||
} catch (error: unknown) {
|
||||
try {
|
||||
await sandbox.kill()
|
||||
} catch (_cleanupFailure) {
|
||||
// Preserve the setup failure as the public error while retaining the
|
||||
// created handle for the service disposer to retry this rollback.
|
||||
this.failedSetupSandbox = sandbox
|
||||
} catch (_sandboxSetupRollbackFailure) {
|
||||
// TODO(e2b-setup-rollback): Add retry state only if a real double failure
|
||||
// outlives E2B's configured sandbox timeout.
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ describe('E2BSandboxService', () => {
|
||||
await fiber.dispose()
|
||||
})
|
||||
|
||||
it('preserves the setup failure even when cleanup also fails', async () => {
|
||||
it('preserves the setup failure after its one rollback attempt fails', async () => {
|
||||
const fixture = fakeSandbox()
|
||||
fixture.run.mockRejectedValueOnce(new Error('chmod failed'))
|
||||
fixture.kill.mockRejectedValueOnce(new Error('cleanup failed'))
|
||||
@@ -191,23 +191,7 @@ describe('E2BSandboxService', () => {
|
||||
expect(fixture.kill).toHaveBeenCalledOnce()
|
||||
|
||||
await fiber.dispose()
|
||||
expect(fixture.kill).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['retries a still-failing rollback', new Error('retry failed')],
|
||||
['accepts a setup sandbox that expired before retry', new SandboxNotFoundError('sandbox expired')],
|
||||
])('%s during disposal', async (_label, retryError) => {
|
||||
const fixture = fakeSandbox()
|
||||
fixture.run.mockRejectedValueOnce(new Error('chmod failed'))
|
||||
fixture.kill.mockRejectedValueOnce(new Error('cleanup failed')).mockRejectedValueOnce(retryError)
|
||||
sdk.create.mockResolvedValue(fixture.sandbox)
|
||||
const ctx = new Context()
|
||||
const fiber = await ctx.plugin(E2BSandboxService, { apiKey: 'test-key' })
|
||||
|
||||
await expect(ctx.e2b.getSandbox()).rejects.toThrow('chmod failed')
|
||||
await fiber.dispose()
|
||||
expect(fixture.kill).toHaveBeenCalledTimes(2)
|
||||
expect(fixture.kill).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it.each([
|
||||
|
||||
Reference in New Issue
Block a user