container 停止时,运行时不会唤醒 Durable Object。Durable Object 在内存中时会监视 container,并在每条命令和每次 alarm 上检查是否错过了停止。本例中,Durable Object 还会自行停止空闲 sandbox,因此记录会显示每个 sandbox 为何停止。
- 带有 Durable Object 的 Worker,该 Durable Object 使用 Durable Object 调度策略 启动 container。要创建一个,请参阅 运行 Linux 命令。
-
在
src/index.ts顶部定义检查间隔、空闲限制、不活动超时,以及 Durable Object 保存的记录:src/index.tsts const CHECK_INTERVAL_MS = 60 * 1000; // Stop a sandbox after this long without requests const IDLE_LIMIT_MS = 10 * 60 * 1000; const INACTIVITY_TIMEOUT_MS = 5 * 60 * 1000; interface SandboxRecord { state: "running" | "stopped"; lastActivity: number; reason?: string; exitCode?: number | null; stoppedAt?: number; }将不活动超时保持得比检查间隔更长,以便 container 在 alarm 之间继续运行。
monitor()等待时,不活动超时不会开始计时,因此由 alarm 强制执行空闲限制。如果 alarm 停止,超时仍会在 Durable Object 离开内存后停止 container。更多信息请参阅 Sandbox 生命周期。 -
在
MyContainer中添加构造函数、监视 container 的方法,以及记录停止的方法:src/index.tsts export class MyContainer extends DurableObject<Env> { // ... constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); const container = ctx.container; // A restarted Durable Object loses the earlier `monitor()` call and the // inactivity timeout if (container?.running) { void ctx.blockConcurrencyWhile(() => container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS), ); this.watch(); } } watch() { this.ctx.container?.monitor().then( () => this.onStop("exited", 0), (error: unknown) => { const exitCode = (error as { exitCode?: number }).exitCode; if (exitCode === undefined) { return this.onStop(String(error), null); } return this.onStop("exited", exitCode); }, ); } async onStop(reason: string, exitCode: number | null) { const record = await this.ctx.storage.get<SandboxRecord>("sandbox"); // Record each stop once, even when two checks report it if (record?.state !== "running") { return; } await this.ctx.storage.put<SandboxRecord>("sandbox", { ...record, state: "stopped", reason, exitCode, stoppedAt: Date.now(), }); await this.ctx.storage.deleteAlarm(); console.log(JSON.stringify({ event: "sandbox.stopped", reason, exitCode })); } }当主进程失败时,
monitor()会以带有exitCode的错误拒绝。当你的代码带着原因调用destroy()时,monitor()会以该原因拒绝。请在记录保存后再放入你自己的代码。 -
添加记录
monitor()错过的停止的方法:src/index.tsts export class MyContainer extends DurableObject<Env> { // ... async reconcile() { const record = await this.ctx.storage.get<SandboxRecord>("sandbox"); if (record?.state === "running" && !this.ctx.container?.running) { // A stop that happened while the Durable Object was not in memory has // no exit code await this.onStop("unknown", null); } } } -
替换
exec()。它会检查错过的停止、监视它启动的 container、安排下一次检查,并在命令完成后记录活动:src/index.tsts export class MyContainer extends DurableObject<Env> { // ... // Count the commands that are running. A request in progress keeps the // Durable Object in memory, so the count does not need storage busy = 0; async exec(argv: string[]) { const container = this.ctx.container; if (!container) { throw new Error("No container is configured"); } await this.reconcile(); if (!container.running) { container.start({ image: "cloudflare/debian-trixie", entrypoint: ["sleep", "infinity"], enableInternet: false, }); await container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS); this.watch(); } await this.ctx.storage.put<SandboxRecord>("sandbox", { state: "running", lastActivity: Date.now(), }); await this.ctx.storage.setAlarm(Date.now() + CHECK_INTERVAL_MS); this.busy++; try { const process = await container.exec(argv); const output = await process.output(); return { stdout: new TextDecoder().decode(output.stdout), exitCode: output.exitCode, }; } finally { this.busy--; const record = await this.ctx.storage.get<SandboxRecord>("sandbox"); if (record?.state === "running") { await this.ctx.storage.put<SandboxRecord>("sandbox", { ...record, lastActivity: Date.now(), }); } } } }即使命令运行时间超过空闲限制,alarm 也不会在命令运行期间停止 sandbox。
-
添加 alarm handler:检查 sandbox,并在
IDLE_LIMIT_MS内没有请求后停止它。然后添加返回记录和停止 sandbox 的方法:src/index.tsts export class MyContainer extends DurableObject<Env> { // ... async alarm() { await this.reconcile(); const record = await this.ctx.storage.get<SandboxRecord>("sandbox"); if (record?.state !== "running") { return; } if (this.busy === 0 && Date.now() - record.lastActivity > IDLE_LIMIT_MS) { await this.ctx.container?.destroy("idle"); return; } await this.ctx.storage.setAlarm(Date.now() + CHECK_INTERVAL_MS); } async status() { return (await this.ctx.storage.get<SandboxRecord>("sandbox")) ?? null; } async stop() { await this.ctx.container?.destroy("stopped"); } }传给
destroy()的原因会到达monitor(),因此空闲停止会记录idle,通过stop()的停止会记录stopped。Durable Object 只有一个 alarm。如果你的类已有
alarm()handler,请将停止和空闲检查合并进去,并将 alarm 设为任何检查所需的最早时间。更多信息请参阅 Alarms。 -
向 Worker 添加路由。
GET返回记录,DELETE停止 sandbox,POST运行命令:src/index.tsts export default { async fetch(request: Request, env: Env): Promise<Response> { const sandbox = env.MY_CONTAINER.getByName("sandbox"); if (request.method === "GET") { return Response.json(await sandbox.status()); } if (request.method === "DELETE") { await sandbox.stop(); return new Response(null, { status: 204 }); } const { argv } = (await request.json()) as { argv: string[] }; return Response.json(await sandbox.exec(argv)); }, };请先认证调用方,以免其他人使用或停止 sandbox。更多信息请参阅 Sandbox 安全。
-
部署你的 Worker:
npx wrangler deployyarn wrangler deploypnpm wrangler deploy -
运行命令、停止 sandbox,并读取记录,使用 Wrangler 打印的
workers.devURL:curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev --request POST --json '{"argv":["uname","-s"]}' curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev --request DELETE curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev{ "state": "stopped", "lastActivity": 1790270030663, "reason": "stopped", "exitCode": null, "stoppedAt": 1790270031496 }再运行一条命令,然后 10 分钟内不发送请求。下一次 alarm 会停止 sandbox,记录会显示
"reason": "idle"。sandbox 运行期间的部署不会记录停止。
reason 字段显示 sandbox 如何停止:
| 原因 | 含义 |
|---|---|
exited |
主进程退出。exitCode 保存其退出码。 |
idle 或 stopped |
你的 Durable Object 带着该原因调用了 destroy()。 |
unknown |
Durable Object 不在内存中时 container 停止,后续命令或 alarm 发现它已停止。退出码不可用。 |
| 任何其他文本 | container 失败时来自 monitor() 的错误。 |
- Sandbox 生命周期:是什么让 sandbox 保持运行,以及是什么会停止它。
- 自动保存 sandbox:在空闲停止前保存 snapshot。
- 列出用户的 sandbox
monitor()和destroy()- Durable Object alarms