跳转到内容
本站为社区维护的非官方中文翻译,与 Cloudflare, Inc. 无关联。本站不提供账户登录,请勿在任何非官方站点输入您的 Cloudflare 账户信息。官方文档:developers.cloudflare.com

sandbox 停止时运行代码

最后更新 查看 MarkdownAgent 设置

container 停止时,运行时不会唤醒 Durable Object。Durable Object 在内存中时会监视 container,并在每条命令和每次 alarm 上检查是否错过了停止。本例中,Durable Object 还会自行停止空闲 sandbox,因此记录会显示每个 sandbox 为何停止。

前提条件

记录每次停止

  1. 在 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 生命周期。

  2. 在 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() 会以该原因拒绝。请在记录保存后再放入你自己的代码。

  3. 添加记录 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);
            }
        }
    }
  4. 替换 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。

  5. 添加 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。

  6. 向 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 安全。

  7. 部署你的 Worker:

    npx wrangler deploy
  8. 运行命令、停止 sandbox,并读取记录,使用 Wrangler 打印的 workers.dev URL:

    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() 的错误。

相关资源

这篇文档对您有帮助吗?