Skip to content

Commit d081228

Browse files
authored
feat!: drain/needsDrain accounts for task queue (#872)
1 parent 5204b9b commit d081228

5 files changed

Lines changed: 39 additions & 38 deletions

File tree

docs/docs/api-reference/event.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@ A `'drain'` event is emitted whenever the `queueSize` reaches `0`.
2121

2222
## Event: `'needsDrain'`
2323

24-
Similar to [`Piscina#needsDrain`](https://github.com/piscinajs/piscina#property-needsdrain-readonly);
25-
this event is triggered once the total capacity of the pool is exceeded
26-
by number of tasks enqueued that are pending of execution.
24+
Similar to [`Piscina#needsDrain`](https://github.com/piscinajs/piscina#property-needsdrain-readonly).
2725

2826
## Event: `'message'`
2927

docs/docs/api-reference/property.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,17 @@ The current number of tasks waiting to be assigned to a Worker thread.
6868

6969
## Property: `needsDrain` (readonly)
7070

71-
Boolean value that specifies whether the capacity of the pool has
72-
been exceeded by the number of tasks submitted.
71+
Boolean value that specifies whether the pool requires draining before processing new tasks.
7372

74-
This property is helpful to make decisions towards creating backpressure
75-
over the number of tasks submitted to the pool.
73+
This is `true` when there is not enough capacity to process more tasks
74+
without exceeding the maximum queue size or within the maximum number of threads.
75+
76+
This is often calculated based on the number of threads (current and pending if number of threads is below `maxThreads` threshold),
77+
the maximum queue size, and the current number of in-flight tasks.
78+
79+
This property is helpful to make decisions towards creating backpressure.
80+
81+
> **Note**: Piscina does not buffer tasks beyond the `maxQueue` threshold, so this property is not a guarantee that the pool will accept more tasks.
7682
7783
## Property: `utilization` (readonly)
7884

src/index.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,7 @@ class ThreadPool {
452452
this.histogram?.recordWaitTime(now - task.created)
453453
task.started = now;
454454
candidate[kWorkerData].postTask(task);
455-
queueMicrotask(() => this._maybeDrain());
455+
this._maybeDrain();
456456
// If candidate, let's try to distribute more tasks
457457
return true;
458458
}
@@ -515,7 +515,7 @@ class ThreadPool {
515515
resolve(result);
516516
}
517517

518-
queueMicrotask(this._maybeDrain.bind(this))
518+
this._maybeDrain();
519519
});
520520

521521
if (signal != null) {
@@ -558,7 +558,7 @@ class ThreadPool {
558558
this.taskQueue.push(taskInfo);
559559
}
560560

561-
queueMicrotask(this._maybeDrain.bind(this))
561+
this._maybeDrain();
562562
return ret;
563563
}
564564

@@ -578,7 +578,7 @@ class ThreadPool {
578578
}
579579
};
580580

581-
queueMicrotask(this._maybeDrain.bind(this))
581+
this._maybeDrain();
582582
return ret;
583583
}
584584

@@ -594,16 +594,26 @@ class ThreadPool {
594594
* since we want to avoid creating tasks that can't execute
595595
* immediately in order to provide back pressure to the task source.
596596
*/
597-
const { maxCapacity } = this;
597+
const { maxCapacity, } = this;
598598
const currentUsage = this.workers.getCurrentUsage();
599+
const maxQueueSize = this.options.maxQueue;
600+
const queueSize = this.publicInterface.queueSize;
599601

600-
if (maxCapacity === currentUsage) {
602+
if (this._needsDrain === true) {
603+
if (queueSize === 0) {
604+
this._needsDrain = false;
605+
queueMicrotask(() => this.publicInterface.emit('drain'));
606+
}
607+
return;
608+
}
609+
610+
// Attempting to provide a similar behaviour to a Writable stream
611+
// if maxquesize is already reached, let's attempt to inform that the
612+
// queue needs drain before handling more tasks
613+
if (maxCapacity === currentUsage && queueSize === maxQueueSize) {
601614
this._needsDrain = true;
602615
queueMicrotask(() => this.publicInterface.emit('needsDrain'));
603-
} else if (maxCapacity > currentUsage && this._needsDrain) {
604-
this._needsDrain = false;
605-
queueMicrotask(() => this.publicInterface.emit('drain'));
606-
}
616+
}
607617
}
608618

609619
async destroy () {
@@ -853,7 +863,7 @@ export default class Piscina<Exports extends Record<string, (payload: any) => an
853863

854864
get queueSize () : number {
855865
const pool = this.#pool;
856-
return Math.max(pool.taskQueue.size - pool.pendingCapacity(), 0);
866+
return Math.max((pool.taskQueue.size + pool.skipQueue.length) - pool.pendingCapacity(), 0);
857867
}
858868

859869
get completed () : number {

test/abort-task.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ test('tasks can be aborted through EventEmitter before running', async () => {
5050
const ee = new EventEmitter();
5151
const task1 = pool.run(bufs[0]);
5252
const abortable = pool.run(bufs[1], { signal: ee });
53-
assert.strictEqual(pool.queueSize, 0); // Means it's running
53+
assert.strictEqual(pool.queueSize, 1); // Means it's running and abortable enqueued
5454
assert.rejects(abortable, /The task has been aborted/);
5555

5656
ee.emit('abort');
@@ -75,7 +75,7 @@ test('abortable tasks will not share workers (abortable posted second)', async (
7575
const task1 = pool.run(bufs[0]);
7676
const ee = new EventEmitter();
7777
assert.rejects(pool.run(bufs[1], { signal: ee }), /The task has been aborted/);
78-
assert.strictEqual(pool.queueSize, 0);
78+
assert.strictEqual(pool.queueSize, 1);
7979

8080
ee.emit('abort');
8181

test/post-task.test.ts

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import assert from 'node:assert';
22
import { test, TestContext } from 'node:test';
33
import { MessageChannel } from 'node:worker_threads';
44
import { resolve } from 'node:path';
5+
56
import Piscina from '..';
67
import { getAvailableParallelism } from '../dist/common';
78

@@ -77,36 +78,22 @@ test('postTask() validates abortSignal', () => {
7778
/signal argument must be an object/);
7879
});
7980

80-
test('Piscina emits drain', async (t: TestContext) => {
81-
const pool = new Piscina({
82-
filename: resolve(__dirname, 'fixtures/eval.js'),
83-
maxThreads: 1
84-
});
85-
86-
t.plan(2);
87-
88-
pool.on('drain', () => {
89-
t.assert.ok(true);
90-
t.assert.ok(!pool.needsDrain);
91-
});
92-
93-
await Promise.all([pool.run('123'), pool.run('123'), pool.run('123')]);
94-
});
95-
96-
test('Piscina exposes/emits needsDrain to true when capacity is exceeded', async (t: TestContext) => {
81+
test('Piscina exposes/emits drain/needsDrain to true when capacity is exceeded', async (t: TestContext) => {
9782
const pool = new Piscina({
9883
filename: resolve(__dirname, 'fixtures/eval.js'),
9984
maxQueue: 3,
10085
maxThreads: 1
10186
});
10287

103-
t.plan(2);
88+
t.plan(4);
10489

10590
pool.once('drain', () => {
10691
t.assert.ok(true);
92+
t.assert.ok(pool.queueSize < 3);
10793
});
10894
pool.once('needsDrain', () => {
10995
t.assert.ok(pool.needsDrain);
96+
t.assert.equal(pool.queueSize, 3);
11097
});
11198

11299
await Promise.all([

0 commit comments

Comments
 (0)