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

迁移后台进程

最后更新 查看 MarkdownAgent 设置

在 Sandbox SDK 0.12 中,startProcess() 返回 Process 对象,容器会保留进程表供后续请求查询。在 1.0 中,你的 Durable Object 在容器中为每个进程保留一个目录。每个 Process 方法都成为接受进程 ID 的 Durable Object 方法。

开始之前

你的 1.0 类已替换 0.12 Sandbox 类,并具有 替换 Sandbox 类 中的 container getter、ensureRunning() 方法,以及 ENV 和 INACTIVITY_TIMEOUT_MS 常量。

本页替换 startProcess()、Process 方法、listProcesses()、getProcess()、getProcessLogs()、streamProcessLogs()、killProcess()、killAllProcesses()、cleanupCompletedProcesses() 以及进程回调。它不涵盖请求打开期间完成的命令。那些请参阅 更改命令调用。

添加进程方法

  1. 将运行后台进程中的脚本和方法添加到你的类中,包括 alarm 处理程序,以及跟踪后台进程中的 follow() 和 waitForLog()。

    一个 Durable Object 只有一个 alarm。如果你的类已有 alarm() handler(例如 通过一次部署迁移所有 sandbox 中的那个),请将进程检查合并进去,而不是添加第二个。

  2. 更改 startProcess(),用 ensureRunning() 启动容器,并用 0.12 提供给它的工作目录和环境运行每个进程:

    src/index.tsts
    export class MySandbox extends DurableObject<Env> {
        // ...
    
        async startProcess(id: string, argv: string[]): Promise<boolean> {
            await this.ensureRunning();
    
            // mkdir fails when another process already uses the ID.
            const dir = `${ROOT}/${id}`;
            await run(this.container, ["mkdir", "-p", ROOT]);
            const created = await run(this.container, ["mkdir", dir]);
    
            if (created.exitCode !== 0) {
                return false;
            }
    
            // Ignoring the output lets the process outlive this request.
            const command = ["sh", "-c", RUN, "sh", dir, ...argv];
            await this.container.exec(command, {
                cwd: "/workspace",
                env: ENV,
                stdout: "ignore",
                stderr: "ignore",
            });
    
            // exec() returns before the process records its ID. Wait for it, so
            // a request that follows the output right away finds it running.
            for (let i = 0; i < 50; i++) {
                if ((await this.status(id))?.state !== "starting") break;
                await scheduler.wait(100);
            }
    
            await this.ctx.storage.put(`process:${id}`, argv);
            await this.ctx.storage.setAlarm(Date.now() + 60_000);
            return true;
        }
    }

    0.12 在 /workspace 中运行每个进程,并带有来自 envVars 的变量。exec() 从 / 启动,即使镜像设置了 WORKDIR。它不会继承 start() 设置的变量,PATH 除外。

  3. 从第 1 步添加的脚本中删除 INACTIVITY_TIMEOUT_MS。你的类文件已定义此常量,因此构造函数和 alarm 处理程序会继续使用类的不活动超时设置。

在本页中,sandbox 是 getByName() 返回的 stub。

启动进程

0.12 在 bash 中运行每条命令字符串。要保留字符串,请用 ["bash", "-c", command] 运行它:

src/index.ts (0.12)ts
const process = await sandbox.startProcess("npm run dev", {
    processId: "dev",
});
src/index.ts (1.0)ts
const started = await sandbox.startProcess("dev", [
    "bash",
    "-c",
    "npm run dev",
]);

每个进程都需要 ID。0.12 生成 ID 的地方(例如 proc_1790379992394_6snsuw)请使用 crypto.randomUUID()。ID 会成为目录名,因此只使用路径中安全的字符。

当已有进程使用该 ID 时,startProcess() 返回 false。0.12 会替换记录,而第一个进程会在没有记录的情况下继续运行。已结束的进程会保留其目录,因此 startProcess() 也会对该 ID 返回 false。要再次启动该 ID,请先删除目录,如 停止进程 所述。

python3 -m http.server(0.12 用作预览服务器)在 1.0 容器中启动时会退出。有关原因和替换项,请参阅 主机名长于 DNS 标签。

删除 timeout 选项;它在 0.12 中无法停止后台进程。要限制进程的运行时间,请使用 timeout 命令启动进程,例如 ["timeout", "300", "npm", "test"]。时间到时,命令会以代码 124 退出;如果进程忽略 SIGTERM,使用 --kill-after 时则会以 137 退出。

有关 sessionId 选项,请参阅 替换会话。

检查并列出进程

将 getProcess() 和 Process.getStatus() 替换为 status(),它对没有任何进程使用的 ID 返回 undefined。每个 0.12 状态映射到 1.0 状态:

0.12 状态 1.0 状态
starting starting
running running,带有 pid
completed exited,exitCode 为 0
failed exited,带有另一个 exitCode
killed exited,SIGTERM 后 exitCode 为 143,SIGKILL 后为 137
error startProcess() 抛出错误
— lost,当进程结束却没有记录退出码时

要替换 listProcesses(),添加列出进程目录的方法:

src/index.tsts
export class MySandbox extends DurableObject<Env> {
    // ...

