233 lines
6.9 KiB
TypeScript
Executable file
233 lines
6.9 KiB
TypeScript
Executable file
import { EventEmitter } from 'events';
|
|
import Piscina from '..';
|
|
import { test } from 'tap';
|
|
import { resolve } from 'path';
|
|
|
|
test('tasks can be aborted through AbortController while running', async ({ equal, rejects }) => {
|
|
const pool = new Piscina({
|
|
filename: resolve(__dirname, 'fixtures/notify-then-sleep.ts')
|
|
});
|
|
|
|
const buf = new Int32Array(new SharedArrayBuffer(4));
|
|
const abortController = new AbortController();
|
|
rejects(pool.runTask(buf, abortController.signal),
|
|
/The task has been aborted/);
|
|
|
|
Atomics.wait(buf, 0, 0);
|
|
equal(Atomics.load(buf, 0), 1);
|
|
|
|
abortController.abort();
|
|
});
|
|
|
|
test('tasks can be aborted through EventEmitter while running', async ({ equal, rejects }) => {
|
|
const pool = new Piscina({
|
|
filename: resolve(__dirname, 'fixtures/notify-then-sleep.ts')
|
|
});
|
|
|
|
const buf = new Int32Array(new SharedArrayBuffer(4));
|
|
const ee = new EventEmitter();
|
|
rejects(pool.runTask(buf, ee), /The task has been aborted/);
|
|
rejects(pool.run(buf, { signal: ee }), /The task has been aborted/);
|
|
|
|
Atomics.wait(buf, 0, 0);
|
|
equal(Atomics.load(buf, 0), 1);
|
|
|
|
ee.emit('abort');
|
|
});
|
|
|
|
test('tasks can be aborted through EventEmitter before running', async ({ equal, rejects }) => {
|
|
const pool = new Piscina({
|
|
filename: resolve(__dirname, 'fixtures/wait-for-notify.ts'),
|
|
maxThreads: 1
|
|
});
|
|
|
|
const bufs = [
|
|
new Int32Array(new SharedArrayBuffer(4)),
|
|
new Int32Array(new SharedArrayBuffer(4))
|
|
];
|
|
const task1 = pool.runTask(bufs[0]);
|
|
const ee = new EventEmitter();
|
|
rejects(pool.runTask(bufs[1], ee), /The task has been aborted/);
|
|
rejects(pool.run(bufs[1], { signal: ee }), /The task has been aborted/);
|
|
equal(pool.queueSize, 2);
|
|
|
|
ee.emit('abort');
|
|
|
|
// Wake up the thread handling the first task.
|
|
Atomics.store(bufs[0], 0, 1);
|
|
Atomics.notify(bufs[0], 0, 1);
|
|
await task1;
|
|
});
|
|
|
|
test('abortable tasks will not share workers (abortable posted second)', async ({ equal, rejects }) => {
|
|
const pool = new Piscina({
|
|
filename: resolve(__dirname, 'fixtures/wait-for-notify.ts'),
|
|
maxThreads: 1,
|
|
concurrentTasksPerWorker: 2
|
|
});
|
|
|
|
const bufs = [
|
|
new Int32Array(new SharedArrayBuffer(4)),
|
|
new Int32Array(new SharedArrayBuffer(4))
|
|
];
|
|
const task1 = pool.runTask(bufs[0]);
|
|
const ee = new EventEmitter();
|
|
rejects(pool.runTask(bufs[1], ee), /The task has been aborted/);
|
|
equal(pool.queueSize, 1);
|
|
|
|
ee.emit('abort');
|
|
|
|
// Wake up the thread handling the first task.
|
|
Atomics.store(bufs[0], 0, 1);
|
|
Atomics.notify(bufs[0], 0, 1);
|
|
await task1;
|
|
});
|
|
|
|
test('abortable tasks will not share workers (abortable posted first)', async ({ equal, rejects }) => {
|
|
const pool = new Piscina({
|
|
filename: resolve(__dirname, 'fixtures/eval.js'),
|
|
maxThreads: 1,
|
|
concurrentTasksPerWorker: 2
|
|
});
|
|
|
|
const ee = new EventEmitter();
|
|
rejects(pool.runTask('while(true);', ee), /The task has been aborted/);
|
|
const task2 = pool.runTask('42');
|
|
equal(pool.queueSize, 1);
|
|
|
|
ee.emit('abort');
|
|
|
|
// Wake up the thread handling the second task.
|
|
equal(await task2, 42);
|
|
});
|
|
|
|
test('abortable tasks will not share workers (on worker available)', async ({ equal }) => {
|
|
const pool = new Piscina({
|
|
filename: resolve(__dirname, 'fixtures/sleep.js'),
|
|
maxThreads: 1,
|
|
concurrentTasksPerWorker: 2
|
|
});
|
|
|
|
// Task 1 will sleep 100 ms then complete,
|
|
// Task 2 will sleep 300 ms then complete.
|
|
// Abortable task 3 should still be in the queue
|
|
// when Task 1 completes, but should not be selected
|
|
// until after Task 2 completes because it is abortable.
|
|
|
|
const ret = await Promise.all([
|
|
pool.runTask({ time: 100, a: 1 }),
|
|
pool.runTask({ time: 300, a: 2 }),
|
|
pool.runTask({ time: 100, a: 3 }, new EventEmitter())
|
|
]);
|
|
|
|
equal(ret[0], 0);
|
|
equal(ret[1], 1);
|
|
equal(ret[2], 2);
|
|
});
|
|
|
|
test('abortable tasks will not share workers (destroy workers)', async ({ rejects }) => {
|
|
const pool = new Piscina({
|
|
filename: resolve(__dirname, 'fixtures/sleep.js'),
|
|
maxThreads: 1,
|
|
concurrentTasksPerWorker: 2
|
|
});
|
|
|
|
// Task 1 will sleep 100 ms then complete,
|
|
// Task 2 will sleep 300 ms then complete.
|
|
// Abortable task 3 should still be in the queue
|
|
// when Task 1 completes, but should not be selected
|
|
// until after Task 2 completes because it is abortable.
|
|
|
|
pool.runTask({ time: 100, a: 1 }).then(() => {
|
|
pool.destroy();
|
|
});
|
|
|
|
rejects(pool.runTask({ time: 300, a: 2 }), /Terminating worker thread/);
|
|
rejects(pool.runTask({ time: 100, a: 3 }, new EventEmitter()),
|
|
/Terminating worker thread/);
|
|
});
|
|
|
|
test('aborted AbortSignal rejects task immediately', async ({ rejects, equal }) => {
|
|
const pool = new Piscina({
|
|
filename: resolve(__dirname, 'fixtures/move.ts')
|
|
});
|
|
|
|
const controller = new AbortController();
|
|
// Abort the controller early
|
|
controller.abort();
|
|
equal(controller.signal.aborted, true);
|
|
|
|
// The data won't be moved because the task will abort immediately.
|
|
const data = new Uint8Array(new SharedArrayBuffer(4));
|
|
rejects(pool.runTask(data, [data.buffer], controller.signal),
|
|
/The task has been aborted/);
|
|
|
|
equal(data.length, 4);
|
|
});
|
|
|
|
test('task with AbortSignal cleans up properly', async ({ equal }) => {
|
|
const pool = new Piscina({
|
|
filename: resolve(__dirname, 'fixtures/eval.js')
|
|
});
|
|
|
|
const ee = new EventEmitter();
|
|
|
|
await pool.runTask('1+1', ee);
|
|
|
|
const { getEventListeners } = EventEmitter as any;
|
|
if (typeof getEventListeners === 'function') {
|
|
equal(getEventListeners(ee, 'abort').length, 0);
|
|
}
|
|
|
|
const controller = new AbortController();
|
|
|
|
await pool.runTask('1+1', controller.signal);
|
|
});
|
|
|
|
test('aborted AbortSignal rejects task immediately (with reason)', async ({ match, equal }) => {
|
|
const pool = new Piscina({
|
|
filename: resolve(__dirname, 'fixtures/move.ts')
|
|
});
|
|
const customReason = new Error('custom reason');
|
|
|
|
const controller = new AbortController();
|
|
controller.abort(customReason);
|
|
equal(controller.signal.aborted, true);
|
|
equal(controller.signal.reason, customReason);
|
|
|
|
// The data won't be moved because the task will abort immediately.
|
|
const data = new Uint8Array(new SharedArrayBuffer(4));
|
|
|
|
try {
|
|
await pool.run(data, { transferList: [data.buffer], signal: controller.signal });
|
|
} catch (error) {
|
|
equal(error.message, 'The task has been aborted');
|
|
match(error.cause, customReason);
|
|
}
|
|
|
|
equal(data.length, 4);
|
|
});
|
|
|
|
test('tasks can be aborted through AbortController while running', async ({ equal, match }) => {
|
|
const pool = new Piscina({
|
|
filename: resolve(__dirname, 'fixtures/notify-then-sleep.ts')
|
|
});
|
|
const reason = new Error('custom reason');
|
|
|
|
const buf = new Int32Array(new SharedArrayBuffer(4));
|
|
const abortController = new AbortController();
|
|
|
|
try {
|
|
const promise = pool.run(buf, { signal: abortController.signal });
|
|
|
|
Atomics.wait(buf, 0, 0);
|
|
equal(Atomics.load(buf, 0), 1);
|
|
|
|
abortController.abort(reason);
|
|
|
|
await promise;
|
|
} catch (error) {
|
|
equal(error.message, 'The task has been aborted');
|
|
match(error.cause, reason);
|
|
}
|
|
});
|