Files
OmniRoute/open-sse/services/compression/compressionWorkerPool.ts
NightStalker-87 a784b42060 fix: resolve compression worker file using runtime anchors instead of… (#12183)
* fix: resolve compression worker file using runtime anchors instead of import.meta.url

Replace workerUrl() function that used import.meta.url with resolveWorkerFile()
function that uses runtime anchors (process.cwd() and process.argv[1]) to locate
the worker file. This fixes webpack module resolution in Next.js standalone
bundles where import.meta.url is replaced with a stub pointing to build machine
path.

Also update Dockerfile to copy required worker-related scripts and adjust
npm install flags for better compatibility.

* fix(docker): restore base Dockerfile — keep npm ci --ignore-scripts supply-chain guard

Revert every Dockerfile change from this branch back to release/v3.8.51:
the branch dropped --ignore-scripts (reopening install-time script
execution for all transitive deps), swapped the reproducible npm ci for
npm install, invoked the nonexistent 'npm approve-scripts' command, and
broke the better-sqlite3 smoke test with a stray space in ':memory: '.
The worker-file fix does not need any Dockerfile change.

* test(compression): export runtime-anchor helpers and cover worker-file resolution

firstAncestorWith's doc already claimed 'exported for tests' without the
export; export it together with resolveWorkerFile and add unit coverage
for the runtime-anchor resolution: cwd anchor, dirname(argv[1]) anchor,
bounded walk-up (8-level cap boundary), prod-first .js-over-.ts ordering,
dev .ts fallback and the fail-open cwd fallback when nothing exists.
Fixtures live in mkdtemp sandboxes only — the repo tree is never touched.

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-01 15:37:48 -03:00

221 lines
7.5 KiB
TypeScript

import { existsSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { Worker } from "node:worker_threads";
import type { CompressionResult } from "./types.ts";
import type { StackedCompressionStep } from "./strategySelector.ts";
import type {
CompressionWorkerJob,
CompressionWorkerMessage,
CompressionWorkerOptions,
} from "./compressionWorkerProtocol.ts";
function positiveInteger(value: string | undefined, fallback: number): number {
const parsed = Number(value);
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
}
/** Relative path (from an install root) to the compression worker. */
const WORKER_JS_REL = join("open-sse", "services", "compression", "compressionWorker.js");
const WORKER_TS_REL = join("open-sse", "services", "compression", "compressionWorker.ts");
const MAX_WALK_UP = 8;
/**
* Walk up from each anchor directory (≤ MAX_WALK_UP levels) and return the first
* ancestor that actually contains `relPath`, or null. Pure + exported for tests.
*
* This deliberately avoids `import.meta.url`/`__dirname` (both dead in the standalone
* bundle) — see the LLMLingua worker comments in llmlingua/worker.ts.
*/
export function firstAncestorWith(anchors: string[], relPath: string): string | null {
for (const anchor of anchors) {
if (!anchor) continue;
let dir = resolve(anchor);
for (let i = 0; i <= MAX_WALK_UP; i++) {
if (existsSync(join(dir, relPath))) return dir;
const parent = dirname(dir);
if (parent === dir) break;
dir = parent;
}
}
return null;
}
/**
* Runtime install-root anchors that SURVIVE the standalone bundle:
* - `process.cwd()` — `dist/server.js` runs `process.chdir(__dirname)` → the dist root.
* - `dirname(process.argv[1])` — the entry script (server.js / bin), walked up.
*/
function runtimeAnchors(): string[] {
const anchors = [process.cwd()];
const argv1 = process.argv[1];
if (typeof argv1 === "string" && argv1) anchors.push(dirname(argv1));
return anchors;
}
/**
* Resolve the worker entry file across dev and prod WITHOUT `import.meta.url`.
*
* Prod: the worker is likely a .js file under the install root
* Dev: the same relative path resolves to the `.ts` source under the project
* root (cwd) and runs via the default Node.js loader.
*
* First existing candidate wins. Exported for tests.
*/
export function resolveWorkerFile(): string {
const anchors = runtimeAnchors();
// Prod first: the .js under the install root.
const jsRoot = firstAncestorWith(anchors, WORKER_JS_REL);
if (jsRoot) return join(jsRoot, WORKER_JS_REL);
// Dev: the .ts source.
const tsRoot = firstAncestorWith(anchors, WORKER_TS_REL);
if (tsRoot) return join(tsRoot, WORKER_TS_REL);
// Nothing found — return a cwd-relative .js path; the spawn will fail-open.
return join(process.cwd(), WORKER_JS_REL);
}
function unchanged(body: Record<string, unknown>): CompressionResult {
return { body, compressed: false, stats: null };
}
interface PendingJob extends CompressionWorkerJob {
originalBody: Record<string, unknown>;
resolve: (result: CompressionResult) => void;
onEngineStep?: (step: StackedCompressionStep) => void;
}
interface PoolWorker {
worker: Worker;
job: PendingJob | null;
timeout: NodeJS.Timeout | null;
idle: NodeJS.Timeout | null;
}
export class CompressionWorkerPool {
private readonly queue: PendingJob[] = [];
private readonly workers = new Set<PoolWorker>();
private nextId = 1;
private readonly size: number;
private readonly timeoutMs: number;
private readonly idleMs: number;
constructor({
size = positiveInteger(process.env.OMNI_COMPRESSION_WORKERS, 2),
timeoutMs = positiveInteger(process.env.OMNI_COMPRESSION_WORKER_TIMEOUT_MS, 120_000),
idleMs = positiveInteger(process.env.OMNI_COMPRESSION_WORKER_IDLE_MS, 60_000),
}: { size?: number; timeoutMs?: number; idleMs?: number } = {}) {
this.size = Math.max(1, Math.floor(size));
this.timeoutMs = Math.max(1, Math.floor(timeoutMs));
this.idleMs = Math.max(1, Math.floor(idleMs));
}
run(
body: Record<string, unknown>,
mode: CompressionWorkerJob["mode"],
options?: CompressionWorkerOptions,
onEngineStep?: (step: StackedCompressionStep) => void
): Promise<CompressionResult> {
return new Promise((resolve) => {
this.queue.push({
id: this.nextId++,
body,
mode,
options,
originalBody: body,
resolve,
onEngineStep,
});
this.dispatch();
});
}
async close(): Promise<void> {
for (const job of this.queue.splice(0)) job.resolve(unchanged(job.originalBody));
await Promise.all([...this.workers].map((slot) => this.remove(slot, true)));
}
private spawn(): PoolWorker {
const slot: PoolWorker = {
worker: new Worker(resolveWorkerFile()),
job: null,
timeout: null,
idle: null,
};
this.workers.add(slot);
slot.worker.on("message", (message: CompressionWorkerMessage) =>
this.handleMessage(slot, message)
);
slot.worker.on("error", () => this.fail(slot));
slot.worker.on("exit", () => {
if (this.workers.has(slot)) this.fail(slot);
});
return slot;
}
private dispatch(): void {
while (this.queue.length) {
let slot = [...this.workers].find((candidate) => !candidate.job);
if (!slot && this.workers.size < this.size) slot = this.spawn();
if (!slot) return;
if (slot.idle) clearTimeout(slot.idle);
const job = this.queue.shift();
if (!job) return;
slot.job = job;
slot.timeout = setTimeout(() => this.fail(slot!), this.timeoutMs);
slot.timeout.unref();
const { originalBody: _body, resolve: _resolve, onEngineStep: _step, ...wireJob } = job;
slot.worker.postMessage(wireJob);
}
}
private handleMessage(slot: PoolWorker, message: CompressionWorkerMessage): void {
const job = slot.job;
if (!job || job.id !== message.id) return;
if (message.type === "step") {
try {
job.onEngineStep?.(message.step);
} catch {
// Telemetry is best-effort.
}
return;
}
this.finish(slot, message.type === "result" ? message.result : unchanged(job.originalBody));
}
private finish(slot: PoolWorker, result: CompressionResult): void {
const job = slot.job;
if (!job) return;
if (slot.timeout) clearTimeout(slot.timeout);
slot.timeout = null;
slot.job = null;
job.resolve(result);
slot.idle = setTimeout(() => void this.remove(slot, false), this.idleMs);
slot.idle.unref();
this.dispatch();
}
private fail(slot: PoolWorker): void {
const job = slot.job;
if (job) job.resolve(unchanged(job.originalBody));
slot.job = null;
void this.remove(slot, true).finally(() => this.dispatch());
}
private async remove(slot: PoolWorker, terminate: boolean): Promise<void> {
if (!this.workers.delete(slot)) return;
if (slot.timeout) clearTimeout(slot.timeout);
if (slot.idle) clearTimeout(slot.idle);
if (terminate) await slot.worker.terminate().catch(() => undefined);
}
}
let pool: CompressionWorkerPool | null = null;
export function runCompressionInWorker(
body: Record<string, unknown>,
mode: CompressionWorkerJob["mode"],
options?: CompressionWorkerOptions,
onEngineStep?: (step: StackedCompressionStep) => void
): Promise<CompressionResult> {
pool ??= new CompressionWorkerPool();
return pool.run(body, mode, options, onEngineStep);
}
export async function closeCompressionWorkerPoolForTests(): Promise<void> {
const active = pool;
pool = null;
await active?.close();
}