    async listProcesses(): Promise<{ id: string; status: Status }[]> {
        if (!this.container.running) {
            return [];
        }

        const { stdout } = await run(this.container, ["ls", ROOT]);
        const processes = [];

        for (const id of stdout.split("\n").filter((line) => line !== "")) {
            const status = await this.status(id);

            if (status) {
                processes.push({ id, status });
            }
        }

        return processes;
    }
}

目录会保留到你删除它们,就像已结束的进程留在 0.12 表中一样。alarm 会在一分钟内删除已结束进程的 process: 键,因此请列出目录以找到已结束的进程。

读取日志

将 getProcessLogs() 和 Process.getLogs() 替换为对每个流调用 logs():

src/index.ts (0.12)ts
const { stdout, stderr } = await sandbox.getProcessLogs("dev");
src/index.ts (1.0)ts
const stdout = await sandbox.logs("dev", "stdout");
const stderr = await sandbox.logs("dev", "stderr");

将 streamProcessLogs() 替换为 follow()。它返回一个 Response,跟随一个流直到进程退出,然后发送带有退出码的 exit 事件。

0.12 发送没有名称的事件,每个事件的 data 字段中有一个 LogEvent 对象。follow() 将每个事件命名为 stdout、stderr 或 exit。输出事件的 data 字段保存 JSON 编码的输出块,exit 事件的 data 字段保存 { "exitCode": <CODE> }。将 parseSSEStream() 替换为每个事件名称的监听器,或读取每个事件的 event 行并用 JSON.parse() 解析其 data 字段。

等待输出或退出

将 Process.waitForLog() 替换为 waitForLog()。它返回状态,而 0.12 会抛出错误:

src/index.ts (0.12)ts
const { line } = await process.waitForLog("ready.", 30_000);
src/index.ts (1.0)ts
const result = await sandbox.waitForLog(
    "dev",
    literal("ready."),
    30_000,
);

if (result?.state !== "matched") {
    throw new Error(`The server did not start: ${result?.state}`);
}

状态为 timed-out 时对应 0.12 抛出 ProcessReadyTimeoutError,为 exited 时对应抛出 ProcessExitedBeforeReadyError。

该模式是 grep -E 扩展正则表达式。0.12 按原样匹配字符串模式,因此请将每个字符串模式通过转义函数传入:

src/index.tsts
// Escapes a 0.12 string pattern, so that it matches as written.
function literal(text: string): string {
    return text.replace(/[\\^$.|?*+()[\]{}]/g, "\\$&");
}

没有它时,模式 [ok] 会匹配包含 o 或 k 的任何行。将 0.12 RegExp 作为其 source 传入,并将 \d 替换为 grep 需要的 [0-9]。

要替换 Process.waitForExit(),添加一个检查状态直到进程结束的方法:

src/index.tsts
export class MySandbox extends DurableObject<Env> {
    // ...

    async waitForExit(id: string, timeoutMs: number) {
        const deadline = Date.now() + timeoutMs;
        let status = await this.status(id);

        while (
            (status?.state === "running" || status?.state === "starting") &&
            Date.now() < deadline
        ) {
            await scheduler.wait(500);
            status = await this.status(id);
        }

        return status;
    }
}

当 timeoutMs 先到期时,该方法返回状态 running 或 starting。

将 Process.waitForPort() 替换为对端口的请求,如 预览 Web 应用 所示。

停止进程

将 Process.kill() 和 killProcess() 替换为 stop():

src/index.ts (0.12)ts
await sandbox.killProcess("dev");
src/index.ts (1.0)ts
await sandbox.stop("dev");

stop() 向进程组发送 SIGTERM,它会到达该命令启动的进程。0.12 会在 5 秒后向仍在运行的任何进程发送 SIGKILL,并忽略 signal 参数。要停止忽略 SIGTERM 的进程,请在 stop() 中将 TERM 改为 KILL。

将 killAllProcesses() 替换为对 listProcesses() 返回的每个正在运行的进程调用 stop()。移除 cleanupCompletedProcesses() 和 autoCleanup 选项,它们在 0.12 中不做任何事。要删除已结束进程的目录,请对其运行 rm -rf。

替换回调

运行后台进程中的 alarm 每分钟检查每个进程,并在任何进程运行时保持容器运行。每个 0.12 回调都会移到你自己的代码:

0.12 1.0
onStart startProcess() 返回 true 之后的代码。
onOutput follow(),或用于目前输出的 logs()。
onExit alarm 的 process.ended 分支,在退出后一分钟内。要立即得到退出,请调用 waitForExit()。
onError 抛出或返回 false 的 startProcess() 调用。

切换后再次启动进程

1.0 看不到 0.12 启动的进程。status() 对它们的 ID 返回 undefined,listProcesses() 会省略它们。0.12 进程表会随其容器结束。切换后请启动应用需要的每个进程,例如在第一次调用 ensureRunning() 的方法中。

检查进程

部署切换后,用 0.12 进程的 ID(例如 dev)启动进程。startProcess() 返回 true,status("dev") 会报告它:

{ "state": "running", "pid": 142 }

logs("dev", "stdout") 返回来自 /workspace 的输出,并带有 ENV 中的变量。stop("dev") 之后,listProcesses() 返回退出码:

[{ "id": "dev", "status": { "state": "exited", "exitCode": 143 } }]

相关资源

这篇文档对您有帮助吗?