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.
This commit is contained in:
2026-10-29 14:35:40 +00:00
parent 705336b422
commit 6a2457315e
5 changed files with 63 additions and 1 deletions
+5
View File
@@ -53,3 +53,8 @@ SUPABASE_URL=
# refuses to start without it, because anything that knows this secret can # 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 # connect as any farm's gateway. Generate one with: openssl rand -hex 32
EDGE_JWT_SECRET= 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
+5 -1
View File
@@ -245,7 +245,11 @@ if [ "$AUTH" = "dev" ]; then
else else
echo "==> starting the backend on :$PORT, verifying tokens against the real Supabase JWKS" echo "==> starting the backend on :$PORT, verifying tokens against the real Supabase JWKS"
fi 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=$! BACKEND_PID=$!
for _ in $(seq 1 60); do curl -sf "$BASE/health" >/dev/null 2>&1 && break; sleep 0.5; done 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" curl -sf "$BASE/health" >/dev/null || fail "backend did not become healthy"
+25
View File
@@ -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<string, string | undefined> = 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<string, string | undefined> = 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`;
}
+9
View File
@@ -4,6 +4,7 @@ import { printers, printJobs } from "../db/schema";
import { getDownloadUrl, headObject } from "../s3"; import { getDownloadUrl, headObject } from "../s3";
import { isGatewayOnline, sendPrinterCommand } from "../routes/ws/edge"; import { isGatewayOnline, sendPrinterCommand } from "../routes/ws/edge";
import { nextJobStatus } from "./job_status"; import { nextJobStatus } from "./job_status";
import { dispatchEnabled, dispatchSwitchWarning } from "./enabled";
/** /**
* The job scheduler: picks up `queued` jobs and puts them on idle printers. * 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 { nextJobStatus } from "./job_status";
export async function dispatchTick(): Promise<number> { export async function dispatchTick(): Promise<number> {
// Also covers the nudge from `POST /jobs`, which calls this directly.
if (!dispatchEnabled()) return 0;
await reapStalledUploads(); await reapStalledUploads();
let dispatched = 0; let dispatched = 0;
@@ -286,6 +289,12 @@ let timer: ReturnType<typeof setInterval> | undefined;
/** Starts the scheduler. Idempotent, so a second call does not double-tick. */ /** Starts the scheduler. Idempotent, so a second call does not double-tick. */
export function startDispatcher() { export function startDispatcher() {
if (timer) return; 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(() => { timer = setInterval(() => {
dispatchTick().catch((err) => console.error("[dispatch] tick failed", err)); dispatchTick().catch((err) => console.error("[dispatch] tick failed", err));
}, TICK_MS); }, TICK_MS);
+19
View File
@@ -70,3 +70,22 @@ describe("nextJobStatus", () => {
expect(completable).toEqual(["printing", "paused"]); 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();
});
});