nestworker 2.1.1 → 2.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -0
- package/dist/core/worker.interfaces.d.ts +30 -15
- package/dist/core/worker.pool.d.ts +46 -5
- package/dist/core/worker.pool.js +410 -199
- package/dist/core/worker.pool.js.map +1 -1
- package/dist/core/worker.service.js +28 -15
- package/dist/core/worker.service.js.map +1 -1
- package/dist/di/worker-container.js +26 -1
- package/dist/di/worker-container.js.map +1 -1
- package/dist/example/bench.js +9 -8
- package/dist/example/bench.js.map +1 -1
- package/dist/example/image.service.d.ts +2 -0
- package/dist/example/image.service.js +10 -0
- package/dist/example/image.service.js.map +1 -1
- package/dist/example/main.js +2 -2
- package/dist/example/main.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/worker/worker-runtime.js +196 -84
- package/dist/worker/worker-runtime.js.map +1 -1
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -23,6 +23,8 @@ Enterprise-grade worker thread module for NestJS. Offload CPU-bound work to a ma
|
|
|
23
23
|
|
|
24
24
|
- **Worker pool** — pre-spawned threads, warmed up before the first request
|
|
25
25
|
- **Zero cold start** — pool initialises on `onModuleInit`, not on the first call
|
|
26
|
+
- **Per-worker concurrency** — opt-in pipelining (`concurrency > 1`) keeps each worker busy across awaits and short tasks
|
|
27
|
+
- **Automatic message batching** — jobs and results are coalesced into a single `postMessage` per scheduling pass, amortising `structuredClone` overhead
|
|
26
28
|
- **Priority queue** — `HIGH / NORMAL / LOW`, binary-search sorted; no jobs are ever dropped
|
|
27
29
|
- **Decorator discovery** — `@WorkerClass` + `@WorkerTask` replace all manual registration
|
|
28
30
|
- **deps** — services serialised into the worker via `vm.runInContext()` + snapshot; use for plain config/data helpers
|
|
@@ -151,6 +153,7 @@ export class ImageController {
|
|
|
151
153
|
| Option | Type | Default | Description |
|
|
152
154
|
|---|---|---|---|
|
|
153
155
|
| `poolSize` | `number` | `os.cpus().length` | Worker thread count |
|
|
156
|
+
| `concurrency` | `number` | `1` | Max in-flight jobs **per worker**. Set `> 1` to pipeline jobs so workers don't sit idle between results, or while a task is awaiting I/O (proxy IPC, `fetch`, `fs`, …). Keep at `1` for purely CPU-bound, fully blocking tasks. |
|
|
154
157
|
| `shutdownTimeout` | `number` | `30_000` | Ms to wait for in-flight jobs on shutdown |
|
|
155
158
|
| `asyncLocalStorages` | `AsyncLocalStorage[]` | `[]` | ALS instances to propagate into workers |
|
|
156
159
|
|
|
@@ -547,6 +550,36 @@ await Promise.all([
|
|
|
547
550
|
|
|
548
551
|
---
|
|
549
552
|
|
|
553
|
+
## Per-Worker Concurrency
|
|
554
|
+
|
|
555
|
+
By default each worker processes one job at a time. When tasks are short, or
|
|
556
|
+
they `await` I/O (proxy IPC round-trips, `fetch`, `fs`, queue calls), the worker
|
|
557
|
+
sits idle while the main thread processes the previous result. Set
|
|
558
|
+
`concurrency > 1` to pipeline jobs into each worker and keep them saturated:
|
|
559
|
+
|
|
560
|
+
```ts
|
|
561
|
+
WorkerModule.forRoot({
|
|
562
|
+
poolSize: 4, // 4 worker threads
|
|
563
|
+
concurrency: 8, // up to 8 in-flight jobs per worker → 32 concurrent jobs
|
|
564
|
+
})
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
Guidance:
|
|
568
|
+
|
|
569
|
+
- **CPU-bound, fully blocking tasks** → keep at `1`. Extra concurrency cannot
|
|
570
|
+
help when the JS thread never yields.
|
|
571
|
+
- **Short tasks (sub-millisecond)** → `2–4` is usually enough to hide the
|
|
572
|
+
per-job postMessage cost.
|
|
573
|
+
- **Tasks awaiting I/O or proxy calls** → match `concurrency` to your typical
|
|
574
|
+
in-flight wait depth (e.g. `8–32`).
|
|
575
|
+
|
|
576
|
+
Internally the pool also coalesces every job it dispatches in a single
|
|
577
|
+
scheduling pass into one `postMessage` envelope per worker, and the worker
|
|
578
|
+
flushes accumulated results once per microtask tick. Batching is automatic —
|
|
579
|
+
there is nothing to configure.
|
|
580
|
+
|
|
581
|
+
---
|
|
582
|
+
|
|
550
583
|
## Constraints
|
|
551
584
|
|
|
552
585
|
### Arguments and return values
|
|
@@ -1,27 +1,22 @@
|
|
|
1
1
|
import type { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
2
|
export type TaskPriority = 'HIGH' | 'NORMAL' | 'LOW';
|
|
3
3
|
export interface WorkerJob {
|
|
4
|
-
jobId:
|
|
4
|
+
jobId: number;
|
|
5
5
|
serviceName: string;
|
|
6
6
|
methodName: string;
|
|
7
7
|
args: unknown[];
|
|
8
|
-
priority: TaskPriority;
|
|
9
|
-
timeout?: number;
|
|
10
|
-
/** Retry policy — sourced from @WorkerTask or overridden per call */
|
|
11
|
-
retry?: number;
|
|
12
|
-
retryDelay?: number;
|
|
13
|
-
/** Current attempt index (0 = first attempt) */
|
|
14
|
-
attempt?: number;
|
|
15
8
|
proxyServices?: ProxyServiceDescriptor[];
|
|
16
9
|
/** ALS context snapshot — restored in worker before task runs */
|
|
17
10
|
alsContext?: Record<string, unknown>;
|
|
18
11
|
/** OTEL trace context — W3C traceparent/tracestate headers */
|
|
19
12
|
traceContext?: Record<string, string>;
|
|
20
13
|
/** AbortSignal is non-transferable; we send the signal ID instead */
|
|
21
|
-
abortSignalId?:
|
|
14
|
+
abortSignalId?: number;
|
|
22
15
|
}
|
|
23
16
|
export interface WorkerResult<T = unknown> {
|
|
24
17
|
type: 'result';
|
|
18
|
+
/** ID of the job this result is for (required when concurrency > 1) */
|
|
19
|
+
jobId?: number;
|
|
25
20
|
ok: boolean;
|
|
26
21
|
data?: T;
|
|
27
22
|
error?: SerializedError;
|
|
@@ -36,7 +31,7 @@ export interface SerializedError {
|
|
|
36
31
|
}
|
|
37
32
|
export interface WorkerAbortMessage {
|
|
38
33
|
type: 'abort';
|
|
39
|
-
abortSignalId:
|
|
34
|
+
abortSignalId: number;
|
|
40
35
|
}
|
|
41
36
|
export interface ProxyServiceDescriptor {
|
|
42
37
|
propertyKey: string;
|
|
@@ -44,14 +39,14 @@ export interface ProxyServiceDescriptor {
|
|
|
44
39
|
}
|
|
45
40
|
export interface IpcInvokeRequest {
|
|
46
41
|
type: 'ipc:invoke';
|
|
47
|
-
callId:
|
|
42
|
+
callId: number;
|
|
48
43
|
propertyKey: string;
|
|
49
44
|
methodName: string;
|
|
50
45
|
args: unknown[];
|
|
51
46
|
}
|
|
52
47
|
export interface IpcInvokeResponse {
|
|
53
48
|
type: 'ipc:result';
|
|
54
|
-
callId:
|
|
49
|
+
callId: number;
|
|
55
50
|
ok: boolean;
|
|
56
51
|
data?: unknown;
|
|
57
52
|
error?: string;
|
|
@@ -59,10 +54,18 @@ export interface IpcInvokeResponse {
|
|
|
59
54
|
export interface WorkerReadySignal {
|
|
60
55
|
type: 'worker:ready';
|
|
61
56
|
}
|
|
62
|
-
export
|
|
63
|
-
|
|
57
|
+
export interface WorkerJobBatch {
|
|
58
|
+
type: 'batch';
|
|
59
|
+
jobs: WorkerJob[];
|
|
60
|
+
}
|
|
61
|
+
export interface WorkerResultBatch {
|
|
62
|
+
type: 'results';
|
|
63
|
+
results: WorkerResult[];
|
|
64
|
+
}
|
|
65
|
+
export type WorkerInboundMessage = WorkerJob | WorkerJobBatch | IpcInvokeResponse | WorkerAbortMessage;
|
|
66
|
+
export type WorkerOutboundMessage = WorkerResult | WorkerResultBatch | IpcInvokeRequest | WorkerReadySignal;
|
|
64
67
|
export interface DeadLetterEvent {
|
|
65
|
-
jobId:
|
|
68
|
+
jobId: number;
|
|
66
69
|
serviceName: string;
|
|
67
70
|
methodName: string;
|
|
68
71
|
args: unknown[];
|
|
@@ -91,6 +94,18 @@ export interface ProxyInstance {
|
|
|
91
94
|
export interface WorkerModuleOptions {
|
|
92
95
|
/** Number of worker threads. Defaults to os.cpus().length */
|
|
93
96
|
poolSize?: number;
|
|
97
|
+
/**
|
|
98
|
+
* Maximum number of in-flight jobs per worker. Defaults to 1.
|
|
99
|
+
*
|
|
100
|
+
* Set > 1 to pipeline jobs into each worker: the main thread will keep
|
|
101
|
+
* dispatching to a worker as long as its in-flight count is below this
|
|
102
|
+
* limit, so the worker never sits idle between jobs while the main thread
|
|
103
|
+
* is processing a result. Significant throughput win for short tasks and
|
|
104
|
+
* for tasks that await I/O (proxy IPC, fetch, fs, ...).
|
|
105
|
+
*
|
|
106
|
+
* Safe to keep at 1 for purely CPU-bound, blocking tasks.
|
|
107
|
+
*/
|
|
108
|
+
concurrency?: number;
|
|
94
109
|
/**
|
|
95
110
|
* How long (ms) to wait for in-flight jobs to finish before force-killing
|
|
96
111
|
* workers on application shutdown. Defaults to 30_000.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
-
import type { WorkerJob, ProxyInstance, DeadLetterEvent, SerializedError, PoolStats } from './worker.interfaces';
|
|
2
|
+
import type { WorkerJob, TaskPriority, ProxyInstance, DeadLetterEvent, SerializedError, PoolStats } from './worker.interfaces';
|
|
3
3
|
import type { SerializedService } from '../di/worker-container';
|
|
4
4
|
export declare interface WorkerPool {
|
|
5
5
|
on(event: 'dead', listener: (event: DeadLetterEvent) => void): this;
|
|
@@ -18,21 +18,62 @@ export declare class WorkerPool extends EventEmitter {
|
|
|
18
18
|
private readonly size;
|
|
19
19
|
private readonly shutdownTimeout;
|
|
20
20
|
private readonly workers;
|
|
21
|
+
/**
|
|
22
|
+
* Available "slots" — each worker is pushed `concurrency` times when it
|
|
23
|
+
* becomes ready, then popped/pushed as jobs are dispatched/completed.
|
|
24
|
+
* This naturally supports per-worker pipelining without any per-job
|
|
25
|
+
* counter bookkeeping.
|
|
26
|
+
*/
|
|
21
27
|
private readonly idle;
|
|
22
28
|
private readonly warmingUp;
|
|
23
29
|
private readonly queue;
|
|
30
|
+
private queueHead;
|
|
24
31
|
private destroyed;
|
|
25
|
-
private
|
|
32
|
+
private activeCount;
|
|
33
|
+
/**
|
|
34
|
+
* `schedule()` is invoked many times in a single synchronous burst (e.g.
|
|
35
|
+
* `for (...) ws.run(...)` floods 20k enqueues). Running the dispatch loop
|
|
36
|
+
* after every enqueue limits us to batches of size 1 per worker — the
|
|
37
|
+
* whole point of batching is then defeated. Defer to the next microtask
|
|
38
|
+
* so all synchronously-enqueued jobs land in one schedule pass and get
|
|
39
|
+
* coalesced into a single postMessage per worker.
|
|
40
|
+
*/
|
|
41
|
+
private scheduleQueued;
|
|
26
42
|
/** Maps abortSignalId → worker currently running that job */
|
|
27
43
|
private readonly signalWorkerMap;
|
|
44
|
+
private readonly concurrency;
|
|
28
45
|
private readonly proxyMap;
|
|
29
|
-
constructor(services: SerializedService[], proxyInstances: ProxyInstance[], size?: number, shutdownTimeout?: number);
|
|
30
|
-
execute<T = unknown>(job: WorkerJob,
|
|
46
|
+
constructor(services: SerializedService[], proxyInstances: ProxyInstance[], size?: number, shutdownTimeout?: number, concurrency?: number);
|
|
47
|
+
execute<T = unknown>(job: WorkerJob, meta: {
|
|
48
|
+
priority: TaskPriority;
|
|
49
|
+
timeout?: number;
|
|
50
|
+
retry?: number;
|
|
51
|
+
retryDelay?: number;
|
|
52
|
+
}, signal?: AbortSignal): Promise<T>;
|
|
31
53
|
stats(): PoolStats;
|
|
32
54
|
private spawnWorker;
|
|
55
|
+
private handleIpcInvoke;
|
|
33
56
|
private enqueue;
|
|
34
57
|
private schedule;
|
|
35
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Synchronous drain used on the COMPLETION path — when a worker becomes
|
|
60
|
+
* idle as a result of a result message arriving, we want to hand it the
|
|
61
|
+
* next queued job in the SAME tick. The microtask-deferred `schedule()`
|
|
62
|
+
* adds a full microtask hop per round-trip, which dominates throughput
|
|
63
|
+
* for short tasks with concurrency=1.
|
|
64
|
+
*
|
|
65
|
+
* Initial-burst dispatch still goes through the deferred `schedule()` so
|
|
66
|
+
* synchronous floods of `execute()` calls get coalesced into per-worker
|
|
67
|
+
* batches.
|
|
68
|
+
*/
|
|
69
|
+
private dispatchNow;
|
|
70
|
+
/** Pre-bound for queueMicrotask — avoids closure allocation per schedule. */
|
|
71
|
+
private readonly drain;
|
|
72
|
+
private prepareDispatch;
|
|
73
|
+
/** Called from the persistent message listener when a job result arrives. */
|
|
74
|
+
private completeJob;
|
|
75
|
+
private handleFailure;
|
|
76
|
+
private handleTimeout;
|
|
36
77
|
private handleWorkerError;
|
|
37
78
|
private handleWorkerExit;
|
|
38
79
|
private replaceWorker;
|