每个用户都有一个 registry:记录该用户 sandbox 的 Durable Object。本例中,当 sandbox 首次运行命令时,Worker 会将其添加到 registry。要报告哪些 sandbox 正在运行,Worker 会询问每个 sandbox 的 Durable Object 其 container 是否在运行。
- 带有 Durable Object 的 Worker,该 Durable Object 使用 Durable Object 调度策略 启动 container。要创建一个,请参阅 运行 Linux 命令。
-
在
wrangler.jsonc中,为 registry 类添加绑定,并在exports中声明该类:{ "durable_objects": { "bindings": [ { "class_name": "MyContainer", "name": "MY_CONTAINER", }, { "class_name": "SandboxDirectory", "name": "SANDBOX_DIRECTORY", }, ], }, "exports": { "MyContainer": { "type": "durable-object", "storage": "sqlite", }, "SandboxDirectory": { "type": "durable-object", "storage": "sqlite", }, }, }[[durable_objects.bindings]] class_name = "MyContainer" name = "MY_CONTAINER" [[durable_objects.bindings]] class_name = "SandboxDirectory" name = "SANDBOX_DIRECTORY" [exports.MyContainer] type = "durable-object" storage = "sqlite" [exports.SandboxDirectory] type = "durable-object" storage = "sqlite"如果你的 Worker 改用
migrations数组声明类,请添加带有新tag和"new_sqlite_classes": ["SandboxDirectory"]的迁移。更多信息请参阅 Durable Object 类迁移(旧版)。npx wrangler typesyarn wrangler typespnpm wrangler types -
将 registry 类添加到 Worker。每个用户获得一个
SandboxDirectory,它会在 SQLite 中存储该用户的 sandbox session:src/index.tsts export class SandboxDirectory extends DurableObject<Env> { constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); ctx.storage.sql.exec( "CREATE TABLE IF NOT EXISTS sandboxes (session TEXT PRIMARY KEY, created_at INTEGER NOT NULL)", ); } add(session: string) { // Keep the time of the first command when the same session runs again this.ctx.storage.sql.exec( "INSERT OR IGNORE INTO sandboxes (session, created_at) VALUES (?, ?)", session, Date.now(), ); } remove(session: string) { this.ctx.storage.sql.exec("DELETE FROM sandboxes WHERE session = ?", session); } list() { return this.ctx.storage.sql .exec<{ session: string }>("SELECT session FROM sandboxes ORDER BY created_at") .toArray() .map((row) => row.session); } } -
在
MyContainer中设置不活动超时,并添加报告和停止 container 的方法。在文件顶部定义超时,并添加构造函数,以便在重启后的 Durable Object 发现 container 仍在运行时设置超时:src/index.tsts const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000; export class MyContainer extends DurableObject<Env> { constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); const container = ctx.container; if (container?.running) { void ctx.blockConcurrencyWhile(() => container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS), ); } } }在
exec()中,于start()之后、if (!container.running)块内设置超时:src/index.tsts await container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS);然后将这两个方法添加到
MyContainer:src/index.tsts export class MyContainer extends DurableObject<Env> { // ... isRunning() { // Reading `running` does not start the container return this.ctx.container?.running ?? false; } async destroy() { await this.ctx.container?.destroy(); } }列出请求可能会唤醒重启后的 Durable Object,而它启动时没有
exec()设置的超时。构造函数会再次设置超时,因此 sandbox 不会在列出请求结束后很快停止。更多信息请参阅 Sandbox 生命周期。 -
向 Worker 添加路由:将每个 sandbox 命名为
<OWNER>/<SESSION>,在运行命令前记录 session,列出带状态的 session,并在停止 container 后移除 session:src/index.tsts export default { async fetch(request: Request, env: Env): Promise<Response> { const url = new URL(request.url); const [, users, owner, sandboxes, session] = url.pathname.split("/"); if (users !== "users" || !owner || sandboxes !== "sandboxes") { return new Response("Not found", { status: 404 }); } const directory = env.SANDBOX_DIRECTORY.getByName(owner); if (request.method === "GET" && !session) { const sessions = await directory.list(); // Call the Durable Object of every session in parallel const list = await Promise.all( sessions.map(async (name) => ({ session: name, running: await env.MY_CONTAINER.getByName(`${owner}/${name}`).isRunning(), })), ); return Response.json(list); } if (!session) { return new Response("Not found", { status: 404 }); } const sandbox = env.MY_CONTAINER.getByName(`${owner}/${session}`); if (request.method === "POST") { const { argv } = (await request.json()) as { argv: string[] }; await directory.add(session); return Response.json(await sandbox.exec(argv)); } if (request.method === "DELETE") { await sandbox.destroy(); await directory.remove(session); return new Response(null, { status: 204 }); } return new Response("Not found", { status: 404 }); }, };container 已停止的 sandbox 会以
running: false留在列表中,直到 Worker 移除其 session。请先认证调用方,并根据其身份派生 owner,而不是从 URL 读取。否则任何人都可以列出、使用或停止其他用户的 sandbox。更多信息请参阅 Sandbox 安全。
-
部署你的 Worker:
npx wrangler deployyarn wrangler deploypnpm wrangler deploy -
为用户
ada启动两个 sandbox,使用 Wrangler 打印的workers.devURL:curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/users/ada/sandboxes/notes --request POST --json '{"argv":["uname","-s"]}' curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/users/ada/sandboxes/tests --request POST --json '{"argv":["uname","-s"]}' -
列出它们:
curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/users/ada/sandboxes[ { "session": "notes", "running": true }, { "session": "tests", "running": true } ] -
停止其中一个,然后再次列出:
curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/users/ada/sandboxes/tests --request DELETE curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/users/ada/sandboxes[{ "session": "notes", "running": true }]
registry 只列出 Worker 记录的 sandbox。Containers API 会列出 Container 应用启动的每个 sandbox,包括失败的或 registry 遗漏的 sandbox。对于每个 sandbox,实例列表会返回传给 getByName() 的名称和状态:
curl "https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/containers/applications/<APPLICATION_ID>/instances?name_prefix=ada/" \
--header "Authorization: Bearer <API_TOKEN>"- 应用 ID 会出现在
wrangler containers list中。应用以 Worker 和 Durable Object 类命名,例如sandbox-linux-mycontainer。 - API token 需要 Containers Read 权限,该权限覆盖账户中的每个 Container 应用。将其用于管理和清理,而不是用户调用的路由。
name_prefix按名称开头过滤,state=active只返回正在启动、运行或停止的 sandbox。已停止的 sandbox 仍会留在列表中。- 新 sandbox 可能需要几秒钟才会出现,在
wrangler dev下运行的 sandbox 不会出现。
- Sandbox 生命周期:是什么让 sandbox 保持运行,以及是什么会停止它。
- sandbox 停止时运行代码:记录每个 sandbox 何时以及为何停止。
destroy()- SQLite 存储 API