@nadrif/thread 0.1.0
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/LICENSE +21 -0
- package/README.md +1156 -0
- package/package.json +133 -0
- package/src/adapters.js +404 -0
- package/src/config/adapters.js +188 -0
- package/src/config/define.js +171 -0
- package/src/config/env.js +218 -0
- package/src/config/frameworks.js +268 -0
- package/src/config/index.js +256 -0
- package/src/config/schema.js +375 -0
- package/src/config.js +52 -0
- package/src/deno.js +29 -0
- package/src/edge.js +38 -0
- package/src/env.js +361 -0
- package/src/error.js +340 -0
- package/src/factory.js +591 -0
- package/src/gpu/adapters.js +227 -0
- package/src/gpu/chains.js +261 -0
- package/src/gpu/env.js +372 -0
- package/src/gpu/gpu.js +1199 -0
- package/src/gpu/helpers.js +240 -0
- package/src/gpu/hooks.js +284 -0
- package/src/gpu/index.js +16 -0
- package/src/gpu/shaders.js +834 -0
- package/src/gpu/special.js +493 -0
- package/src/hooks.js +448 -0
- package/src/index.js +557 -0
- package/src/metrix.js +222 -0
- package/src/node.js +36 -0
- package/src/pool.js +740 -0
- package/src/serializer.js +139 -0
- package/src/thread.js +1246 -0
- package/src/types.js +1354 -0
- package/src/worker-factory.js +347 -0
package/src/pool.js
ADDED
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Managed pool of Web Worker threads.
|
|
3
|
+
*
|
|
4
|
+
* `ThreadPool` maintains a fixed (or dynamically resizable) set of
|
|
5
|
+
* workers and distributes incoming tasks among them. Key features:
|
|
6
|
+
*
|
|
7
|
+
* - **Priority queue** – tasks are dequeued in priority order.
|
|
8
|
+
* - **Dependency resolution** – declare that a task must wait for other
|
|
9
|
+
* tasks to finish before it starts.
|
|
10
|
+
* - **Work stealing** – idle threads can pick up work from busy threads'
|
|
11
|
+
* local queues.
|
|
12
|
+
* - **Affinity routing** – optional `keyHasher` routes tasks with the
|
|
13
|
+
* same key to the same thread (good for caches).
|
|
14
|
+
* - **Auto-restart** – crashed workers are replaced automatically and
|
|
15
|
+
* queued tasks are re-dispatched.
|
|
16
|
+
* - **Dynamic resizing** – call `scaleTo()` at runtime.
|
|
17
|
+
* - **Graceful shutdown** – `terminateGracefully()` waits for all tasks
|
|
18
|
+
* to finish before killing workers.
|
|
19
|
+
*
|
|
20
|
+
* **Quick start:**
|
|
21
|
+
*
|
|
22
|
+
* ```js
|
|
23
|
+
* import { ThreadPool } from './pool.js';
|
|
24
|
+
*
|
|
25
|
+
* const pool = new ThreadPool(4, (x) => x * 2);
|
|
26
|
+
*
|
|
27
|
+
* const { id, promise } = pool.run(21);
|
|
28
|
+
* console.log(await promise); // 42
|
|
29
|
+
*
|
|
30
|
+
* await pool.terminateGracefully();
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* **With priorities and dependencies:**
|
|
34
|
+
*
|
|
35
|
+
* ```js
|
|
36
|
+
* const pool = new ThreadPool(2, (x) => x + 1);
|
|
37
|
+
*
|
|
38
|
+
* // High-priority task runs first
|
|
39
|
+
* pool.run(10, { priority: 0 });
|
|
40
|
+
*
|
|
41
|
+
* // Low-priority task
|
|
42
|
+
* pool.run(20, { priority: 10 });
|
|
43
|
+
*
|
|
44
|
+
* // Dependent task – runs after task 0 completes
|
|
45
|
+
* const a = pool.run(1);
|
|
46
|
+
* const b = pool.run(2, { dependsOn: [a.id] });
|
|
47
|
+
* console.log(await b.promise); // 3
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* @module pool
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
import { Thread } from "./thread";
|
|
54
|
+
import {
|
|
55
|
+
ThreadAbortError,
|
|
56
|
+
ThreadDependencyError,
|
|
57
|
+
ThreadTerminatedError,
|
|
58
|
+
} from "./error";
|
|
59
|
+
import { Metrics } from "./metrix";
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// ThreadPool class
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A thread pool with work-stealing, priorities, and dependency resolution.
|
|
67
|
+
*/
|
|
68
|
+
export class ThreadPool {
|
|
69
|
+
// -----------------------------------------------------------------------
|
|
70
|
+
// Constructor
|
|
71
|
+
// -----------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Create a new thread pool.
|
|
75
|
+
*
|
|
76
|
+
* @param {number} initialSize
|
|
77
|
+
* Number of worker threads to spawn immediately. The pool can be
|
|
78
|
+
* resized later with {@link ThreadPool.scaleTo}.
|
|
79
|
+
* @param {import('./types.js').ThreadDefinition | Function} definition
|
|
80
|
+
* Worker definition forwarded to each {@link Thread} constructor.
|
|
81
|
+
* Can be a plain function or a `{ setup, exec, cleanup }` object.
|
|
82
|
+
* @param {import('./types.js').PoolOptions} [options={}]
|
|
83
|
+
* Pool and thread configuration.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```js
|
|
87
|
+
* // Simple pool with 4 workers
|
|
88
|
+
* const pool = new ThreadPool(4, (data) => processData(data));
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```js
|
|
93
|
+
* // Stateful workers with health checks
|
|
94
|
+
* const pool = new ThreadPool(2, {
|
|
95
|
+
* setup() { return { cache: new Map() }; },
|
|
96
|
+
* exec(state, key, ctx) {
|
|
97
|
+
* if (state.cache.has(key)) return state.cache.get(key);
|
|
98
|
+
* const result = expensiveLookup(key);
|
|
99
|
+
* state.cache.set(key, result);
|
|
100
|
+
* ctx.log(`Cached ${key}`);
|
|
101
|
+
* return result;
|
|
102
|
+
* },
|
|
103
|
+
* }, {
|
|
104
|
+
* timeout: 10_000,
|
|
105
|
+
* healthCheckInterval: 5_000,
|
|
106
|
+
* autoRestart: true,
|
|
107
|
+
* });
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
constructor(initialSize, definition, options = {}) {
|
|
111
|
+
this._definition = definition;
|
|
112
|
+
this._baseOptions = { ...options };
|
|
113
|
+
this._autoRestart = options.autoRestart !== false;
|
|
114
|
+
this._maxSize = options.maxSize || Infinity;
|
|
115
|
+
this._keyHasher = options.keyHasher || ((args) => JSON.stringify(args));
|
|
116
|
+
this._enableStealing = options.enableStealing !== false;
|
|
117
|
+
this._threads = [];
|
|
118
|
+
this._globalQueue = [];
|
|
119
|
+
this._blockedTasks = [];
|
|
120
|
+
this._running = 0;
|
|
121
|
+
this._terminated = false;
|
|
122
|
+
this._idCounter = 0;
|
|
123
|
+
this._taskMap = new Map(); // taskId -> task
|
|
124
|
+
this._dependencyGraph = new Map(); // taskId -> Set of dependent taskIds
|
|
125
|
+
this._completed = new Set();
|
|
126
|
+
this._metrics = new Metrics();
|
|
127
|
+
|
|
128
|
+
for (let i = 0; i < initialSize; i++) {
|
|
129
|
+
this._addThread();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// -----------------------------------------------------------------------
|
|
134
|
+
// Thread management (internal)
|
|
135
|
+
// -----------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Add a new thread to the pool.
|
|
139
|
+
*
|
|
140
|
+
* Respects `maxSize` – returns `null` if the limit is reached.
|
|
141
|
+
* Registers a crash handler that moves local-queue tasks back to
|
|
142
|
+
* the global queue and spawns a replacement.
|
|
143
|
+
*
|
|
144
|
+
* @returns {Thread | null} The new thread, or `null` if at capacity.
|
|
145
|
+
* @private
|
|
146
|
+
*/
|
|
147
|
+
_addThread() {
|
|
148
|
+
if (this._threads.length >= this._maxSize) return null;
|
|
149
|
+
const t = new Thread(this._definition, this._baseOptions);
|
|
150
|
+
t._poolTask = true;
|
|
151
|
+
t._busy = false;
|
|
152
|
+
t._localQueue = [];
|
|
153
|
+
t._taskId = null;
|
|
154
|
+
|
|
155
|
+
if (this._autoRestart) {
|
|
156
|
+
t._onCrash = () => {
|
|
157
|
+
const idx = this._threads.indexOf(t);
|
|
158
|
+
if (idx !== -1) {
|
|
159
|
+
while (t._localQueue.length) {
|
|
160
|
+
this._globalQueue.push(t._localQueue.shift());
|
|
161
|
+
}
|
|
162
|
+
this._threads.splice(idx, 1);
|
|
163
|
+
if (!this._terminated && this._threads.length < this._maxSize) {
|
|
164
|
+
this._addThread();
|
|
165
|
+
}
|
|
166
|
+
this._processQueue();
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this._threads.push(t);
|
|
172
|
+
return t;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Remove a thread from the pool, moving its local tasks back to
|
|
177
|
+
* the global queue.
|
|
178
|
+
*
|
|
179
|
+
* @param {Thread} thread - Thread to remove.
|
|
180
|
+
* @private
|
|
181
|
+
*/
|
|
182
|
+
_removeThread(thread) {
|
|
183
|
+
const idx = this._threads.indexOf(thread);
|
|
184
|
+
if (idx !== -1) {
|
|
185
|
+
while (thread._localQueue.length) {
|
|
186
|
+
this._globalQueue.push(thread._localQueue.shift());
|
|
187
|
+
}
|
|
188
|
+
thread.terminate();
|
|
189
|
+
this._threads.splice(idx, 1);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// -----------------------------------------------------------------------
|
|
194
|
+
// Work stealing (internal)
|
|
195
|
+
// -----------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Steal a task from the busiest thread's local queue.
|
|
199
|
+
*
|
|
200
|
+
* @param {Thread} thread - The idle thread that will receive the task.
|
|
201
|
+
* @returns {Object | null} A task object, or `null` if nothing to steal.
|
|
202
|
+
* @private
|
|
203
|
+
*/
|
|
204
|
+
_stealWork(thread) {
|
|
205
|
+
if (!this._enableStealing) return null;
|
|
206
|
+
let best = null;
|
|
207
|
+
let maxLocal = 0;
|
|
208
|
+
for (const other of this._threads) {
|
|
209
|
+
if (other === thread || other.terminated) continue;
|
|
210
|
+
if (other._localQueue && other._localQueue.length > maxLocal) {
|
|
211
|
+
maxLocal = other._localQueue.length;
|
|
212
|
+
best = other;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (best && best._localQueue.length > 0) {
|
|
216
|
+
return best._localQueue.pop();
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// -----------------------------------------------------------------------
|
|
222
|
+
// Queue processing (internal)
|
|
223
|
+
// -----------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Process the global task queue.
|
|
227
|
+
*
|
|
228
|
+
* Called after every task submission, completion, or failure. Flow:
|
|
229
|
+
* 1. Resolve any blocked tasks whose dependencies are now met.
|
|
230
|
+
* 2. Dequeue the highest-priority task (or steal one).
|
|
231
|
+
* 3. Find an idle thread and dispatch the task.
|
|
232
|
+
* 4. If no idle thread is available, push the task back to the front.
|
|
233
|
+
*
|
|
234
|
+
* @private
|
|
235
|
+
*/
|
|
236
|
+
_processQueue() {
|
|
237
|
+
if (this._terminated) return;
|
|
238
|
+
|
|
239
|
+
this._resolveBlockedTasks();
|
|
240
|
+
|
|
241
|
+
let task = null;
|
|
242
|
+
if (this._globalQueue.length > 0) {
|
|
243
|
+
task = this._globalQueue.shift();
|
|
244
|
+
} else {
|
|
245
|
+
const idle = this._threads.find((t) => !t._busy && !t.terminated);
|
|
246
|
+
if (idle) {
|
|
247
|
+
const stolen = this._stealWork(idle);
|
|
248
|
+
if (stolen) task = stolen;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (!task) return;
|
|
252
|
+
|
|
253
|
+
let thread = this._threads.find((t) => !t._busy && !t.terminated);
|
|
254
|
+
if (!thread) {
|
|
255
|
+
this._globalQueue.unshift(task);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
thread._busy = true;
|
|
260
|
+
thread._taskId = task.taskId;
|
|
261
|
+
this._running++;
|
|
262
|
+
|
|
263
|
+
const runOptions = {};
|
|
264
|
+
if (task.timeout != null) runOptions.timeout = task.timeout;
|
|
265
|
+
if (task.transfer) runOptions.transfer = task.transfer;
|
|
266
|
+
if (task.signal) runOptions.signal = task.signal;
|
|
267
|
+
if (task.retries != null) runOptions.retries = task.retries;
|
|
268
|
+
|
|
269
|
+
thread
|
|
270
|
+
.run(...task.args, runOptions)
|
|
271
|
+
.then((result) => {
|
|
272
|
+
this._running--;
|
|
273
|
+
thread._busy = false;
|
|
274
|
+
thread._taskId = null;
|
|
275
|
+
this._taskMap.delete(task.taskId);
|
|
276
|
+
this._completed.add(task.taskId);
|
|
277
|
+
task.resolve(result);
|
|
278
|
+
this._processQueue();
|
|
279
|
+
})
|
|
280
|
+
.catch((err) => {
|
|
281
|
+
this._running--;
|
|
282
|
+
thread._busy = false;
|
|
283
|
+
thread._taskId = null;
|
|
284
|
+
this._taskMap.delete(task.taskId);
|
|
285
|
+
if (this._autoRestart && thread.terminated) {
|
|
286
|
+
const idx = this._threads.indexOf(thread);
|
|
287
|
+
if (idx !== -1) {
|
|
288
|
+
this._threads.splice(idx, 1);
|
|
289
|
+
if (!this._terminated && this._threads.length < this._maxSize) {
|
|
290
|
+
this._addThread();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
task.reject(err);
|
|
295
|
+
this._rejectDependents(task.taskId, err);
|
|
296
|
+
this._processQueue();
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Move blocked tasks whose dependencies are now satisfied into the
|
|
302
|
+
* global queue.
|
|
303
|
+
*
|
|
304
|
+
* @private
|
|
305
|
+
*/
|
|
306
|
+
_resolveBlockedTasks() {
|
|
307
|
+
const ready = [];
|
|
308
|
+
const remaining = [];
|
|
309
|
+
for (const task of this._blockedTasks) {
|
|
310
|
+
const allCompleted = task.dependsOn.every((depId) => this._completed.has(depId));
|
|
311
|
+
if (allCompleted) {
|
|
312
|
+
ready.push(task);
|
|
313
|
+
} else {
|
|
314
|
+
remaining.push(task);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
this._blockedTasks = remaining;
|
|
318
|
+
for (const task of ready) {
|
|
319
|
+
task._blocked = false;
|
|
320
|
+
this._enqueueTask(task);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Reject all tasks that depend on a failed task.
|
|
326
|
+
*
|
|
327
|
+
* When a task fails, any task waiting on it via `dependsOn` is
|
|
328
|
+
* immediately rejected with a {@link ThreadDependencyError} instead of
|
|
329
|
+
* being left blocked forever.
|
|
330
|
+
*
|
|
331
|
+
* @param {number} failedTaskId - The ID of the task that failed.
|
|
332
|
+
* @param {Error} err - The original error.
|
|
333
|
+
* @private
|
|
334
|
+
*/
|
|
335
|
+
_rejectDependents(failedTaskId, err) {
|
|
336
|
+
const dependentIds = this._dependencyGraph.get(failedTaskId);
|
|
337
|
+
if (!dependentIds) return;
|
|
338
|
+
const depError = new ThreadDependencyError(
|
|
339
|
+
`Dependency task ${failedTaskId} failed: ${err.message || err}`
|
|
340
|
+
);
|
|
341
|
+
for (const depId of dependentIds) {
|
|
342
|
+
const bIdx = this._blockedTasks.findIndex((t) => t.taskId === depId);
|
|
343
|
+
if (bIdx !== -1) {
|
|
344
|
+
const depTask = this._blockedTasks.splice(bIdx, 1)[0];
|
|
345
|
+
this._taskMap.delete(depId);
|
|
346
|
+
depTask.reject(depError);
|
|
347
|
+
}
|
|
348
|
+
const qIdx = this._globalQueue.findIndex((t) => t.taskId === depId);
|
|
349
|
+
if (qIdx !== -1) {
|
|
350
|
+
const depTask = this._globalQueue.splice(qIdx, 1)[0];
|
|
351
|
+
this._taskMap.delete(depId);
|
|
352
|
+
depTask.reject(depError);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
this._dependencyGraph.delete(failedTaskId);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Insert a task into the global queue in priority order.
|
|
360
|
+
*
|
|
361
|
+
* Lower `priority` values are dequeued first.
|
|
362
|
+
*
|
|
363
|
+
* @param {Object} task - Task to enqueue.
|
|
364
|
+
* @private
|
|
365
|
+
*/
|
|
366
|
+
_enqueueTask(task) {
|
|
367
|
+
let inserted = false;
|
|
368
|
+
for (let i = 0; i < this._globalQueue.length; i++) {
|
|
369
|
+
if (this._globalQueue[i].priority > task.priority) {
|
|
370
|
+
this._globalQueue.splice(i, 0, task);
|
|
371
|
+
inserted = true;
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (!inserted) this._globalQueue.push(task);
|
|
376
|
+
this._processQueue();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// -----------------------------------------------------------------------
|
|
380
|
+
// Public API – run
|
|
381
|
+
// -----------------------------------------------------------------------
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Submit a task to the pool.
|
|
385
|
+
*
|
|
386
|
+
* The task is placed into a priority queue and dispatched to the
|
|
387
|
+
* first available thread. Returns both a numeric `id` (for
|
|
388
|
+
* dependency tracking and cancellation) and a `promise` that
|
|
389
|
+
* resolves with the result.
|
|
390
|
+
*
|
|
391
|
+
* If the last argument is a {@link PoolRunOptions} object, its pool-
|
|
392
|
+
* specific keys (`priority`, `dependsOn`, `key`) are consumed and its
|
|
393
|
+
* thread-level keys (`timeout`, `transfer`, `signal`, `retries`) are
|
|
394
|
+
* forwarded to the worker.
|
|
395
|
+
*
|
|
396
|
+
* @param {...any} args
|
|
397
|
+
* Arguments forwarded to the worker's `exec` function. The last
|
|
398
|
+
* argument may be a {@link PoolRunOptions} object.
|
|
399
|
+
* @returns {import('./types.js').PoolTaskResult}
|
|
400
|
+
* `{ id: number, promise: Promise<any> }`.
|
|
401
|
+
*
|
|
402
|
+
* @example
|
|
403
|
+
* ```js
|
|
404
|
+
* const pool = new ThreadPool(2, (x) => x + 1);
|
|
405
|
+
*
|
|
406
|
+
* const { id, promise } = pool.run(10);
|
|
407
|
+
* console.log(await promise); // 11
|
|
408
|
+
* console.log(id); // 0
|
|
409
|
+
* ```
|
|
410
|
+
*
|
|
411
|
+
* @example
|
|
412
|
+
* ```js
|
|
413
|
+
* // With priority
|
|
414
|
+
* pool.run(urgent, { priority: 0 }); // runs first
|
|
415
|
+
* pool.run(batch, { priority: 10 }); // runs second
|
|
416
|
+
* ```
|
|
417
|
+
*
|
|
418
|
+
* @example
|
|
419
|
+
* ```js
|
|
420
|
+
* // With dependencies
|
|
421
|
+
* const a = pool.run(data);
|
|
422
|
+
* const b = pool.run(a.id, { dependsOn: [a.id] });
|
|
423
|
+
* await b.promise; // waits for 'a' to finish first
|
|
424
|
+
* ```
|
|
425
|
+
*
|
|
426
|
+
* @example
|
|
427
|
+
* ```js
|
|
428
|
+
* // With abort and timeout
|
|
429
|
+
* const controller = new AbortController();
|
|
430
|
+
* const { promise } = pool.run(bigData, {
|
|
431
|
+
* timeout: 30_000,
|
|
432
|
+
* signal: controller.signal,
|
|
433
|
+
* transfer: [bigData.buffer],
|
|
434
|
+
* retries: 2,
|
|
435
|
+
* });
|
|
436
|
+
* ```
|
|
437
|
+
*/
|
|
438
|
+
run(...args) {
|
|
439
|
+
let options = {};
|
|
440
|
+
const last = args[args.length - 1];
|
|
441
|
+
if (
|
|
442
|
+
last &&
|
|
443
|
+
typeof last === 'object' &&
|
|
444
|
+
!Array.isArray(last) &&
|
|
445
|
+
('priority' in last ||
|
|
446
|
+
'dependsOn' in last ||
|
|
447
|
+
'key' in last ||
|
|
448
|
+
'timeout' in last ||
|
|
449
|
+
'transfer' in last ||
|
|
450
|
+
'signal' in last ||
|
|
451
|
+
'retries' in last)
|
|
452
|
+
) {
|
|
453
|
+
options = args.pop();
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const priority = options.priority ?? 0;
|
|
457
|
+
const dependsOn = options.dependsOn || [];
|
|
458
|
+
const key = options.key || null;
|
|
459
|
+
|
|
460
|
+
if (this._terminated) {
|
|
461
|
+
return {
|
|
462
|
+
id: -1,
|
|
463
|
+
promise: Promise.reject(new ThreadTerminatedError('Pool terminated')),
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const taskId = this._idCounter++;
|
|
468
|
+
let resolveFn, rejectFn;
|
|
469
|
+
const promise = new Promise((resolve, reject) => {
|
|
470
|
+
resolveFn = resolve;
|
|
471
|
+
rejectFn = reject;
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const task = {
|
|
475
|
+
args,
|
|
476
|
+
resolve: resolveFn,
|
|
477
|
+
reject: rejectFn,
|
|
478
|
+
taskId,
|
|
479
|
+
priority,
|
|
480
|
+
dependsOn,
|
|
481
|
+
key,
|
|
482
|
+
retries: options.retries,
|
|
483
|
+
signal: options.signal,
|
|
484
|
+
timeout: options.timeout,
|
|
485
|
+
transfer: options.transfer,
|
|
486
|
+
_blocked: dependsOn.length > 0,
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
this._taskMap.set(taskId, task);
|
|
490
|
+
|
|
491
|
+
const allCompleted = dependsOn.every((depId) => this._completed.has(depId));
|
|
492
|
+
if (allCompleted) {
|
|
493
|
+
this._enqueueTask(task);
|
|
494
|
+
} else {
|
|
495
|
+
this._blockedTasks.push(task);
|
|
496
|
+
for (const depId of dependsOn) {
|
|
497
|
+
if (!this._completed.has(depId)) {
|
|
498
|
+
if (!this._dependencyGraph.has(depId)) {
|
|
499
|
+
this._dependencyGraph.set(depId, new Set());
|
|
500
|
+
}
|
|
501
|
+
this._dependencyGraph.get(depId).add(taskId);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return { id: taskId, promise };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Cancel a queued task by its ID.
|
|
511
|
+
*
|
|
512
|
+
* Only works for tasks that are **still queued** (either in the global
|
|
513
|
+
* queue or blocked waiting on dependencies). Tasks that are already
|
|
514
|
+
* running on a thread cannot be cancelled this way – use an
|
|
515
|
+
* `AbortSignal` instead.
|
|
516
|
+
*
|
|
517
|
+
* @param {number} taskId - The task ID returned by {@link ThreadPool.run}.
|
|
518
|
+
* @returns {boolean} `true` if the task was found and cancelled.
|
|
519
|
+
*
|
|
520
|
+
* @example
|
|
521
|
+
* ```js
|
|
522
|
+
* const { id } = pool.run(slowTask);
|
|
523
|
+
*
|
|
524
|
+
* // User changed their mind
|
|
525
|
+
* if (pool.cancel(id)) {
|
|
526
|
+
* console.log('Task cancelled before it started');
|
|
527
|
+
* }
|
|
528
|
+
* ```
|
|
529
|
+
*/
|
|
530
|
+
cancel(taskId) {
|
|
531
|
+
const idx = this._globalQueue.findIndex((t) => t.taskId === taskId);
|
|
532
|
+
if (idx !== -1) {
|
|
533
|
+
const task = this._globalQueue.splice(idx, 1)[0];
|
|
534
|
+
this._taskMap.delete(taskId);
|
|
535
|
+
task.reject(new ThreadAbortError('Task cancelled'));
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
const bIdx = this._blockedTasks.findIndex((t) => t.taskId === taskId);
|
|
539
|
+
if (bIdx !== -1) {
|
|
540
|
+
const task = this._blockedTasks.splice(bIdx, 1)[0];
|
|
541
|
+
this._taskMap.delete(taskId);
|
|
542
|
+
task.reject(new ThreadAbortError('Task cancelled'));
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// -----------------------------------------------------------------------
|
|
549
|
+
// Public API – pool management
|
|
550
|
+
// -----------------------------------------------------------------------
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Dynamically resize the pool.
|
|
554
|
+
*
|
|
555
|
+
* If `newSize` is larger, new threads are spawned. If smaller, excess
|
|
556
|
+
* threads are removed (busy threads are marked for deferred removal).
|
|
557
|
+
*
|
|
558
|
+
* @param {number} newSize - Desired number of threads.
|
|
559
|
+
* @throws {Error} If `newSize < 0`.
|
|
560
|
+
* @throws {Error} If `newSize > maxSize`.
|
|
561
|
+
*
|
|
562
|
+
* @example
|
|
563
|
+
* ```js
|
|
564
|
+
* // Scale up for a burst of work
|
|
565
|
+
* pool.scaleTo(8);
|
|
566
|
+
* await Promise.all(bigBatch.map((item) => pool.run(item).promise));
|
|
567
|
+
*
|
|
568
|
+
* // Scale back down to conserve resources
|
|
569
|
+
* pool.scaleTo(2);
|
|
570
|
+
* ```
|
|
571
|
+
*/
|
|
572
|
+
scaleTo(newSize) {
|
|
573
|
+
if (newSize < 0) throw new Error('Size must be non‑negative');
|
|
574
|
+
if (newSize > this._maxSize) throw new Error(`Exceeds maxSize ${this._maxSize}`);
|
|
575
|
+
const current = this._threads.length;
|
|
576
|
+
if (newSize > current) {
|
|
577
|
+
for (let i = 0; i < newSize - current; i++) this._addThread();
|
|
578
|
+
} else if (newSize < current) {
|
|
579
|
+
const toRemove = this._threads.slice(newSize);
|
|
580
|
+
for (const t of toRemove) {
|
|
581
|
+
if (!t._busy) this._removeThread(t);
|
|
582
|
+
else t._pendingRemoval = true;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Gracefully shut down after all tasks finish.
|
|
589
|
+
*
|
|
590
|
+
* Polls every 50ms until the global queue, blocked list, and all
|
|
591
|
+
* threads are idle, then calls {@link ThreadPool.terminateAll}.
|
|
592
|
+
*
|
|
593
|
+
* @returns {Promise<void>}
|
|
594
|
+
*
|
|
595
|
+
* @example
|
|
596
|
+
* ```js
|
|
597
|
+
* process.on('SIGTERM', async () => {
|
|
598
|
+
* console.log('Shutting down gracefully...');
|
|
599
|
+
* await pool.terminateGracefully();
|
|
600
|
+
* console.log('All tasks finished');
|
|
601
|
+
* });
|
|
602
|
+
* ```
|
|
603
|
+
*/
|
|
604
|
+
async terminateGracefully() {
|
|
605
|
+
while (
|
|
606
|
+
this._globalQueue.length > 0 ||
|
|
607
|
+
this._blockedTasks.length > 0 ||
|
|
608
|
+
this._threads.some((t) => t._busy)
|
|
609
|
+
) {
|
|
610
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
611
|
+
}
|
|
612
|
+
this.terminateAll();
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Immediately terminate all threads and clear all queues.
|
|
617
|
+
*
|
|
618
|
+
* - Every thread is killed via `thread.terminate()`.
|
|
619
|
+
* - All queued and blocked tasks are rejected with
|
|
620
|
+
* {@link ThreadTerminatedError}.
|
|
621
|
+
* - The dependency graph, completed set, and task map are cleared.
|
|
622
|
+
*
|
|
623
|
+
* @example
|
|
624
|
+
* ```js
|
|
625
|
+
* // Emergency shutdown
|
|
626
|
+
* pool.terminateAll();
|
|
627
|
+
* console.log(pool.status().total); // 0
|
|
628
|
+
* ```
|
|
629
|
+
*/
|
|
630
|
+
terminateAll() {
|
|
631
|
+
this._terminated = true;
|
|
632
|
+
this._threads.forEach((t) => t.terminate());
|
|
633
|
+
this._threads = [];
|
|
634
|
+
this._globalQueue = [];
|
|
635
|
+
for (const task of this._blockedTasks) {
|
|
636
|
+
task.reject(new ThreadTerminatedError('Pool terminated'));
|
|
637
|
+
}
|
|
638
|
+
this._blockedTasks = [];
|
|
639
|
+
for (const [id, task] of this._taskMap) {
|
|
640
|
+
task.reject(new ThreadTerminatedError('Pool terminated'));
|
|
641
|
+
}
|
|
642
|
+
this._taskMap.clear();
|
|
643
|
+
this._dependencyGraph.clear();
|
|
644
|
+
this._completed.clear();
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Return a snapshot of the pool's current state.
|
|
649
|
+
*
|
|
650
|
+
* @returns {import('./types.js').PoolStatus}
|
|
651
|
+
*
|
|
652
|
+
* @example
|
|
653
|
+
* ```js
|
|
654
|
+
* const s = pool.status();
|
|
655
|
+
* console.log(`${s.busy}/${s.total} threads busy, ${s.queued} tasks queued`);
|
|
656
|
+
* ```
|
|
657
|
+
*/
|
|
658
|
+
status() {
|
|
659
|
+
const total = this._threads.length;
|
|
660
|
+
const busy = this._threads.filter((t) => t._busy).length;
|
|
661
|
+
const localQueues = this._threads.reduce(
|
|
662
|
+
(sum, t) => sum + (t._localQueue ? t._localQueue.length : 0),
|
|
663
|
+
0,
|
|
664
|
+
);
|
|
665
|
+
return {
|
|
666
|
+
total,
|
|
667
|
+
busy,
|
|
668
|
+
idle: total - busy,
|
|
669
|
+
queued: this._globalQueue.length + this._blockedTasks.length,
|
|
670
|
+
localQueued: localQueues,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Wait for all tasks to finish **without** terminating the pool.
|
|
676
|
+
*
|
|
677
|
+
* Useful when you want to process all submitted work and then inspect
|
|
678
|
+
* results, but keep the pool alive for more tasks.
|
|
679
|
+
*
|
|
680
|
+
* @returns {Promise<void>}
|
|
681
|
+
*
|
|
682
|
+
* @example
|
|
683
|
+
* ```js
|
|
684
|
+
* for (const item of items) pool.run(item);
|
|
685
|
+
* await pool.drain();
|
|
686
|
+
* console.log('All items processed');
|
|
687
|
+
* // pool is still alive – submit more tasks if needed
|
|
688
|
+
* ```
|
|
689
|
+
*/
|
|
690
|
+
async drain() {
|
|
691
|
+
while (
|
|
692
|
+
this._globalQueue.length > 0 ||
|
|
693
|
+
this._blockedTasks.length > 0 ||
|
|
694
|
+
this._threads.some((t) => t._busy)
|
|
695
|
+
) {
|
|
696
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Warm up all threads by running a no-op task on each.
|
|
702
|
+
*
|
|
703
|
+
* Forces every worker to compile the exec function so the first real
|
|
704
|
+
* task is fast.
|
|
705
|
+
*
|
|
706
|
+
* @param {number} [timeout=5000] - Max ms per warmup task.
|
|
707
|
+
* @returns {Promise<void>}
|
|
708
|
+
*
|
|
709
|
+
* @example
|
|
710
|
+
* ```js
|
|
711
|
+
* const pool = new ThreadPool(4, heavyComputation);
|
|
712
|
+
* await pool.warmup(); // all 4 workers are now compiled
|
|
713
|
+
* ```
|
|
714
|
+
*/
|
|
715
|
+
async warmup(timeout = 5000) {
|
|
716
|
+
await Promise.all(this._threads.map((t) => t.warmup(timeout)));
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// -----------------------------------------------------------------------
|
|
720
|
+
// Getters
|
|
721
|
+
// -----------------------------------------------------------------------
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Cumulative pool-wide metrics snapshot.
|
|
725
|
+
*
|
|
726
|
+
* Only tracks tasks that completed through the pool's own `.run()`
|
|
727
|
+
* method. Individual thread metrics are not aggregated here.
|
|
728
|
+
*
|
|
729
|
+
* @type {import('./types.js').MetricsSnapshot}
|
|
730
|
+
*
|
|
731
|
+
* @example
|
|
732
|
+
* ```js
|
|
733
|
+
* const m = pool.metrics;
|
|
734
|
+
* console.log(`${m.count} tasks, ${(m.errorRate * 100).toFixed(1)}% errors`);
|
|
735
|
+
* ```
|
|
736
|
+
*/
|
|
737
|
+
get metrics() {
|
|
738
|
+
return this._metrics.snapshot();
|
|
739
|
+
}
|
|
740
|
+
}
|