Skip to content

Commit ab2d077

Browse files
committed
fix(sandbox): pause OpenSandbox resources on stop
1 parent b776b71 commit ab2d077

9 files changed

Lines changed: 1297 additions & 909 deletions

File tree

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# OpenSandbox Adapter 生命周期与 SDK 适配重构
2+
3+
状态:已实现
4+
5+
最后核对:2026-07-23
6+
7+
## 1. 背景与目标
8+
9+
FastGPT 会在 Sandbox 闲置 5 分钟后调用 provider adapter 的 `stop()`。当前
10+
`OpenSandboxAdapter.stop()` 删除远端 sandbox,导致下次使用时只能重新创建,无法保留
11+
容器状态。本次调整要求 OpenSandbox 的 `stop()` 使用 pause,并通过 resume 复用原实例。
12+
13+
同时完整核对 `OpenSandboxAdapter` 与当前 OpenSandbox JavaScript SDK 的实现,处理生命周期
14+
状态、HTTP agent 释放、readiness、命令输出和测试职责中已经存在的问题。
15+
16+
## 2. 范围
17+
18+
- `OpenSandboxAdapter` 的 create/connect/ensure/start/stop/delete/getInfo/close 生命周期。
19+
- OpenSandbox SDK 的 ConnectionConfig、Sandbox、SandboxManager、readiness 和命令执行约定。
20+
- 基于 FastGPT `sandboxId` 的远端实例复用,以及同一 adapter 内的 SDK client 复用。
21+
- OpenSandbox adapter 单元测试、上传恢复 helper 测试和 OpenSandbox 集成测试。
22+
- OpenSandbox SDK 依赖与锁文件版本。
23+
24+
不调整 FastGPT application 层的 5 分钟 cron、Mongo 生命周期状态机和其他 sandbox provider。
25+
26+
## 3. 当前实现审查
27+
28+
### 3.1 已有 reuse
29+
30+
当前已经存在远端资源复用,但不是 SDK 内置的 pool:
31+
32+
1. FastGPT 把稳定的业务 `sandboxId` 作为 adapter `sessionId`
33+
2. 创建 OpenSandbox 时写入 `metadata.sessionId`
34+
3. `ensureRunning()` 通过 `SandboxManager.listSandboxInfos()` 按 metadata 查找远端实例。
35+
4. 找到运行中实例时 connect,找到暂停实例时 resume,不存在时才 create。
36+
37+
因此本次不新增另一套资源池。需要保留并强化 metadata 寻址,使 pause 后仍能找到同一个远端
38+
sandbox。
39+
40+
### 3.2 本地 SDK client 未复用
41+
42+
`ensureRunning()` 在远端状态为 Running 时始终调用 `Sandbox.connect()`,即使 adapter 已经绑定
43+
同一个 `Sandbox`。SDK 的每个静态 create/connect 和 SandboxManager 都可能持有独立的 undici
44+
agent;旧 `Sandbox` 没有 close 就被覆盖,会泄漏 client 资源。
45+
46+
目标行为:
47+
48+
- 已绑定同一个 Running sandbox 时直接复用,不重新 connect。
49+
- 静态 create/connect 替换旧实例时释放旧实例。
50+
- instance resume 返回的新对象复用原 connection transport,不重复关闭共享 transport。
51+
- `close()` 幂等释放并解除绑定。
52+
53+
### 3.3 SandboxManager 生命周期过碎
54+
55+
删除等待会每秒创建并关闭一个 SandboxManager。每个 manager 都拥有独立 transport,这会产生
56+
不必要的 agent churn。
57+
58+
目标行为是在一次顶层 lifecycle 操作内只创建一个 manager,查找、pause/kill 和轮询共用它,
59+
并在 `finally` 中关闭。manager 不提升为 adapter 长生命周期字段,因为资源 cron 构造临时 adapter
60+
后只调用 stop/delete,不保证额外调用 close。
61+
62+
### 3.4 生命周期状态映射不完整
63+
64+
当前映射缺少 OpenSandbox 的 `Pending``Pausing``Resuming``Terminated``Failed`
65+
尤其是 pause API 返回 202 后会经历 `Running -> Pausing -> Paused`;如果 stop 在 202 后立即返回,
66+
FastGPT 会提前把本地记录标成 stopped,紧接着 resume 也可能因为仍在 Pausing 而失败。
67+
68+
目标映射:
69+
70+
| OpenSandbox | Adapter |
71+
| --- | --- |
72+
| Pending / Creating | Creating |
73+
| Running | Running |
74+
| Resuming / Starting | Starting |
75+
| Pausing / Stopping | Stopping |
76+
| Paused / Stopped | Stopped |
77+
| Deleting | Deleting |
78+
| Deleted / Terminated | UnExist |
79+
| Error / Failed / unknown | Error |
80+
81+
`stop()` 必须等到 Paused 或资源已不存在才返回;遇到 Pausing 时只等待,不重复发 pause。恢复时
82+
遇到 Pausing 先等 Paused,再发 resume。
83+
84+
### 3.5 未遵循当前 SDK 约定的实现
85+
86+
- create 和 start 在 SDK 内置 readiness 之后又调用 adapter `waitUntilReady()`,重复轮询,并让
87+
`skipHealthCheck` 失效。
88+
- adapter 已有 ping 失败时用命令兜底的需求,但没有通过 SDK 的 custom `healthCheck` 接口接入。
89+
- bound stop/delete 仍通过额外的 SandboxManager 操作,没有使用 `sandbox.pause()` / `kill()`
90+
当前 SDK 会在这两个 instance 方法中同步失效 endpoint cache。
91+
- `getInfo()` 为读取生命周期信息强制 connect execd,导致 Paused sandbox 被错误返回为不存在。
92+
- 命令 exit code 忽略 SDK 已提供的 `execution.exitCode`
93+
- SDK 0.1.10 支持 handler `skipAccumulation`;adapter 已自行使用有界 buffer 时应禁用 SDK 的重复
94+
日志累积,避免长输出双份占用内存。
95+
- OpenSandbox metadata 实际要求 `Record<string, string>`,当前 provider 类型和透传中仍有
96+
`any` / `unknown` 未归一化。
97+
98+
## 4. 开发设计
99+
100+
### 4.1 Manager 作用域
101+
102+
增加内部 `withSandboxManager()`:每次顶层管理操作创建一个 manager,通过 callback 复用,
103+
最后无条件 close。查找、按 id 获取状态、pause、kill 和状态轮询都接收同一个 manager。
104+
105+
### 4.2 远端状态等待
106+
107+
增加统一状态轮询 helper,输入 sandbox id、期望 adapter 状态和 info getter:
108+
109+
- 404 视为 `UnExist`
110+
- 到达期望状态后返回最新 info。
111+
- Error 立即抛出 ConnectionError。
112+
- 超时抛出 SandboxStateError,并保留当前状态和期望状态。
113+
114+
stop 使用 `Stopped | UnExist` 作为完成条件。ensure/start 对 Pausing 使用 `Stopped`,对
115+
Creating/Resuming 使用 `Running`
116+
117+
### 4.3 生命周期操作
118+
119+
- `create()`:使用 `Sandbox.create()`,metadata 归一化为字符串,接入 custom health check;不再
120+
额外调用 adapter readiness。
121+
- `connect()`:同 id 已绑定时直接返回;新连接成功后替换绑定并关闭旧的独立 Sandbox client。
122+
- `ensureRunning()`:优先读取已绑定 sandbox 的 info;未绑定时按 `metadata.sessionId` 查找。
123+
Running 复用、Paused resume、Pausing 等待后 resume、Creating/Resuming 等待后 connect、Deleting
124+
`allowCreate` 决定等待重建或报错。
125+
- `start()`:复用 ensure 语义但禁止创建;bound sandbox 使用 instance resume。
126+
- `stop()`:bound sandbox 使用 `sandbox.pause()`,unbound sandbox 使用 manager pause;等待 Paused
127+
后返回,不清除绑定。
128+
- `delete()`:bound target 使用 `sandbox.kill()`,unbound/显式其他 id 使用 manager kill;删除后
129+
close 并清除匹配绑定。
130+
- `getInfo()`:bound 时用 `sandbox.getInfo()`;unbound 时直接用 manager 的 lifecycle info,不为
131+
Paused sandbox 建立 execd 连接。
132+
- `close()`:未绑定时也成功,释放后清除本地引用,不改变远端状态。
133+
134+
### 4.4 Readiness 与命令输出
135+
136+
`/ping` + `true` 命令兜底封装为接收明确 Sandbox 参数的 health check,传给 SDK 的
137+
create/connect/static resume。instance resume 先跳过默认检查,再调用 SDK `waitUntilReady()` 并传入
138+
同一个 custom health check,保证只有一套轮询。
139+
140+
命令结果优先使用 `execution.exitCode`,旧服务没有返回时才解析 error value/traceback。execute 和
141+
executeStream handler 开启 `skipAccumulation`,adapter 继续用 `BoundedOutputBuffer` 控制输出上限。
142+
143+
### 4.5 类型与测试收敛
144+
145+
- touched provider config 使用 `type`,移除 `any`
146+
- `OpenSandboxConfigType` 从共享 create spec 派生,只收窄 image、metadata、volumes。
147+
- Adapter 测试使用统一 SDK sandbox/manager factory,移除访问真实保留端口的伪单测。
148+
- 错误类自身行为只留在 errors 测试,不在 adapter 测试重复验证。
149+
- uploadRecovery 的纯逻辑留在专属测试;adapter 文件只验证一次上传失败恢复的 wiring,不重复每个
150+
helper 分支。
151+
- 增加 pause 完成等待、Pausing 后 resume、bound Running client 复用、paused getInfo、幂等 close、
152+
transport 替换释放和 SDK exitCode 测试。
153+
- 集成测试恢复 stop/start contract,并修正 `timeout``timeoutSeconds`
154+
155+
## 5. 验收标准
156+
157+
1. 5 分钟闲置 stop 对 OpenSandbox 发 pause,不发 delete。
158+
2. stop 仅在 Paused/不存在后完成,本地 stopped 不领先于远端状态。
159+
3. 后续 ensure/start 恢复同一个 OpenSandbox id,工作区状态保留。
160+
4. 同一 adapter 重复 ensure Running 不创建新的 SDK Sandbox client。
161+
5. 所有临时 SandboxManager 和被替换/断开的独立 Sandbox client 都会释放 transport。
162+
6. Paused sandbox 的 getInfo 可正常返回,不依赖 execd connect。
163+
7. OpenSandbox 单元测试、adapter build 和格式检查通过;按用户要求不运行仓库全量测试。
164+
165+
## 6. TODO
166+
167+
- [x] 完整审查 OpenSandboxAdapter、调用方、现有测试和当前 OpenSandbox SDK。
168+
- [x] 明确已有 metadata 远端复用和缺失的本地 client 复用。
169+
- [x] 重构状态映射、manager 作用域和 lifecycle 轮询。
170+
- [x] 改造 create/connect/ensure/start/stop/delete/getInfo/close。
171+
- [x] 接入 SDK custom health check、exitCode 和 skipAccumulation。
172+
- [x] 收敛 provider 类型和 upload recovery 类型。
173+
- [x] 清理并补充 OpenSandbox adapter 单元测试和集成测试配置。
174+
- [x] 更新 OpenSandbox SDK 依赖及 lockfile(已确认只支持 0.1.10,不保留 0.1.6 兼容)。
175+
- [x] 只运行 sandbox-adapter 定向测试、build、格式与 diff 检查。

pnpm-lock.yaml

Lines changed: 7 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sdk/sandbox-adapter/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"test": "vitest run --config ./vitest.config.ts"
3131
},
3232
"dependencies": {
33-
"@alibaba-group/opensandbox": "^0.1.5"
33+
"@alibaba-group/opensandbox": "^0.1.10"
3434
},
3535
"devDependencies": {
3636
"@types/node": "catalog:",

0 commit comments

Comments
 (0)