fix(stylesheet): warm and reuse the server PDF preflight worker

The server PDF preflight spawned a fresh worker per semantic-CSS edit, each
racing a 15s startup deadline to cold-load the ~721kB+5MB PDF runtime. That
load is super-linear in CPU (~3s at 1 vCPU, >15s on a throttled/shared vCPU),
so on a constrained box every edit hit the startup-timeout path and returned
STYLESHEET_PREFLIGHT_WORKER_FAILED. Since the service only advances the applied
stylesheet when preflight passes, custom styles never applied and the editor
stuck on Checking.

Warm one worker at boot and reuse it (message-based input, respawn on
crash/timeout), so the cold load is paid once instead of per edit. Raise the
render deadline 5s->30s (a rich resume renders ~5-18s on a slow box) and the
readiness ceiling to 120s so the one-time warm completes even when throttled.
Surface worker load failures instead of an unhandled-rejection crash, and log
runner-side failure paths so the previously opaque failure is diagnosable.
Verified in node:24-slim under --cpus=0.25/0.35/0.5: all reused requests pass.

Claude-Session: https://claude.ai/code/session_01ULhhLQ24DvnYwzP4afDuye
This commit is contained in:
Amruth Pillai
2026-08-09 14:30:55 +02:00
parent 88a19619da
commit e6a31aab97
5 changed files with 379 additions and 159 deletions
+5
View File
@@ -2,6 +2,7 @@ import { pathToFileURL } from "node:url";
import { serve } from "@hono/node-server"; import { serve } from "@hono/node-server";
import { env } from "@reactive-resume/env/server"; import { env } from "@reactive-resume/env/server";
import { createApp } from "./http/app"; import { createApp } from "./http/app";
import { stylesheetPreflightRunner } from "./services/stylesheet-preflight";
import { runStartupChecks } from "./startup/checks"; import { runStartupChecks } from "./startup/checks";
export { createApp } from "./http/app"; export { createApp } from "./http/app";
@@ -23,6 +24,10 @@ async function main() {
console.info(`🚀 Up and running on http://localhost:${info.port}`); console.info(`🚀 Up and running on http://localhost:${info.port}`);
}, },
); );
// Load the heavy PDF preflight runtime once, now, so the first semantic-CSS edit
// does not pay (and time out on) the cold worker start.
stylesheetPreflightRunner.warmup();
} }
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
@@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest"; import { afterEach, describe, expect, it } from "vitest";
import { defaultResumeData } from "@reactive-resume/schema/resume/default"; import { defaultResumeData } from "@reactive-resume/schema/resume/default";
import { createStylesheetPreflightRunner, STYLESHEET_PREFLIGHT_LIMITS } from "./stylesheet-preflight"; import { createStylesheetPreflightRunner, STYLESHEET_PREFLIGHT_LIMITS } from "./stylesheet-preflight";
@@ -13,6 +13,17 @@ const input = {
stylesheet: validStylesheet, stylesheet: validStylesheet,
} as const; } as const;
// Runners now own a long-lived, reused worker; destroy them so no worker thread
// outlives its test.
const runners: Array<{ destroy(): Promise<void> }> = [];
const track = <T extends { destroy(): Promise<void> }>(runner: T): T => {
runners.push(runner);
return runner;
};
afterEach(async () => {
await Promise.all(runners.splice(0).map((runner) => runner.destroy()));
});
const memoryExhaustionWorker = new URL( const memoryExhaustionWorker = new URL(
`data:text/javascript,${encodeURIComponent(` `data:text/javascript,${encodeURIComponent(`
const retained = []; const retained = [];
@@ -27,35 +38,61 @@ const failedWorker = new URL(
`data:text/javascript,${encodeURIComponent('throw new Error("sensitive worker details");')}`, `data:text/javascript,${encodeURIComponent('throw new Error("sensitive worker details");')}`,
); );
// Counts how many preflights this single worker instance served so a reuse test
// can prove the worker is not respawned per request.
const reuseCountingWorker = new URL(
`data:text/javascript,${encodeURIComponent(`
import { parentPort } from "node:worker_threads";
let served = 0;
parentPort.postMessage({ type: "ready" });
parentPort.on("message", (message) => {
if (message?.type !== "preflight") return;
served += 1;
parentPort.postMessage({
type: "result",
requestId: message.requestId,
result: { ok: true, pageCount: 1, byteCount: served, diagnostics: [] },
});
});
`)}`,
);
const delayedSuccessfulWorker = new URL( const delayedSuccessfulWorker = new URL(
`data:text/javascript,${encodeURIComponent(` `data:text/javascript,${encodeURIComponent(`
import { parentPort, workerData } from "node:worker_threads"; import { parentPort } from "node:worker_threads";
parentPort.postMessage({ type: "ready" }); parentPort.postMessage({ type: "ready" });
setTimeout(() => { parentPort.on("message", (message) => {
parentPort.postMessage({ if (message?.type !== "preflight") return;
ok: true, setTimeout(() => {
pageCount: 1, parentPort.postMessage({
byteCount: Number(workerData.input.data.basics.name), type: "result",
diagnostics: [], requestId: message.requestId,
}); result: {
}, 300); ok: true,
pageCount: 1,
byteCount: Number(message.input.data.basics.name),
diagnostics: [],
},
});
}, 300);
});
`)}`, `)}`,
); );
const delayedReadyWorker = new URL( const delayedReadyWorker = new URL(
`data:text/javascript,${encodeURIComponent(` `data:text/javascript,${encodeURIComponent(`
import { parentPort } from "node:worker_threads"; import { parentPort } from "node:worker_threads";
setTimeout(() => { parentPort.on("message", (message) => {
parentPort.postMessage({ type: "ready" }); if (message?.type !== "preflight") return;
setTimeout(() => { setTimeout(() => {
parentPort.postMessage({ parentPort.postMessage({
ok: true, type: "result",
pageCount: 1, requestId: message.requestId,
byteCount: 1, result: { ok: true, pageCount: 1, byteCount: 1, diagnostics: [] },
diagnostics: [],
}); });
}, 10); }, 10);
}, 50); });
setTimeout(() => parentPort.postMessage({ type: "ready" }), 50);
`)}`, `)}`,
); );
@@ -101,7 +138,7 @@ const invalidInput = () => {
describe("stylesheet PDF preflight worker", () => { describe("stylesheet PDF preflight worker", () => {
it("keeps the production resource policy fixed and immutable", () => { it("keeps the production resource policy fixed and immutable", () => {
expect(STYLESHEET_PREFLIGHT_LIMITS).toEqual({ expect(STYLESHEET_PREFLIGHT_LIMITS).toEqual({
timeoutMs: 5_000, timeoutMs: 30_000,
maxPages: 20, maxPages: 20,
maxBytes: 10_000_000, maxBytes: 10_000_000,
maxPageWidthPt: 2_000, maxPageWidthPt: 2_000,
@@ -115,7 +152,7 @@ describe("stylesheet PDF preflight worker", () => {
}); });
it("accepts a bounded candidate render in an isolated worker", async () => { it("accepts a bounded candidate render in an isolated worker", async () => {
const runner = createStylesheetPreflightRunner({ timeoutMs: 15_000 }); const runner = track(createStylesheetPreflightRunner({ timeoutMs: 15_000 }));
const result = await runner.run(input); const result = await runner.run(input);
@@ -131,8 +168,22 @@ describe("stylesheet PDF preflight worker", () => {
expect(runner.activeWorkerCount).toBe(0); expect(runner.activeWorkerCount).toBe(0);
}, 45_000); }, 45_000);
it("reuses one warm worker across sequential requests instead of cold-starting each one", async () => {
const runner = track(createStylesheetPreflightRunner({}, reuseCountingWorker));
const first = await runner.run(input);
const second = await runner.run(input);
const third = await runner.run(input);
// A single reused worker increments its per-instance counter; a per-request
// worker would report byteCount 1 every time.
expect([first, second, third].map((result) => (result.ok ? result.byteCount : -1))).toEqual([1, 2, 3]);
expect(runner.activeWorkerCount).toBe(0);
expect(runner.queuedPreflightCount).toBe(0);
});
it("preserves structured resume-data failures across the worker boundary", async () => { it("preserves structured resume-data failures across the worker boundary", async () => {
const runner = createStylesheetPreflightRunner({ timeoutMs: 15_000 }); const runner = track(createStylesheetPreflightRunner({ timeoutMs: 15_000 }));
const result = runner.run(invalidInput()); const result = runner.run(invalidInput());
@@ -145,7 +196,7 @@ describe("stylesheet PDF preflight worker", () => {
}, 20_000); }, 20_000);
it("terminates a worker when the render exceeds its deadline", async () => { it("terminates a worker when the render exceeds its deadline", async () => {
const runner = createStylesheetPreflightRunner({ timeoutMs: 1 }, neverCompletesWorker); const runner = track(createStylesheetPreflightRunner({ timeoutMs: 1 }, neverCompletesWorker));
const result = await runner.run(input); const result = await runner.run(input);
@@ -155,15 +206,15 @@ describe("stylesheet PDF preflight worker", () => {
}); });
it("starts the authored render deadline after the worker runtime is ready", async () => { it("starts the authored render deadline after the worker runtime is ready", async () => {
const runner = createStylesheetPreflightRunner({ timeoutMs: 20 }, delayedReadyWorker); const runner = track(createStylesheetPreflightRunner({ timeoutMs: 20 }, delayedReadyWorker));
await expect(runner.run(input)).resolves.toEqual(expect.objectContaining({ ok: true, pageCount: 1 })); await expect(runner.run(input)).resolves.toEqual(expect.objectContaining({ ok: true, pageCount: 1 }));
expect(runner.activeWorkerCount).toBe(0); expect(runner.activeWorkerCount).toBe(0);
}); });
it("returns deterministic output byte and page limit codes", async () => { it("returns deterministic output byte and page limit codes", async () => {
const byteRunner = createStylesheetPreflightRunner({ maxBytes: 16 }); const byteRunner = track(createStylesheetPreflightRunner({ maxBytes: 16 }));
const pageRunner = createStylesheetPreflightRunner({ maxPages: 0 }); const pageRunner = track(createStylesheetPreflightRunner({ maxPages: 0 }));
await expect(byteRunner.run(input)).resolves.toEqual( await expect(byteRunner.run(input)).resolves.toEqual(
expect.objectContaining({ ok: false, code: "STYLESHEET_PREFLIGHT_BYTE_LIMIT" }), expect.objectContaining({ ok: false, code: "STYLESHEET_PREFLIGHT_BYTE_LIMIT" }),
@@ -176,9 +227,8 @@ describe("stylesheet PDF preflight worker", () => {
}, 30_000); }, 30_000);
it("maps worker heap exhaustion to a controlled memory-limit result", async () => { it("maps worker heap exhaustion to a controlled memory-limit result", async () => {
const runner = createStylesheetPreflightRunner( const runner = track(
{ maxOldGenerationMb: 8, timeoutMs: 10_000 }, createStylesheetPreflightRunner({ maxOldGenerationMb: 8, timeoutMs: 10_000 }, memoryExhaustionWorker),
memoryExhaustionWorker,
); );
const result = await runner.run(input); const result = await runner.run(input);
@@ -188,7 +238,7 @@ describe("stylesheet PDF preflight worker", () => {
}, 15_000); }, 15_000);
it("does not expose internal errors from a failed worker bootstrap", async () => { it("does not expose internal errors from a failed worker bootstrap", async () => {
const runner = createStylesheetPreflightRunner({}, failedWorker); const runner = track(createStylesheetPreflightRunner({}, failedWorker));
const result = await runner.run(input); const result = await runner.run(input);
@@ -207,15 +257,22 @@ describe("stylesheet PDF preflight worker", () => {
`data:text/javascript,${encodeURIComponent(` `data:text/javascript,${encodeURIComponent(`
import { parentPort } from "node:worker_threads"; import { parentPort } from "node:worker_threads";
parentPort.postMessage({ type: "ready" }); parentPort.postMessage({ type: "ready" });
parentPort.postMessage({ parentPort.on("message", (message) => {
ok: false, if (message?.type !== "preflight") return;
code: "STYLESHEET_PREFLIGHT_WORKER_FAILED", parentPort.postMessage({
message: "The PDF preflight worker failed. (Error: Canvas is already closed)", type: "result",
diagnostics: [], requestId: message.requestId,
result: {
ok: false,
code: "STYLESHEET_PREFLIGHT_WORKER_FAILED",
message: "The PDF preflight worker failed. (Error: Canvas is already closed)",
diagnostics: [],
},
});
}); });
`)}`, `)}`,
); );
const runner = createStylesheetPreflightRunner({ timeoutMs: 5_000 }, throwingWorker); const runner = track(createStylesheetPreflightRunner({ timeoutMs: 5_000 }, throwingWorker));
const result = await runner.run(input); const result = await runner.run(input);
@@ -228,13 +285,15 @@ describe("stylesheet PDF preflight worker", () => {
}); });
it("bounds concurrent workers and queued requests without charging queue time to the worker deadline", async () => { it("bounds concurrent workers and queued requests without charging queue time to the worker deadline", async () => {
const runner = createStylesheetPreflightRunner( const runner = track(
{ createStylesheetPreflightRunner(
timeoutMs: 500, {
maxConcurrentWorkers: 1, timeoutMs: 500,
maxQueuedRequests: 2, maxConcurrentWorkers: 1,
}, maxQueuedRequests: 2,
delayedSuccessfulWorker, },
delayedSuccessfulWorker,
),
); );
const completionOrder: number[] = []; const completionOrder: number[] = [];
const accepted = [1, 2, 3].map((number) => const accepted = [1, 2, 3].map((number) =>
@@ -262,7 +321,7 @@ describe("stylesheet PDF preflight worker", () => {
}, 5_000); }, 5_000);
it("does not leak a slot when the worker constructor throws synchronously", async () => { it("does not leak a slot when the worker constructor throws synchronously", async () => {
const runner = createStylesheetPreflightRunner({}, synchronousFailureWorker); const runner = track(createStylesheetPreflightRunner({}, synchronousFailureWorker));
await expect(runner.run(input)).resolves.toEqual( await expect(runner.run(input)).resolves.toEqual(
expect.objectContaining({ ok: false, code: "STYLESHEET_PREFLIGHT_WORKER_FAILED" }), expect.objectContaining({ ok: false, code: "STYLESHEET_PREFLIGHT_WORKER_FAILED" }),
+226 -90
View File
@@ -23,8 +23,12 @@ type StylesheetPreflightLimits = {
}; };
const SOURCE_WORKER_LOADER_HEAP_MB = 256; const SOURCE_WORKER_LOADER_HEAP_MB = 256;
const SOURCE_WORKER_STARTUP_TIMEOUT_MS = 30_000; // The worker is warmed once and reused, so the one-time cold load (which is
const WORKER_STARTUP_TIMEOUT_MS = 15_000; // super-linear in available CPU — measured ~9s at 0.5 vCPU, ~53s at 0.35, ~93s at
// 0.25) is paid at startup, not per request. This ceiling must exceed that cold
// load or warmup is killed mid-bootstrap and never completes on a throttled box;
// it only bounds a genuinely stuck bootstrap and is off the per-edit path.
const WORKER_READINESS_TIMEOUT_MS = 120_000;
type SerializedPreflightCause = { type SerializedPreflightCause = {
name: string; name: string;
@@ -33,13 +37,16 @@ type SerializedPreflightCause = {
}; };
type StylesheetPreflightWorkerMessage = type StylesheetPreflightWorkerMessage =
| PdfPreflightResult
| { type: "ready" } | { type: "ready" }
| { type: "preflight_error"; cause: SerializedPreflightCause }; | { type: "load_error"; message: string }
| { type: "result"; requestId: number; result: PdfPreflightResult }
| { type: "preflight_error"; requestId: number; cause: SerializedPreflightCause };
export type NodeStylesheetPreflightRunner = StylesheetPreflightRunner & { export type NodeStylesheetPreflightRunner = StylesheetPreflightRunner & {
readonly activeWorkerCount: number; readonly activeWorkerCount: number;
readonly queuedPreflightCount: number; readonly queuedPreflightCount: number;
warmup(): void;
destroy(): Promise<void>;
}; };
const failure = (code: PdfPreflightFailure["code"], message: string): PdfPreflightFailure => ({ const failure = (code: PdfPreflightFailure["code"], message: string): PdfPreflightFailure => ({
@@ -88,31 +95,140 @@ const workerLocation = () => {
}; };
}; };
type PendingRequest = {
resolve: (result: PdfPreflightResult) => void;
reject: (cause: unknown) => void;
renderTimer?: ReturnType<typeof setTimeout>;
};
type Readiness = {
promise: Promise<Worker>;
resolve: (worker: Worker) => void;
reject: (error: Error) => void;
timer: ReturnType<typeof setTimeout>;
};
export function createStylesheetPreflightRunner( export function createStylesheetPreflightRunner(
overrides: Partial<StylesheetPreflightLimits> = {}, overrides: Partial<StylesheetPreflightLimits> = {},
testWorkerUrl?: URL, testWorkerUrl?: URL,
): NodeStylesheetPreflightRunner { ): NodeStylesheetPreflightRunner {
const limits = Object.freeze({ ...STYLESHEET_PREFLIGHT_LIMITS, ...overrides }); const limits = Object.freeze({ ...STYLESHEET_PREFLIGHT_LIMITS, ...overrides });
let activeWorkerCount = 0;
// ponytail: Keep admission process-local and bounded; upgrade to a distributed/pooled queue only for multi-process coordination. let worker: Worker | undefined;
let ready = false;
let readiness: Readiness | undefined;
let destroyed = false;
let requestSeq = 0;
const pending = new Map<number, PendingRequest>();
// ponytail: process-local, bounded admission; upgrade to a pooled/distributed queue only for multi-process coordination.
const queue: Array<{ const queue: Array<{
input: StylesheetPreflightInput; input: StylesheetPreflightInput;
resolve: (result: PdfPreflightResult) => void; resolve: PendingRequest["resolve"];
reject: (cause: unknown) => void; reject: PendingRequest["reject"];
}> = []; }> = [];
const runWorker = ( const teardownWorker = () => {
input: StylesheetPreflightInput, const dead = worker;
resolve: (result: PdfPreflightResult) => void, worker = undefined;
reject: (cause: unknown) => void, ready = false;
): boolean => { if (readiness) {
// The URL seam is internal to the server package and keeps worker failure tests independent from the PDF renderer. clearTimeout(readiness.timer);
readiness.reject(new Error("Stylesheet preflight worker did not become ready."));
readiness = undefined;
}
if (!dead) return;
dead.off("message", onMessage);
dead.off("error", onError);
dead.off("exit", onExit);
void dead.terminate().catch(() => undefined);
};
const clearPending = (requestId: number): PendingRequest | undefined => {
const entry = pending.get(requestId);
if (!entry) return undefined;
if (entry.renderTimer) clearTimeout(entry.renderTimer);
pending.delete(requestId);
return entry;
};
const drainQueue = () => {
while (!destroyed && pending.size < limits.maxConcurrentWorkers && queue.length > 0) {
const next = queue.shift();
if (!next) return;
dispatch(next.input, next.resolve, next.reject);
}
};
const resolveRequest = (requestId: number, result: PdfPreflightResult) => {
const entry = clearPending(requestId);
if (!entry) return;
entry.resolve(result);
drainQueue();
};
const rejectRequest = (requestId: number, cause: unknown) => {
const entry = clearPending(requestId);
if (!entry) return;
entry.reject(cause);
drainQueue();
};
// A crashed/exited/timed-out worker is torn down and its in-flight requests are
// failed; the next dispatch (including any queued requests) spawns a fresh one,
// so a poisoned render never lingers across requests.
const failWorker = (result: PdfPreflightFailure) => {
teardownWorker();
const stale = [...pending.keys()];
for (const requestId of stale) resolveRequest(requestId, result);
drainQueue();
};
function onMessage(message: StylesheetPreflightWorkerMessage) {
if (message.type === "ready") {
if (readiness) {
clearTimeout(readiness.timer);
ready = true;
const resolveReady = readiness.resolve;
const readyWorker = worker;
readiness = undefined;
if (readyWorker) resolveReady(readyWorker);
}
return;
}
if (message.type === "load_error") {
failWorker(failure("STYLESHEET_PREFLIGHT_WORKER_FAILED", message.message));
return;
}
if (message.type === "result") {
resolveRequest(message.requestId, message.result);
return;
}
if (message.type === "preflight_error") {
rejectRequest(
message.requestId,
Object.assign(new Error(message.cause.message), { name: message.cause.name, issues: message.cause.issues }),
);
}
}
function onError(error: Error) {
console.warn("[stylesheet-preflight] worker error:", error.message);
failWorker(workerFailure(error));
}
function onExit(code: number) {
if (destroyed || (!worker && pending.size === 0)) return;
console.warn(`[stylesheet-preflight] worker exited unexpectedly (code ${code})`);
failWorker(failure("STYLESHEET_PREFLIGHT_WORKER_FAILED", "The PDF preflight worker failed."));
}
const startWorker = (): Promise<Worker> => {
const location = testWorkerUrl ? { source: false, url: testWorkerUrl, execArgv: [] as string[] } : workerLocation(); const location = testWorkerUrl ? { source: false, url: testWorkerUrl, execArgv: [] as string[] } : workerLocation();
let worker: Worker; let spawned: Worker;
try { try {
worker = new Worker(location.url, { spawned = new Worker(location.url, {
name: "stylesheet-preflight", name: "stylesheet-preflight",
workerData: { input, limits },
resourceLimits: { resourceLimits: {
// The source-only tsx compiler heap is outside the production render budget. // The source-only tsx compiler heap is outside the production render budget.
maxOldGenerationSizeMb: limits.maxOldGenerationMb + (location.source ? SOURCE_WORKER_LOADER_HEAP_MB : 0), maxOldGenerationSizeMb: limits.maxOldGenerationMb + (location.source ? SOURCE_WORKER_LOADER_HEAP_MB : 0),
@@ -121,96 +237,99 @@ export function createStylesheetPreflightRunner(
...("env" in location ? { env: location.env } : {}), ...("env" in location ? { env: location.env } : {}),
}); });
} catch (error) { } catch (error) {
resolve(workerFailure(error instanceof Error ? error : new Error("Failed to start PDF preflight worker."))); return Promise.reject(error instanceof Error ? error : new Error("Failed to start PDF preflight worker."));
return false;
} }
activeWorkerCount += 1; worker = spawned;
let settled = false; ready = false;
let renderTimer: ReturnType<typeof setTimeout> | undefined; // The idle reused worker must not keep the process (or a test run) alive; active
let startupTimer: ReturnType<typeof setTimeout> | undefined; // requests stay alive through their ref'd readiness/render timers.
spawned.unref();
spawned.on("message", onMessage);
spawned.once("error", onError);
spawned.once("exit", onExit);
const cleanup = () => { let resolve!: (value: Worker) => void;
if (renderTimer) clearTimeout(renderTimer); let reject!: (error: Error) => void;
if (startupTimer) clearTimeout(startupTimer); const promise = new Promise<Worker>((resolvePromise, rejectPromise) => {
worker.off("message", onMessage); resolve = resolvePromise;
worker.off("error", onError); reject = rejectPromise;
worker.off("exit", onExit); });
activeWorkerCount -= 1; const timer = setTimeout(() => {
}; console.warn(`[stylesheet-preflight] worker did not become ready within ${WORKER_READINESS_TIMEOUT_MS}ms`);
teardownWorker();
const finish = async (result: PdfPreflightResult) => { }, WORKER_READINESS_TIMEOUT_MS);
if (settled) return; readiness = { promise, resolve, reject, timer };
settled = true; return promise;
await worker.terminate().catch(() => undefined);
cleanup();
resolve(result);
drainQueue();
};
const fail = async (cause: SerializedPreflightCause) => {
if (settled) return;
settled = true;
await worker.terminate().catch(() => undefined);
cleanup();
reject(Object.assign(new Error(cause.message), { name: cause.name, issues: cause.issues }));
drainQueue();
};
const onMessage = (message: StylesheetPreflightWorkerMessage) => {
if ("type" in message) {
if (message.type === "preflight_error") {
void fail(message.cause);
return;
}
if (startupTimer) clearTimeout(startupTimer);
renderTimer = setTimeout(() => {
void finish(failure("STYLESHEET_PREFLIGHT_TIMEOUT", "The PDF preflight exceeded its deadline."));
}, limits.timeoutMs);
return;
}
void finish(message);
};
const onError = (error: Error) => {
void finish(workerFailure(error));
};
const onExit = () => {
if (!settled) {
void finish(failure("STYLESHEET_PREFLIGHT_WORKER_FAILED", "The PDF preflight worker failed."));
}
};
worker.on("message", onMessage);
worker.once("error", onError);
worker.once("exit", onExit);
startupTimer = setTimeout(
() => {
void finish(failure("STYLESHEET_PREFLIGHT_WORKER_FAILED", "The PDF preflight worker failed."));
},
location.source ? SOURCE_WORKER_STARTUP_TIMEOUT_MS : WORKER_STARTUP_TIMEOUT_MS,
);
return true;
}; };
function drainQueue() { const getReadyWorker = (): Promise<Worker> => {
while (activeWorkerCount < limits.maxConcurrentWorkers && queue.length > 0) { if (destroyed) return Promise.reject(new Error("Stylesheet preflight worker was terminated."));
const next = queue.shift(); if (worker && ready) return Promise.resolve(worker);
if (!next) return; if (readiness) return readiness.promise;
runWorker(next.input, next.resolve, next.reject); return startWorker();
};
// One retry: a worker that failed to become ready is torn down; a second
// attempt spawns a fresh worker before the request gives up.
const waitUntilReady = async (): Promise<Worker> => {
try {
return await getReadyWorker();
} catch {
return await getReadyWorker();
} }
};
function dispatch(
input: StylesheetPreflightInput,
resolve: PendingRequest["resolve"],
reject: PendingRequest["reject"],
) {
const requestId = ++requestSeq;
pending.set(requestId, { resolve, reject });
void waitUntilReady()
.then((readyWorker) => {
const entry = pending.get(requestId);
if (!entry) return;
entry.renderTimer = setTimeout(() => {
// A stuck render poisons the reused worker: tear it down and respawn — but
// only the worker this request actually ran on, so a stale timer can never
// kill a newer worker already serving other requests.
clearPending(requestId);
if (worker === readyWorker) teardownWorker();
resolve(failure("STYLESHEET_PREFLIGHT_TIMEOUT", "The PDF preflight exceeded its deadline."));
drainQueue();
}, limits.timeoutMs);
readyWorker.postMessage({ type: "preflight", requestId, input, limits });
})
.catch((error: unknown) => {
resolveRequest(
requestId,
workerFailure(error instanceof Error ? error : new Error("Failed to start PDF preflight worker.")),
);
});
} }
return { return {
get activeWorkerCount() { get activeWorkerCount() {
return activeWorkerCount; return pending.size;
}, },
get queuedPreflightCount() { get queuedPreflightCount() {
return queue.length; return queue.length;
}, },
warmup() {
void waitUntilReady().catch(() => undefined);
},
run(input: StylesheetPreflightInput): Promise<PdfPreflightResult> { run(input: StylesheetPreflightInput): Promise<PdfPreflightResult> {
return new Promise<PdfPreflightResult>((resolve, reject) => { return new Promise<PdfPreflightResult>((resolve, reject) => {
if (activeWorkerCount < limits.maxConcurrentWorkers) { if (destroyed) {
runWorker(input, resolve, reject); resolve(failure("STYLESHEET_PREFLIGHT_WORKER_FAILED", "The PDF preflight worker was terminated."));
return;
}
if (pending.size < limits.maxConcurrentWorkers) {
dispatch(input, resolve, reject);
return; return;
} }
if (queue.length >= limits.maxQueuedRequests) { if (queue.length >= limits.maxQueuedRequests) {
@@ -220,6 +339,23 @@ export function createStylesheetPreflightRunner(
queue.push({ input, resolve, reject }); queue.push({ input, resolve, reject });
}); });
}, },
async destroy() {
destroyed = true;
const dead = worker;
teardownWorker();
await dead?.terminate().catch(() => undefined);
for (const requestId of [...pending.keys()]) {
resolveRequest(
requestId,
failure("STYLESHEET_PREFLIGHT_WORKER_FAILED", "The PDF preflight worker was terminated."),
);
}
while (queue.length > 0) {
const next = queue.shift();
next?.resolve(failure("STYLESHEET_PREFLIGHT_WORKER_FAILED", "The PDF preflight worker was terminated."));
}
},
}; };
} }
+43 -26
View File
@@ -3,7 +3,7 @@ import type {
PdfPreflightResult, PdfPreflightResult,
StylesheetPreflightInput, StylesheetPreflightInput,
} from "@reactive-resume/pdf/preflight"; } from "@reactive-resume/pdf/preflight";
import { parentPort, workerData } from "node:worker_threads"; import { parentPort } from "node:worker_threads";
import * as React from "react"; import * as React from "react";
import { inspectPreflightPdf } from "./stylesheet-preflight-inspection"; import { inspectPreflightPdf } from "./stylesheet-preflight-inspection";
@@ -14,7 +14,12 @@ type StylesheetPreflightWorkerLimits = PdfPreflightPageLimits & {
maxBytes: number; maxBytes: number;
}; };
type StylesheetPreflightWorkerData = { // The worker is long-lived and reused across requests: the heavy PDF runtime is
// loaded once at startup and each preflight arrives as a message (input can no
// longer come from `workerData`, which is fixed at construction time).
type StylesheetPreflightWorkerRequest = {
type: "preflight";
requestId: number;
input: StylesheetPreflightInput; input: StylesheetPreflightInput;
limits: StylesheetPreflightWorkerLimits; limits: StylesheetPreflightWorkerLimits;
}; };
@@ -25,10 +30,6 @@ type SerializedPreflightCause = {
issues: readonly unknown[]; issues: readonly unknown[];
}; };
const send = (result: PdfPreflightResult) => {
parentPort?.postMessage(result);
};
const sanitizeWorkerCause = (cause: unknown): string => { const sanitizeWorkerCause = (cause: unknown): string => {
if (!(cause instanceof Error)) return "The PDF preflight worker failed."; if (!(cause instanceof Error)) return "The PDF preflight worker failed.";
const detail = cause.message.replace(/\s+/g, " ").trim().slice(0, 200); const detail = cause.message.replace(/\s+/g, " ").trim().slice(0, 200);
@@ -43,31 +44,47 @@ const serializeZodCause = (cause: unknown): SerializedPreflightCause | undefined
return { name: cause.name, message: cause.message, issues: cause.issues }; return { name: cause.name, message: cause.message, issues: cause.issues };
}; };
// Load the heavy PDF runtime once. A rejection here (missing/broken dependency in
// a pruned production install) is reported explicitly instead of becoming an
// unhandled rejection that silently kills the worker and surfaces as an opaque
// runner-side failure.
const initialization = import("@reactive-resume/pdf/preflight"); const initialization = import("@reactive-resume/pdf/preflight");
void initialization.then(() => parentPort?.postMessage({ type: "ready" }));
async function run(): Promise<PdfPreflightResult> { void initialization.then(
const { renderPreflightPdf } = await initialization; () => parentPort?.postMessage({ type: "ready" }),
const { input, limits } = workerData as StylesheetPreflightWorkerData; (cause: unknown) => {
const rendered = await renderPreflightPdf(input, limits); console.error("[stylesheet-preflight] worker runtime failed to load", cause);
return rendered.ok ? inspectPreflightPdf(rendered, limits) : rendered; parentPort?.postMessage({ type: "load_error", message: sanitizeWorkerCause(cause) });
} },
);
if (parentPort) { const handle = async (request: StylesheetPreflightWorkerRequest): Promise<void> => {
void run() try {
.then(send) const { renderPreflightPdf } = await initialization;
.catch((cause: unknown) => { const rendered = await renderPreflightPdf(request.input, request.limits);
const serializedCause = serializeZodCause(cause); const result: PdfPreflightResult = rendered.ok ? await inspectPreflightPdf(rendered, request.limits) : rendered;
if (serializedCause) { parentPort?.postMessage({ type: "result", requestId: request.requestId, result });
parentPort?.postMessage({ type: "preflight_error", cause: serializedCause }); } catch (cause) {
return; const serializedCause = serializeZodCause(cause);
} if (serializedCause) {
console.error("[stylesheet-preflight]", cause); parentPort?.postMessage({ type: "preflight_error", requestId: request.requestId, cause: serializedCause });
send({ return;
}
console.error("[stylesheet-preflight]", cause);
parentPort?.postMessage({
type: "result",
requestId: request.requestId,
result: {
ok: false, ok: false,
code: "STYLESHEET_PREFLIGHT_WORKER_FAILED", code: "STYLESHEET_PREFLIGHT_WORKER_FAILED",
message: sanitizeWorkerCause(cause), message: sanitizeWorkerCause(cause),
diagnostics: [], diagnostics: [],
}); },
}); });
} }
};
parentPort?.on("message", (message: StylesheetPreflightWorkerRequest) => {
if (message.type !== "preflight") return;
void handle(message);
});
@@ -40,7 +40,10 @@ export const PDF_PREFLIGHT_DIAGNOSTIC_CATALOG = {
export type PdfPreflightFailureCode = keyof typeof PDF_PREFLIGHT_DIAGNOSTIC_CATALOG; export type PdfPreflightFailureCode = keyof typeof PDF_PREFLIGHT_DIAGNOSTIC_CATALOG;
export const STYLESHEET_PREFLIGHT_LIMITS = Object.freeze({ export const STYLESHEET_PREFLIGHT_LIMITS = Object.freeze({
timeoutMs: 5_000, // Render deadline (after the worker is warm). A rich resume on a throttled/shared
// vCPU renders in ~5-18s, so 5s spuriously failed real resumes; the worker is now
// warmed+reused so this ceiling only bounds a genuinely stuck render.
timeoutMs: 30_000,
maxPages: 20, maxPages: 20,
maxBytes: 10_000_000, maxBytes: 10_000_000,
maxPageWidthPt: 2_000, maxPageWidthPt: 2_000,