// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // SIGTERM, which the platform sends before it replaces a container. The real `server.mjs` runs against a stand-in // for the Start build: the order is stop taking requests, finish the ones in flight, drain the runtime (usage queue, // exporters, pools), and only then exit. A process that exits on its own signal handler loses the queue. import { type ChildProcess, spawn } from "node:child_process" import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs" import { join } from "node:path" import { fileURLToPath } from "node:url" import { afterEach, describe, expect, it } from "vitest" const WEB = fileURLToPath(new URL("../apps/web/", import.meta.url)) /** The stand-in: `/slow` holds a request, `/start` builds the "runtime", which registers its drain as app.ts does. */ const BUILD = ` import { appendFileSync } from "node:fs" const log = process.env.SHUTDOWN_LOG const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) const note = (line) => appendFileSync(log, line + "\\n") const drainMs = Number(process.env.SHUTDOWN_DRAIN_MS ?? 300) export default { async fetch(request) { const { pathname } = new URL(request.url) if (pathname === "/start") { globalThis[Symbol.for("conusai.app.drain")] = async () => { note("drain began") await sleep(drainMs) note("drain ended") } } if (pathname === "/stream") { return new Response(new ReadableStream({ start(controller) { controller.enqueue("open\\n") } })) } if (pathname === "/slow") { await sleep(400) note("request finished") } return new Response("ok") }, } ` let directory: string | undefined let server: ChildProcess | undefined afterEach(() => { server?.kill("SIGKILL") if (directory) rmSync(directory, { recursive: true, force: true }) directory = undefined }) /** Starts the real server.mjs beside the stand-in, inside apps/web so that hono resolves as it does in the image. */ async function boot( env: Record = {}, ): Promise<{ origin: string; log: string; exited: Promise }> { directory = mkdtempSync(join(WEB, ".shutdown-test-")) mkdirSync(join(directory, "dist", "server"), { recursive: true }) mkdirSync(join(directory, "dist", "client"), { recursive: true }) copyFileSync(join(WEB, "server.mjs"), join(directory, "server.mjs")) writeFileSync(join(directory, "dist", "server", "server.js"), BUILD) const log = join(directory, "shutdown.log") writeFileSync(log, "") const port = 34_000 + Math.floor(Math.random() * 20_000) const child = spawn(process.execPath, ["server.mjs", "--port", String(port)], { cwd: directory, env: { ...process.env, HOSTNAME: "127.0.0.1", SHUTDOWN_LOG: log, ...env }, stdio: "ignore", }) server = child const exited = new Promise((resolve) => child.once("exit", (code) => resolve(code ?? -1))) const origin = `http://127.0.0.1:${port}` for (let attempt = 0; attempt < 100; attempt += 1) { if ( await fetch(`${origin}/health`).then( (answer) => answer.ok, () => false, ) ) return { origin, log, exited } await new Promise((resolve) => setTimeout(resolve, 50)) } throw new Error("server.mjs did not start") } const lines = (log: string): string[] => readFileSync(log, "utf8").split("\n").filter(Boolean) describe("the web server on SIGTERM", () => { it("drains the runtime to its end before the process exits", async () => { const { origin, log, exited } = await boot() await fetch(`${origin}/start`) server?.kill("SIGTERM") expect(await exited).toBe(0) expect(lines(log)).toEqual(["drain began", "drain ended"]) }, 15_000) it("finishes the request in flight before it drains, so its usage is in the queue", async () => { const { origin, log, exited } = await boot() await fetch(`${origin}/start`) const slow = fetch(`${origin}/slow`) await new Promise((resolve) => setTimeout(resolve, 100)) server?.kill("SIGTERM") expect((await slow).status).toBe(200) expect(await exited).toBe(0) expect(lines(log)).toEqual(["request finished", "drain began", "drain ended"]) }, 15_000) it("does not let a stream that never ends hold the drain past its bound", async () => { const { origin, log, exited } = await boot({ SHUTDOWN_REQUESTS_MS: "300" }) await fetch(`${origin}/start`) const stream = await fetch(`${origin}/stream`) expect(stream.status).toBe(200) server?.kill("SIGTERM") expect(await exited).toBe(0) expect(lines(log)).toEqual(["drain began", "drain ended"]) }, 15_000) it("exits at once when no request ever built a runtime", async () => { const { log, exited } = await boot() server?.kill("SIGTERM") expect(await exited).toBe(0) expect(lines(log)).toEqual([]) }, 15_000) it("exits non-zero rather than hang when the drain does not finish", async () => { const { origin, log, exited } = await boot({ SHUTDOWN_DRAIN_MS: "60000", SHUTDOWN_FLUSH_MS: "300" }) await fetch(`${origin}/start`) server?.kill("SIGTERM") expect(await exited).toBe(1) expect(lines(log)).toEqual(["drain began"]) }, 15_000) })