Worker Pool Using worker_threads
Run CPU-bound jobs on a fixed pool of Node worker threads with a task queue, timeouts, crash recovery, and clean shutdown.
Code
import { Worker } from "node:worker_threads";
interface QueueItem {
payload: unknown;
resolve: (value: unknown) => void;
reject: (reason?: unknown) => void;
}
interface PendingEntry {
worker: Worker;
resolve: (value: unknown) => void;
reject: (reason?: unknown) => void;
timer: ReturnType<typeof setTimeout>;
}
export interface WorkerPoolOptions {
size?: number;
workerSource?: string;
workerUrl?: string | URL;
taskTimeoutMs?: number;
}
export class WorkerPool {
private readonly workers: Worker[] = [];
private readonly idle: Worker[] = [];
private readonly queue: QueueItem[] = [];
private readonly pending = new Map<number, PendingEntry>();
private readonly size: number;
private readonly workerSource?: string;
private readonly workerUrl?: string | URL;
private readonly taskTimeoutMs: number;
private nextId = 1;
private closed = false;
public constructor(options: WorkerPoolOptions) {
const {
size = 2,
workerSource,
workerUrl,
taskTimeoutMs = 10_000,
} = options;
if (!Number.isInteger(size) || size < 1 || size > 64) {
throw new RangeError("size must be an integer between 1 and 64");
}
if ((workerSource === undefined) === (workerUrl === undefined)) {
throw new TypeError(
"provide exactly one of workerSource or workerUrl",
);
}
if (!Number.isFinite(taskTimeoutMs) || taskTimeoutMs <= 0) {
throw new RangeError("taskTimeoutMs must be a positive finite number");
}
this.size = size;
this.workerSource = workerSource;
this.workerUrl = workerUrl;
this.taskTimeoutMs = taskTimeoutMs;
try {
for (let index = 0; index < this.size; index += 1) {
const worker = this.createWorker();
this.workers.push(worker);
this.idle.push(worker);
}
} catch (error) {
for (const worker of this.workers) {
void worker.terminate().catch(() => undefined);
}
throw error;
}
}
public run<TResult = unknown>(payload: unknown): Promise<TResult> {
if (this.closed) {
return Promise.reject(new Error("worker pool is closed"));
}
return new Promise<TResult>((resolve, reject) => {
this.queue.push({
payload,
resolve: resolve as (value: unknown) => void,
reject,
});
this.dispatch();
});
}
public async close(): Promise<void> {
if (this.closed) return;
this.closed = true;
const error = new Error("worker pool is closed");
for (const item of this.queue.splice(0)) item.reject(error);
for (const entry of this.pending.values()) {
clearTimeout(entry.timer);
entry.reject(error);
}
this.pending.clear();
const workers = this.workers.splice(0);
this.idle.length = 0;
await Promise.all(workers.map((worker) => worker.terminate()));
}
private createWorker(): Worker {
const worker =
this.workerSource !== undefined
? new Worker(this.workerSource, { eval: true })
: new Worker(this.workerUrl as string | URL);
worker.on("message", (message: unknown) => {
this.handleMessage(worker, message);
});
worker.on("error", (error: Error) => {
this.handleFailure(worker, error);
});
worker.on("exit", (code) => {
if (!this.closed && this.workers.includes(worker)) {
this.handleFailure(
worker,
new Error(`worker exited with code ${code}`),
);
}
});
return worker;
}
private dispatch(): void {
while (
!this.closed &&
this.idle.length > 0 &&
this.queue.length > 0
) {
const worker = this.idle.pop();
const task = this.queue.shift();
if (worker === undefined || task === undefined) return;
const id = this.nextId;
this.nextId += 1;
const timer = setTimeout(() => {
if (this.closed) return;
const entry = this.pending.get(id);
if (entry === undefined) return;
this.pending.delete(id);
entry.reject(
new Error(`task ${id} timed out after ${this.taskTimeoutMs}ms`),
);
this.replaceWorker(worker);
}, this.taskTimeoutMs);
this.pending.set(id, {
worker,
resolve: task.resolve,
reject: task.reject,
timer,
});
try {
worker.postMessage({ id, payload: task.payload });
} catch (error) {
clearTimeout(timer);
this.pending.delete(id);
task.reject(error);
this.idle.push(worker);
}
}
}
private handleMessage(worker: Worker, message: unknown): void {
if (typeof message !== "object" || message === null) return;
const data = message as {
id?: unknown;
ok?: unknown;
result?: unknown;
error?: unknown;
};
if (typeof data.id !== "number" || typeof data.ok !== "boolean") return;
const entry = this.pending.get(data.id);
if (entry === undefined || entry.worker !== worker) return;
this.pending.delete(data.id);
clearTimeout(entry.timer);
if (data.ok) {
entry.resolve(data.result);
} else {
entry.reject(
new Error(
typeof data.error === "string"
? data.error
: "worker task failed",
),
);
}
this.markIdle(worker);
}
private markIdle(worker: Worker): void {
if (
this.closed ||
!this.workers.includes(worker) ||
this.idle.includes(worker)
) {
return;
}
this.idle.push(worker);
this.dispatch();
}
private handleFailure(worker: Worker, error: unknown): void {
const index = this.workers.indexOf(worker);
if (index < 0) return;
const failure =
error instanceof Error ? error : new Error(String(error));
this.workers.splice(index, 1);
const idleIndex = this.idle.indexOf(worker);
if (idleIndex >= 0) this.idle.splice(idleIndex, 1);
for (const [id, entry] of this.pending) {
if (entry.worker !== worker) continue;
this.pending.delete(id);
clearTimeout(entry.timer);
entry.reject(failure);
}
void worker.terminate().catch(() => undefined);
if (!this.closed) {
try {
const replacement = this.createWorker();
this.workers.push(replacement);
this.idle.push(replacement);
this.dispatch();
} catch (replacementError) {
for (const item of this.queue.splice(0)) {
item.reject(replacementError);
}
}
}
}
private replaceWorker(worker: Worker): void {
const index = this.workers.indexOf(worker);
if (index < 0) return;
this.workers.splice(index, 1);
const idleIndex = this.idle.indexOf(worker);
if (idleIndex >= 0) this.idle.splice(idleIndex, 1);
void worker.terminate().catch(() => undefined);
if (this.closed) return;
try {
const replacement = this.createWorker();
this.workers.push(replacement);
this.idle.push(replacement);
this.dispatch();
} catch (error) {
for (const item of this.queue.splice(0)) item.reject(error);
}
}
}
const WORKER_SOURCE = `
const { parentPort } = require("node:worker_threads");
parentPort.on("message", ({ id, payload }) => {
try {
if (!payload || !Array.isArray(payload.values)) {
throw new TypeError("payload.values must be an array");
}
const total = payload.values.reduce((sum, value) => {
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new TypeError("values must be finite numbers");
}
return sum + value;
}, 0);
parentPort.postMessage({ id, ok: true, result: total });
} catch (error) {
parentPort.postMessage({
id,
ok: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
`;
async function main(): Promise<void> {
const pool = new WorkerPool({
size: 2,
workerSource: WORKER_SOURCE,
taskTimeoutMs: 2000,
});
const totals = await Promise.all([
pool.run<number>({ values: [1, 2, 3] }),
pool.run<number>({ values: [4, 5, 6] }),
pool.run<number>({ values: [7, 8, 9] }),
]);
console.log(totals);
const failure = await pool
.run({ values: [1, "x"] })
.catch((error: unknown) =>
error instanceof Error ? error.message : String(error),
);
console.log(failure);
await pool.close();
console.log("closed");
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});Report a problem with this exampleOpens a pre-filled issue on GitHub.