启动服务器,使其持续运行到容器停止,例如开发服务器、隧道或代码内核。与作业不同,不会有任何进程等待服务器退出。Durable Object 会在首次使用时启动服务器,等待服务器响应,并在每个新容器中重新启动服务器。
对于会运行到完成的进程(例如构建或 Agent 任务),请参阅 运行后台进程。如果 sandbox 只运行一个 server 且没有你需要保留的文件,可以将该 server 作为主进程,如 预览 Web 应用 所示。主进程退出时,实例会停止,其文件也会随之结束。
- 带有 Durable Object 的 Worker,该 Durable Object 使用 Durable Object 调度策略 启动 container。要创建一个,请参阅 运行 Linux 命令。
-
添加用于启动、检查和停止 server 的 shell 脚本和函数:
src/index.jsjs const SERVERS = "/var/lib/servers"; const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000; // Defines current(), which reads the process ID in a directory into $pid // when the process started in this instance. const CURRENT = `current() { read -r pid boot 2>/dev/null <"$1/pid" && [ "$boot" = "$(cat /proc/sys/kernel/random/boot_id)" ] }`; // Starts the server unless it already runs. The server runs in its own // process group, and writes its process ID and output to its directory. const SERVE = `${CURRENT} dir=$1; shift current "$dir" && kill -0 "$pid" 2>/dev/null && exit 0 mkdir -p "$dir" && rm -f "$dir/pid" setsid sh -c 'echo "$$ $(cat /proc/sys/kernel/random/boot_id)" >"$0/pid"; exec "$@"' \\ "$dir" "$@" >"$dir/log" 2>&1`; // Prints the end of the log and exits with 0 when the server has exited. const EXITED = `${CURRENT} current "$1" || exit 1 kill -0 "$pid" 2>/dev/null && exit 1 tail -n 20 "$1/log"`; // Stops the process group, with SIGKILL if the server still runs after // 10 seconds. Once the server exits, removes its process ID. const STOP = `${CURRENT} current "$1" || exit 0 kill -s TERM -- "-$pid" 2>/dev/null timeout 10 sh -c 'while kill -0 "$0" 2>/dev/null; do sleep 0.1; done' "$pid" || kill -s KILL -- "-$pid" 2>/dev/null while kill -0 "$pid" 2>/dev/null; do sleep 0.1; done rm -f "$1/pid"`; async function startServer(container, dir, argv, options = {}) { // Ignoring the output lets the server outlive this request. await container.exec(["sh", "-c", SERVE, "sh", dir, ...argv], { ...options, stdout: "ignore", stderr: "ignore", }); } async function serverExited(container, dir) { const check = await container.exec(["sh", "-c", EXITED, "sh", dir]); const output = await check.output(); return output.exitCode === 0 ? new TextDecoder().decode(output.stdout) : undefined; } async function waitForServer(container, dir, port, timeoutMs) { const server = container.getTcpPort(port); const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { try { const response = await server.fetch("http://server/", { signal: AbortSignal.timeout(1_000), }); await response.body?.cancel(); return server; } catch { const log = await serverExited(container, dir); if (log !== undefined) { throw new Error(`The server exited: ${log}`); } await scheduler.wait(500); } } throw new Error("The server did not answer in time"); } async function stopServer(container, dir) { const stop = await container.exec(["sh", "-c", STOP, "sh", dir]); const exitCode = await stop.exitCode; if (exitCode !== 0) { throw new Error(`Stopping the server failed with exit code ${exitCode}`); } }src/index.tsts const SERVERS = "/var/lib/servers"; const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000; // Defines current(), which reads the process ID in a directory into $pid // when the process started in this instance. const CURRENT = `current() { read -r pid boot 2>/dev/null <"$1/pid" && [ "$boot" = "$(cat /proc/sys/kernel/random/boot_id)" ] }`; // Starts the server unless it already runs. The server runs in its own // process group, and writes its process ID and output to its directory. const SERVE = `${CURRENT} dir=$1; shift current "$dir" && kill -0 "$pid" 2>/dev/null && exit 0 mkdir -p "$dir" && rm -f "$dir/pid" setsid sh -c 'echo "$$ $(cat /proc/sys/kernel/random/boot_id)" >"$0/pid"; exec "$@"' \\ "$dir" "$@" >"$dir/log" 2>&1`; // Prints the end of the log and exits with 0 when the server has exited. const EXITED = `${CURRENT} current "$1" || exit 1 kill -0 "$pid" 2>/dev/null && exit 1 tail -n 20 "$1/log"`; // Stops the process group, with SIGKILL if the server still runs after // 10 seconds. Once the server exits, removes its process ID. const STOP = `${CURRENT} current "$1" || exit 0 kill -s TERM -- "-$pid" 2>/dev/null timeout 10 sh -c 'while kill -0 "$0" 2>/dev/null; do sleep 0.1; done' "$pid" || kill -s KILL -- "-$pid" 2>/dev/null while kill -0 "$pid" 2>/dev/null; do sleep 0.1; done rm -f "$1/pid"`; type ServerOptions = { cwd?: string; env?: Record<string, string> }; async function startServer( container: Container, dir: string, argv: string[], options: ServerOptions = {}, ): Promise<void> { // Ignoring the output lets the server outlive this request. await container.exec(["sh", "-c", SERVE, "sh", dir, ...argv], { ...options, stdout: "ignore", stderr: "ignore", }); } async function serverExited( container: Container, dir: string, ): Promise<string | undefined> { const check = await container.exec(["sh", "-c", EXITED, "sh", dir]); const output = await check.output(); return output.exitCode === 0 ? new TextDecoder().decode(output.stdout) : undefined; } async function waitForServer( container: Container, dir: string, port: number, timeoutMs: number, ): Promise<Fetcher> { const server = container.getTcpPort(port); const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { try { const response = await server.fetch("http://server/", { signal: AbortSignal.timeout(1_000), }); await response.body?.cancel(); return server; } catch { const log = await serverExited(container, dir); if (log !== undefined) { throw new Error(`The server exited: ${log}`); } await scheduler.wait(500); } } throw new Error("The server did not answer in time"); } async function stopServer(container: Container, dir: string): Promise<void> { const stop = await container.exec(["sh", "-c", STOP, "sh", dir]); const exitCode = await stop.exitCode; if (exitCode !== 0) { throw new Error(`Stopping the server failed with exit code ${exitCode}`); } }如果目录中的服务器已在运行,
SERVE不会启动新进程;而没有服务器的新容器则会启动一个。Durable Object 重启而容器仍在运行时,也适用此行为。进程 ID 文件还会记录 boot ID,每个实例中该值都会变化。snapshot 会恢复该文件,新实例会复用相同的进程 ID,因此恢复的进程 ID 可能属于无关进程。
current()会忽略来自其他实例的进程 ID。setsid会在新的进程组中启动 server,因此STOP也会停止该 server 启动的进程。运行SERVE的 shell 会留在 container 中直到 server 退出。不要改用&启动 server:shell 启动后台作业时会忽略SIGINT。STOP会在服务器退出后移除进程 ID,因此紧接着在同一目录启动的服务器不会显示为已退出。忽略SIGTERM的服务器会在 10 秒后收到SIGKILL。waitForServer()将任何 HTTP 响应视为就绪。如果 server 先退出,它会抛出包含日志末尾的错误。日志会一直增长直到 server 停止,因此请保持繁忙 server 的输出较小,或将其发送到挂载的 bucket。 -
向 Durable Object 添加构造函数。当重启后的 Durable Object 发现 container 仍在运行时,它会再次设置不活动超时:
src/index.tsts 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), ); } } }然后添加先启动 server 再向其发送请求、以及停止它的方法:
src/index.tsts const WEB = `${SERVERS}/web`; const WEB_PORT = 8080; const WEB_SERVER = [ "node", "--eval", `require("node:http") .createServer((req, res) => res.end("hello from the server\\n")) .listen(${WEB_PORT})`, ]; export class MyContainer extends DurableObject<Env> { // ... // The server start that requests in progress share private starting?: Promise<Fetcher>; async fetchServer(request: Request): Promise<Response> { const container = this.ctx.container; if (!container) { throw new Error("No container is configured"); } if (!container.running) { container.start({ image: "cloudflare/debian-trixie", entrypoint: ["sleep", "infinity"], enableInternet: false, }); await container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS); } this.starting ??= startServer(container, WEB, WEB_SERVER) .then(() => waitForServer(container, WEB, WEB_PORT, 30_000)) .finally(() => { this.starting = undefined; }); const server = await this.starting; return server.fetch(request); } async stopWeb(): Promise<void> { const container = this.ctx.container; if (container?.running) { await stopServer(container, WEB); } } }同时到达的请求会共用一次启动操作。服务器记录进程 ID 之前,
startServer()就会返回,因此在这段时间内,第二次SERVE会启动另一个服务器。第二个服务器会因端口冲突而失败,并覆盖进程 ID 文件,导致STOP无法停止第一个服务器。服务器响应后,每个请求都会再次运行SERVE;服务器仍在运行时不会启动新进程,服务器已退出时则会重新启动。正在运行的 server 不会让 container 保持运行。通过 Durable Object 到达 server 的请求会像其他请求一样让它保持运行。不活动超时结束时,container 会停止,server 也随之停止。更多信息请参阅 Sandbox 生命周期。
-
向 Worker 添加调用这些方法的路由:
src/index.jsjs export default { async fetch(request, env) { const url = new URL(request.url); if (url.pathname !== "/server") { return new Response("Not found", { status: 404 }); } const sandbox = env.MY_CONTAINER.getByName("sandbox"); if (request.method === "DELETE") { await sandbox.stopWeb(); return new Response(null, { status: 202 }); } // getTcpPort() accepts only http:// URLs. const forward = new Request("http://container/", request); return sandbox.fetchServer(forward); }, };src/index.tsts export default { async fetch(request: Request, env: Env): Promise<Response> { const url = new URL(request.url); if (url.pathname !== "/server") { return new Response("Not found", { status: 404 }); } const sandbox = env.MY_CONTAINER.getByName("sandbox"); if (request.method === "DELETE") { await sandbox.stopWeb(); return new Response(null, { status: 202 }); } // getTcpPort() accepts only http:// URLs. const forward = new Request("http://container/", request); return sandbox.fetchServer(forward); }, } satisfies ExportedHandler<Env>; -
部署你的 Worker:
npx wrangler deployyarn wrangler deploypnpm wrangler deploy -
向 server 发送请求。将示例 hostname 替换为 Wrangler 打印的
workers.devURL:curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/server第一个请求会启动 container 和 server,并等到 server 应答:
hello from the server -
停止 server,然后发送另一个请求:
curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/server --request DELETE curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/serverDELETE请求会以202响应。下一个请求会再次启动 server 并打印相同响应。
- 运行后台进程:运行到完成的进程。
- 预览 Web 应用:作为主进程的 Web server。
- Sandbox 生命周期
getTcpPort()