Skip to content

Commit 7af6e83

Browse files
authored
feat: Queued task prefers ready worker (#1058)
1 parent 999ca27 commit 7af6e83

3 files changed

Lines changed: 90 additions & 1 deletion

File tree

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,8 @@ class ThreadPool {
404404
this.taskQueue.shift() as TaskInfo;
405405

406406
if (workers == null) {
407-
workers = [...this.workers].map(workerInfo => workerInfo.interface);
407+
workers = [...this.workers.readyItems, ...this.workers.pendingItems]
408+
.map(workerInfo => workerInfo.interface);
408409
}
409410

410411
const distributed = this._distributeTask(taskInfo, workers);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
'use strict';
2+
3+
// A worker whose readiness and task execution the main thread can release on
4+
// demand, letting a test deterministically hold a worker in the pending state
5+
// or keep one busy.
6+
//
7+
// The worker shares an Int32Array with the main thread (passed as workerData).
8+
// Each slot is a gate: the worker blocks on Atomics.wait until the main thread
9+
// stores a non-zero value into that slot and notifies.
10+
//
11+
// The readiness gate is awaited at module top level. A worker is only marked
12+
// ready once its module finishes loading, so blocking here keeps it pending
13+
// until the main thread lets it through. The task gate then holds a running
14+
// task until released. The returned threadId lets a test see which worker ran
15+
// a task.
16+
17+
const { threadId, workerData } = require('node:worker_threads');
18+
19+
const READY_GATE = 0;
20+
const TASK_GATE = 1;
21+
22+
const gates = new Int32Array(workerData);
23+
24+
Atomics.wait(gates, READY_GATE, 0);
25+
26+
module.exports = ({ hold = false } = {}) => {
27+
if (hold) {
28+
Atomics.wait(gates, TASK_GATE, 0);
29+
}
30+
return threadId;
31+
};
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import * as assert from 'node:assert/strict';
2+
import { test } from 'node:test';
3+
import { resolve } from 'node:path';
4+
import Piscina from '..';
5+
6+
const READY_SIGNAL = 0;
7+
const TASK_SIGNAL = 1;
8+
9+
test('a queued task prefers a ready worker over a still-pending one', async () => {
10+
const gates = new Int32Array(
11+
new SharedArrayBuffer(2 * Int32Array.BYTES_PER_ELEMENT)
12+
);
13+
14+
Atomics.store(gates, READY_SIGNAL, 1);
15+
16+
const pool = new Piscina({
17+
filename: resolve(__dirname, 'fixtures/wait-until-released.js'),
18+
minThreads: 1,
19+
maxThreads: 2,
20+
concurrentTasksPerWorker: 1,
21+
workerData: gates.buffer
22+
});
23+
24+
// One ready, idle worker.
25+
const warmThreadId = await pool.run({});
26+
27+
// Keep the next worker from becoming ready.
28+
Atomics.store(gates, READY_SIGNAL, 0);
29+
30+
// Occupy the ready worker, then queue a task behind it. A second worker
31+
// spawns but stays pending.
32+
const held = pool.run({ hold: true });
33+
const queued = pool.run({});
34+
35+
// Free the ready worker while the second is still pending: the queued task
36+
// should land on the ready worker.
37+
Atomics.store(gates, TASK_SIGNAL, 1);
38+
Atomics.notify(gates, TASK_SIGNAL, Infinity);
39+
40+
const heldThreadId = await held;
41+
42+
// Let the second worker finish so the pool can shut down (and the queued task
43+
// resolves even if it was wrongly routed there).
44+
Atomics.store(gates, READY_SIGNAL, 1);
45+
Atomics.notify(gates, READY_SIGNAL, Infinity);
46+
47+
const queuedThreadId = await queued;
48+
49+
assert.strictEqual(heldThreadId, warmThreadId);
50+
assert.strictEqual(
51+
queuedThreadId,
52+
warmThreadId,
53+
'queued task should go to the ready worker, not the still-pending one'
54+
);
55+
56+
await pool.destroy();
57+
});

0 commit comments

Comments
 (0)