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:
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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`;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user