From 6a2457315e671333cb928f3f0daa6fed50a6fe3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iwo=20Strzebo=C5=84ski?= Date: Thu, 29 Oct 2026 14:35:40 +0000 Subject: [PATCH] Add a switch to turn job dispatch off, and use it in the dev stack The scheduler sends a queued job to any idle claimed printer in any farm. The dashboard dev stack is wired to the real printers, so testing the job upload UI there would have started a real print on the first idle one. DISPATCH_ENABLED=false (exactly that string) disables the timer and the nudge from POST /jobs; any other value that looks like an attempt to disable it is warned about, because a typo that silently leaves it on is the dangerous direction. scripts/e2e-camera.sh now always runs the backend with it off. --- .env.example | 5 +++++ scripts/e2e-camera.sh | 6 +++++- src/dispatch/enabled.ts | 25 +++++++++++++++++++++++++ src/dispatch/index.ts | 9 +++++++++ tests/dispatch.test.ts | 19 +++++++++++++++++++ 5 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/dispatch/enabled.ts diff --git a/.env.example b/.env.example index 7c2394a..36ef21d 100644 --- a/.env.example +++ b/.env.example @@ -53,3 +53,8 @@ SUPABASE_URL= # refuses to start without it, because anything that knows this secret can # connect as any farm's gateway. Generate one with: openssl rand -hex 32 EDGE_JWT_SECRET= + +# The scheduler sends queued jobs to any idle, claimed printer in any farm. +# Set to "false" (exactly) on a stack wired to real printers that should not +# start prints - scripts/e2e-camera.sh does this for you. Unset = on. +# DISPATCH_ENABLED=false diff --git a/scripts/e2e-camera.sh b/scripts/e2e-camera.sh index 19433e9..f286ce8 100755 --- a/scripts/e2e-camera.sh +++ b/scripts/e2e-camera.sh @@ -245,7 +245,11 @@ if [ "$AUTH" = "dev" ]; then else echo "==> starting the backend on :$PORT, verifying tokens against the real Supabase JWKS" fi -bun run src/index.ts > "$WORK/backend.log" 2>&1 & +# The scheduler would send any queued job to any idle printer in the fleet - and +# this stack is wired to the real ones. Off, always: a job queued while working +# on the dashboard must never become a real print. (Dispatch is exercised +# against fakes by the other e2e scripts.) +DISPATCH_ENABLED=false bun run src/index.ts > "$WORK/backend.log" 2>&1 & BACKEND_PID=$! for _ in $(seq 1 60); do curl -sf "$BASE/health" >/dev/null 2>&1 && break; sleep 0.5; done curl -sf "$BASE/health" >/dev/null || fail "backend did not become healthy" diff --git a/src/dispatch/enabled.ts b/src/dispatch/enabled.ts new file mode 100644 index 0000000..eeb7aca --- /dev/null +++ b/src/dispatch/enabled.ts @@ -0,0 +1,25 @@ +/** + * Whether the scheduler may put jobs on printers. On unless + * `DISPATCH_ENABLED=false`. + * + * Exists because dispatch is global: it sends a queued job to *any* idle, + * claimed printer in *any* farm, so a development stack wired to real + * printers would start a real print the moment anyone queued a job. The dev + * stack (`SERVE=1 scripts/e2e-camera.sh`) turns it off; production leaves it + * unset. + * + * Only the exact string `false` disables it. A typo ("0", "off", "False") + * leaving the scheduler running is the safe-for-the-product failure, but a + * loud one would be better, so anything else that looks like an attempt to + * disable it is reported by `dispatchSwitchWarning`. + */ +export function dispatchEnabled(env: Record = process.env): boolean { + return env.DISPATCH_ENABLED !== "false"; +} + +/** A message when `DISPATCH_ENABLED` is set to something that does not disable it. */ +export function dispatchSwitchWarning(env: Record = process.env): string | undefined { + const value = env.DISPATCH_ENABLED; + if (value === undefined || value === "" || value === "true" || value === "false") return undefined; + return `DISPATCH_ENABLED=${JSON.stringify(value)} does not disable the scheduler (only "false" does); jobs WILL be dispatched`; +} diff --git a/src/dispatch/index.ts b/src/dispatch/index.ts index f105a58..858ceed 100644 --- a/src/dispatch/index.ts +++ b/src/dispatch/index.ts @@ -4,6 +4,7 @@ import { printers, printJobs } from "../db/schema"; import { getDownloadUrl, headObject } from "../s3"; import { isGatewayOnline, sendPrinterCommand } from "../routes/ws/edge"; import { nextJobStatus } from "./job_status"; +import { dispatchEnabled, dispatchSwitchWarning } from "./enabled"; /** * The job scheduler: picks up `queued` jobs and puts them on idle printers. @@ -152,6 +153,8 @@ async function releaseJob(jobId: string, outcome: "requeue" | "fail", reason: st export { nextJobStatus } from "./job_status"; export async function dispatchTick(): Promise { + // Also covers the nudge from `POST /jobs`, which calls this directly. + if (!dispatchEnabled()) return 0; await reapStalledUploads(); let dispatched = 0; @@ -286,6 +289,12 @@ let timer: ReturnType | undefined; /** Starts the scheduler. Idempotent, so a second call does not double-tick. */ export function startDispatcher() { if (timer) return; + const warning = dispatchSwitchWarning(); + if (warning) console.warn(`[dispatch] ${warning}`); + if (!dispatchEnabled()) { + console.log("[dispatch] DISABLED (DISPATCH_ENABLED=false): queued jobs will not be sent to printers"); + return; + } timer = setInterval(() => { dispatchTick().catch((err) => console.error("[dispatch] tick failed", err)); }, TICK_MS); diff --git a/tests/dispatch.test.ts b/tests/dispatch.test.ts index 15041cb..d741570 100644 --- a/tests/dispatch.test.ts +++ b/tests/dispatch.test.ts @@ -70,3 +70,22 @@ describe("nextJobStatus", () => { expect(completable).toEqual(["printing", "paused"]); }); }); + +describe("the dispatch switch", () => { + test("is on by default and off only for the exact string false", async () => { + const { dispatchEnabled } = await import("../src/dispatch/enabled"); + expect(dispatchEnabled({})).toBe(true); + expect(dispatchEnabled({ DISPATCH_ENABLED: "true" })).toBe(true); + expect(dispatchEnabled({ DISPATCH_ENABLED: "false" })).toBe(false); + }); + + test("a value that looks like an attempt to disable it but does not is reported", async () => { + const { dispatchEnabled, dispatchSwitchWarning } = await import("../src/dispatch/enabled"); + for (const typo of ["0", "off", "False", "no"]) { + expect(dispatchEnabled({ DISPATCH_ENABLED: typo })).toBe(true); + expect(dispatchSwitchWarning({ DISPATCH_ENABLED: typo })).toContain("WILL be dispatched"); + } + expect(dispatchSwitchWarning({})).toBeUndefined(); + expect(dispatchSwitchWarning({ DISPATCH_ENABLED: "false" })).toBeUndefined(); + }); +});