import { EventEmitterAsyncResource } from 'node:events'
+import { dirname, join } from 'node:path'
import { readFileSync } from 'node:fs'
+import { fileURLToPath } from 'node:url'
+import { createHook, executionAsyncId } from 'node:async_hooks'
import { expect } from 'expect'
import { restore, stub } from 'sinon'
import {
import { WorkerNode } from '../../lib/pools/worker-node.js'
describe('Abstract pool test suite', () => {
- const version = JSON.parse(readFileSync('./package.json', 'utf8')).version
+ const version = JSON.parse(
+ readFileSync(
+ join(dirname(fileURLToPath(import.meta.url)), '../..', 'package.json'),
+ 'utf8'
+ )
+ ).version
const numberOfWorkers = 2
class StubPoolWithIsMain extends FixedThreadPool {
isMain () {
restore()
})
- it('Simulate pool creation from a non main thread/process', () => {
+ it('Verify that pool can be created and destroyed', async () => {
+ const pool = new FixedThreadPool(
+ numberOfWorkers,
+ './tests/worker-files/thread/testWorker.mjs'
+ )
+ expect(pool).toBeInstanceOf(FixedThreadPool)
+ await pool.destroy()
+ })
+
+ it('Verify that pool cannot be created from a non main thread/process', () => {
expect(
() =>
new StubPoolWithIsMain(
errorHandler: e => console.error(e)
}
)
- ).toThrowError(
+ ).toThrow(
new Error(
'Cannot start a pool from a worker with the same type as the pool'
)
numberOfWorkers,
'./tests/worker-files/thread/testWorker.mjs'
)
- expect(pool.starting).toBe(false)
expect(pool.started).toBe(true)
+ expect(pool.starting).toBe(false)
+ expect(pool.destroying).toBe(false)
await pool.destroy()
})
it('Verify that filePath is checked', () => {
- const expectedError = new Error(
- 'Please specify a file with a worker implementation'
- )
- expect(() => new FixedThreadPool(numberOfWorkers)).toThrowError(
- expectedError
+ expect(() => new FixedThreadPool(numberOfWorkers)).toThrow(
+ new TypeError('The worker file path must be specified')
)
- expect(() => new FixedThreadPool(numberOfWorkers, '')).toThrowError(
- expectedError
- )
- expect(() => new FixedThreadPool(numberOfWorkers, 0)).toThrowError(
- expectedError
- )
- expect(() => new FixedThreadPool(numberOfWorkers, true)).toThrowError(
- expectedError
+ expect(() => new FixedThreadPool(numberOfWorkers, 0)).toThrow(
+ new TypeError('The worker file path must be a string')
)
expect(
() => new FixedThreadPool(numberOfWorkers, './dummyWorker.ts')
- ).toThrowError(new Error("Cannot find the worker file './dummyWorker.ts'"))
+ ).toThrow(new Error("Cannot find the worker file './dummyWorker.ts'"))
})
it('Verify that numberOfWorkers is checked', () => {
undefined,
'./tests/worker-files/thread/testWorker.mjs'
)
- ).toThrowError(
+ ).toThrow(
new Error(
'Cannot instantiate a pool without specifying the number of workers'
)
expect(
() =>
new FixedClusterPool(-1, './tests/worker-files/cluster/testWorker.js')
- ).toThrowError(
+ ).toThrow(
new RangeError(
'Cannot instantiate a pool with a negative number of workers'
)
expect(
() =>
new FixedThreadPool(0.25, './tests/worker-files/thread/testWorker.mjs')
- ).toThrowError(
+ ).toThrow(
new TypeError(
'Cannot instantiate a pool with a non safe integer number of workers'
)
)
})
+ it('Verify that pool arguments number and pool type are checked', () => {
+ expect(
+ () =>
+ new FixedThreadPool(
+ numberOfWorkers,
+ './tests/worker-files/thread/testWorker.mjs',
+ undefined,
+ numberOfWorkers * 2
+ )
+ ).toThrow(
+ new Error(
+ 'Cannot instantiate a fixed pool with a maximum number of workers specified at initialization'
+ )
+ )
+ })
+
it('Verify that dynamic pool sizing is checked', () => {
expect(
() =>
undefined,
'./tests/worker-files/cluster/testWorker.js'
)
- ).toThrowError(
+ ).toThrow(
new TypeError(
'Cannot instantiate a dynamic pool without specifying the maximum pool size'
)
1,
'./tests/worker-files/thread/testWorker.mjs'
)
- ).toThrowError(
+ ).toThrow(
new TypeError(
'Cannot instantiate a pool with a non safe integer number of workers'
)
0.5,
'./tests/worker-files/cluster/testWorker.js'
)
- ).toThrowError(
+ ).toThrow(
new TypeError(
'Cannot instantiate a dynamic pool with a non safe integer maximum pool size'
)
1,
'./tests/worker-files/thread/testWorker.mjs'
)
- ).toThrowError(
+ ).toThrow(
new RangeError(
'Cannot instantiate a dynamic pool with a maximum pool size inferior to the minimum pool size'
)
0,
'./tests/worker-files/thread/testWorker.mjs'
)
- ).toThrowError(
+ ).toThrow(
new RangeError(
'Cannot instantiate a dynamic pool with a maximum pool size equal to zero'
)
1,
'./tests/worker-files/cluster/testWorker.js'
)
- ).toThrowError(
+ ).toThrow(
new RangeError(
'Cannot instantiate a dynamic pool with a minimum pool size equal to the maximum pool size. Use a fixed pool instead'
)
enableEvents: true,
restartWorkerOnError: true,
enableTasksQueue: false,
- workerChoiceStrategy: WorkerChoiceStrategies.ROUND_ROBIN,
- workerChoiceStrategyOptions: {
- retries: 6,
- runTime: { median: false },
- waitTime: { median: false },
- elu: { median: false }
- }
+ workerChoiceStrategy: WorkerChoiceStrategies.ROUND_ROBIN
})
expect(pool.workerChoiceStrategyContext.opts).toStrictEqual({
- retries: 6,
+ retries:
+ pool.info.maxSize +
+ Object.keys(pool.workerChoiceStrategyContext.opts.weights).length,
runTime: { median: false },
waitTime: { median: false },
- elu: { median: false }
+ elu: { median: false },
+ weights: expect.objectContaining({
+ 0: expect.any(Number),
+ [pool.info.maxSize - 1]: expect.any(Number)
+ })
})
for (const [, workerChoiceStrategy] of pool.workerChoiceStrategyContext
.workerChoiceStrategies) {
- expect(workerChoiceStrategy.opts).toStrictEqual({
- retries: 6,
- runTime: { median: false },
- waitTime: { median: false },
- elu: { median: false }
- })
+ expect(workerChoiceStrategy.opts).toStrictEqual(
+ expect.objectContaining({
+ retries:
+ pool.info.maxSize +
+ Object.keys(workerChoiceStrategy.opts.weights).length,
+ runTime: { median: false },
+ waitTime: { median: false },
+ elu: { median: false }
+ // weights: expect.objectContaining({
+ // 0: expect.any(Number),
+ // [pool.info.maxSize - 1]: expect.any(Number)
+ // })
+ })
+ )
}
await pool.destroy()
const testHandler = () => console.info('test handler executed')
concurrency: 2,
size: Math.pow(numberOfWorkers, 2),
taskStealing: true,
- tasksStealingOnBackPressure: true
+ tasksStealingOnBackPressure: true,
+ tasksFinishedTimeout: 2000
},
workerChoiceStrategy: WorkerChoiceStrategies.LEAST_USED,
workerChoiceStrategyOptions: {
- retries: 6,
runTime: { median: true },
- waitTime: { median: false },
- elu: { median: false },
weights: { 0: 300, 1: 200 }
},
onlineHandler: testHandler,
exitHandler: testHandler
})
expect(pool.workerChoiceStrategyContext.opts).toStrictEqual({
- retries: 6,
+ retries:
+ pool.info.maxSize +
+ Object.keys(pool.opts.workerChoiceStrategyOptions.weights).length,
runTime: { median: true },
waitTime: { median: false },
elu: { median: false },
for (const [, workerChoiceStrategy] of pool.workerChoiceStrategyContext
.workerChoiceStrategies) {
expect(workerChoiceStrategy.opts).toStrictEqual({
- retries: 6,
+ retries:
+ pool.info.maxSize +
+ Object.keys(pool.opts.workerChoiceStrategyOptions.weights).length,
runTime: { median: true },
waitTime: { median: false },
elu: { median: false },
await pool.destroy()
})
- it('Verify that pool options are validated', async () => {
+ it('Verify that pool options are validated', () => {
expect(
() =>
new FixedThreadPool(
workerChoiceStrategy: 'invalidStrategy'
}
)
- ).toThrowError(
- new Error("Invalid worker choice strategy 'invalidStrategy'")
- )
- expect(
- () =>
- new FixedThreadPool(
- numberOfWorkers,
- './tests/worker-files/thread/testWorker.mjs',
- {
- workerChoiceStrategyOptions: {
- retries: 'invalidChoiceRetries'
- }
- }
- )
- ).toThrowError(
- new TypeError(
- 'Invalid worker choice strategy options: retries must be an integer'
- )
- )
- expect(
- () =>
- new FixedThreadPool(
- numberOfWorkers,
- './tests/worker-files/thread/testWorker.mjs',
- {
- workerChoiceStrategyOptions: {
- retries: -1
- }
- }
- )
- ).toThrowError(
- new RangeError(
- "Invalid worker choice strategy options: retries '-1' must be greater or equal than zero"
- )
- )
+ ).toThrow(new Error("Invalid worker choice strategy 'invalidStrategy'"))
expect(
() =>
new FixedThreadPool(
workerChoiceStrategyOptions: { weights: {} }
}
)
- ).toThrowError(
+ ).toThrow(
new Error(
'Invalid worker choice strategy options: must have a weight for each worker node'
)
workerChoiceStrategyOptions: { measurement: 'invalidMeasurement' }
}
)
- ).toThrowError(
+ ).toThrow(
new Error(
"Invalid worker choice strategy options: invalid measurement 'invalidMeasurement'"
)
tasksQueueOptions: 'invalidTasksQueueOptions'
}
)
- ).toThrowError(
+ ).toThrow(
new TypeError('Invalid tasks queue options: must be a plain object')
)
expect(
tasksQueueOptions: { concurrency: 0 }
}
)
- ).toThrowError(
+ ).toThrow(
new RangeError(
'Invalid worker node tasks concurrency: 0 is a negative integer or zero'
)
tasksQueueOptions: { concurrency: -1 }
}
)
- ).toThrowError(
+ ).toThrow(
new RangeError(
'Invalid worker node tasks concurrency: -1 is a negative integer or zero'
)
tasksQueueOptions: { concurrency: 0.2 }
}
)
- ).toThrowError(
+ ).toThrow(
new TypeError('Invalid worker node tasks concurrency: must be an integer')
)
expect(
tasksQueueOptions: { size: 0 }
}
)
- ).toThrowError(
+ ).toThrow(
new RangeError(
'Invalid worker node tasks queue size: 0 is a negative integer or zero'
)
tasksQueueOptions: { size: -1 }
}
)
- ).toThrowError(
+ ).toThrow(
new RangeError(
'Invalid worker node tasks queue size: -1 is a negative integer or zero'
)
tasksQueueOptions: { size: 0.2 }
}
)
- ).toThrowError(
+ ).toThrow(
new TypeError('Invalid worker node tasks queue size: must be an integer')
)
})
'./tests/worker-files/thread/testWorker.mjs',
{ workerChoiceStrategy: WorkerChoiceStrategies.FAIR_SHARE }
)
- expect(pool.opts.workerChoiceStrategyOptions).toStrictEqual({
- retries: 6,
- runTime: { median: false },
- waitTime: { median: false },
- elu: { median: false }
- })
+ expect(pool.opts.workerChoiceStrategyOptions).toBeUndefined()
expect(pool.workerChoiceStrategyContext.opts).toStrictEqual({
- retries: 6,
+ retries:
+ pool.info.maxSize +
+ Object.keys(pool.workerChoiceStrategyContext.opts.weights).length,
runTime: { median: false },
waitTime: { median: false },
- elu: { median: false }
+ elu: { median: false },
+ weights: expect.objectContaining({
+ 0: expect.any(Number),
+ [pool.info.maxSize - 1]: expect.any(Number)
+ })
})
for (const [, workerChoiceStrategy] of pool.workerChoiceStrategyContext
.workerChoiceStrategies) {
expect(workerChoiceStrategy.opts).toStrictEqual({
- retries: 6,
+ retries:
+ pool.info.maxSize +
+ Object.keys(workerChoiceStrategy.opts.weights).length,
runTime: { median: false },
waitTime: { median: false },
- elu: { median: false }
+ elu: { median: false },
+ weights: expect.objectContaining({
+ 0: expect.any(Number),
+ [pool.info.maxSize - 1]: expect.any(Number)
+ })
})
}
expect(
elu: { median: true }
})
expect(pool.opts.workerChoiceStrategyOptions).toStrictEqual({
- retries: 6,
runTime: { median: true },
- waitTime: { median: false },
elu: { median: true }
})
expect(pool.workerChoiceStrategyContext.opts).toStrictEqual({
- retries: 6,
+ retries:
+ pool.info.maxSize +
+ Object.keys(pool.workerChoiceStrategyContext.opts.weights).length,
runTime: { median: true },
waitTime: { median: false },
- elu: { median: true }
+ elu: { median: true },
+ weights: expect.objectContaining({
+ 0: expect.any(Number),
+ [pool.info.maxSize - 1]: expect.any(Number)
+ })
})
for (const [, workerChoiceStrategy] of pool.workerChoiceStrategyContext
.workerChoiceStrategies) {
expect(workerChoiceStrategy.opts).toStrictEqual({
- retries: 6,
+ retries:
+ pool.info.maxSize +
+ Object.keys(workerChoiceStrategy.opts.weights).length,
runTime: { median: true },
waitTime: { median: false },
- elu: { median: true }
+ elu: { median: true },
+ weights: expect.objectContaining({
+ 0: expect.any(Number),
+ [pool.info.maxSize - 1]: expect.any(Number)
+ })
})
}
expect(
elu: { median: false }
})
expect(pool.opts.workerChoiceStrategyOptions).toStrictEqual({
- retries: 6,
runTime: { median: false },
- waitTime: { median: false },
elu: { median: false }
})
expect(pool.workerChoiceStrategyContext.opts).toStrictEqual({
- retries: 6,
+ retries:
+ pool.info.maxSize +
+ Object.keys(pool.workerChoiceStrategyContext.opts.weights).length,
runTime: { median: false },
waitTime: { median: false },
- elu: { median: false }
+ elu: { median: false },
+ weights: expect.objectContaining({
+ 0: expect.any(Number),
+ [pool.info.maxSize - 1]: expect.any(Number)
+ })
})
for (const [, workerChoiceStrategy] of pool.workerChoiceStrategyContext
.workerChoiceStrategies) {
expect(workerChoiceStrategy.opts).toStrictEqual({
- retries: 6,
+ retries:
+ pool.info.maxSize +
+ Object.keys(workerChoiceStrategy.opts.weights).length,
runTime: { median: false },
waitTime: { median: false },
- elu: { median: false }
+ elu: { median: false },
+ weights: expect.objectContaining({
+ 0: expect.any(Number),
+ [pool.info.maxSize - 1]: expect.any(Number)
+ })
})
}
expect(
})
expect(() =>
pool.setWorkerChoiceStrategyOptions('invalidWorkerChoiceStrategyOptions')
- ).toThrowError(
+ ).toThrow(
new TypeError(
'Invalid worker choice strategy options: must be a plain object'
)
)
- expect(() =>
- pool.setWorkerChoiceStrategyOptions({
- retries: 'invalidChoiceRetries'
- })
- ).toThrowError(
- new TypeError(
- 'Invalid worker choice strategy options: retries must be an integer'
- )
- )
- expect(() =>
- pool.setWorkerChoiceStrategyOptions({ retries: -1 })
- ).toThrowError(
- new RangeError(
- "Invalid worker choice strategy options: retries '-1' must be greater or equal than zero"
- )
- )
- expect(() =>
- pool.setWorkerChoiceStrategyOptions({ weights: {} })
- ).toThrowError(
+ expect(() => pool.setWorkerChoiceStrategyOptions({ weights: {} })).toThrow(
new Error(
'Invalid worker choice strategy options: must have a weight for each worker node'
)
)
expect(() =>
pool.setWorkerChoiceStrategyOptions({ measurement: 'invalidMeasurement' })
- ).toThrowError(
+ ).toThrow(
new Error(
"Invalid worker choice strategy options: invalid measurement 'invalidMeasurement'"
)
)
expect(pool.opts.enableTasksQueue).toBe(false)
expect(pool.opts.tasksQueueOptions).toBeUndefined()
- for (const workerNode of pool.workerNodes) {
- expect(workerNode.onEmptyQueue).toBeUndefined()
- expect(workerNode.onBackPressure).toBeUndefined()
- }
pool.enableTasksQueue(true)
expect(pool.opts.enableTasksQueue).toBe(true)
expect(pool.opts.tasksQueueOptions).toStrictEqual({
concurrency: 1,
size: Math.pow(numberOfWorkers, 2),
taskStealing: true,
- tasksStealingOnBackPressure: true
+ tasksStealingOnBackPressure: true,
+ tasksFinishedTimeout: 2000
})
- for (const workerNode of pool.workerNodes) {
- expect(workerNode.onEmptyQueue).toBeInstanceOf(Function)
- expect(workerNode.onBackPressure).toBeInstanceOf(Function)
- }
pool.enableTasksQueue(true, { concurrency: 2 })
expect(pool.opts.enableTasksQueue).toBe(true)
expect(pool.opts.tasksQueueOptions).toStrictEqual({
concurrency: 2,
size: Math.pow(numberOfWorkers, 2),
taskStealing: true,
- tasksStealingOnBackPressure: true
+ tasksStealingOnBackPressure: true,
+ tasksFinishedTimeout: 2000
})
- for (const workerNode of pool.workerNodes) {
- expect(workerNode.onEmptyQueue).toBeInstanceOf(Function)
- expect(workerNode.onBackPressure).toBeInstanceOf(Function)
- }
pool.enableTasksQueue(false)
expect(pool.opts.enableTasksQueue).toBe(false)
expect(pool.opts.tasksQueueOptions).toBeUndefined()
- for (const workerNode of pool.workerNodes) {
- expect(workerNode.onEmptyQueue).toBeUndefined()
- expect(workerNode.onBackPressure).toBeUndefined()
- }
await pool.destroy()
})
concurrency: 1,
size: Math.pow(numberOfWorkers, 2),
taskStealing: true,
- tasksStealingOnBackPressure: true
+ tasksStealingOnBackPressure: true,
+ tasksFinishedTimeout: 2000
})
for (const workerNode of pool.workerNodes) {
expect(workerNode.tasksQueueBackPressureSize).toBe(
pool.opts.tasksQueueOptions.size
)
- expect(workerNode.onEmptyQueue).toBeInstanceOf(Function)
- expect(workerNode.onBackPressure).toBeInstanceOf(Function)
}
pool.setTasksQueueOptions({
concurrency: 2,
size: 2,
taskStealing: false,
- tasksStealingOnBackPressure: false
+ tasksStealingOnBackPressure: false,
+ tasksFinishedTimeout: 3000
})
expect(pool.opts.tasksQueueOptions).toStrictEqual({
concurrency: 2,
size: 2,
taskStealing: false,
- tasksStealingOnBackPressure: false
+ tasksStealingOnBackPressure: false,
+ tasksFinishedTimeout: 3000
})
for (const workerNode of pool.workerNodes) {
expect(workerNode.tasksQueueBackPressureSize).toBe(
pool.opts.tasksQueueOptions.size
)
- expect(workerNode.onEmptyQueue).toBeUndefined()
- expect(workerNode.onBackPressure).toBeUndefined()
}
pool.setTasksQueueOptions({
concurrency: 1,
concurrency: 1,
size: Math.pow(numberOfWorkers, 2),
taskStealing: true,
- tasksStealingOnBackPressure: true
+ tasksStealingOnBackPressure: true,
+ tasksFinishedTimeout: 2000
})
for (const workerNode of pool.workerNodes) {
expect(workerNode.tasksQueueBackPressureSize).toBe(
pool.opts.tasksQueueOptions.size
)
- expect(workerNode.onEmptyQueue).toBeInstanceOf(Function)
- expect(workerNode.onBackPressure).toBeInstanceOf(Function)
}
- expect(() =>
- pool.setTasksQueueOptions('invalidTasksQueueOptions')
- ).toThrowError(
+ expect(() => pool.setTasksQueueOptions('invalidTasksQueueOptions')).toThrow(
new TypeError('Invalid tasks queue options: must be a plain object')
)
- expect(() => pool.setTasksQueueOptions({ concurrency: 0 })).toThrowError(
+ expect(() => pool.setTasksQueueOptions({ concurrency: 0 })).toThrow(
new RangeError(
'Invalid worker node tasks concurrency: 0 is a negative integer or zero'
)
)
- expect(() => pool.setTasksQueueOptions({ concurrency: -1 })).toThrowError(
+ expect(() => pool.setTasksQueueOptions({ concurrency: -1 })).toThrow(
new RangeError(
'Invalid worker node tasks concurrency: -1 is a negative integer or zero'
)
)
- expect(() => pool.setTasksQueueOptions({ concurrency: 0.2 })).toThrowError(
+ expect(() => pool.setTasksQueueOptions({ concurrency: 0.2 })).toThrow(
new TypeError('Invalid worker node tasks concurrency: must be an integer')
)
- expect(() => pool.setTasksQueueOptions({ size: 0 })).toThrowError(
+ expect(() => pool.setTasksQueueOptions({ size: 0 })).toThrow(
new RangeError(
'Invalid worker node tasks queue size: 0 is a negative integer or zero'
)
)
- expect(() => pool.setTasksQueueOptions({ size: -1 })).toThrowError(
+ expect(() => pool.setTasksQueueOptions({ size: -1 })).toThrow(
new RangeError(
'Invalid worker node tasks queue size: -1 is a negative integer or zero'
)
)
- expect(() => pool.setTasksQueueOptions({ size: 0.2 })).toThrowError(
+ expect(() => pool.setTasksQueueOptions({ size: 0.2 })).toThrow(
new TypeError('Invalid worker node tasks queue size: must be an integer')
)
await pool.destroy()
executing: 0,
queued: 0,
maxQueued: 0,
+ sequentiallyStolen: 0,
stolen: 0,
failed: 0
},
await pool.destroy()
})
+ it('Verify that pool statuses are checked at start or destroy', async () => {
+ const pool = new FixedThreadPool(
+ numberOfWorkers,
+ './tests/worker-files/thread/testWorker.mjs'
+ )
+ expect(pool.info.started).toBe(true)
+ expect(pool.info.ready).toBe(true)
+ expect(() => pool.start()).toThrow(
+ new Error('Cannot start an already started pool')
+ )
+ await pool.destroy()
+ expect(pool.info.started).toBe(false)
+ expect(pool.info.ready).toBe(false)
+ await expect(pool.destroy()).rejects.toThrow(
+ new Error('Cannot destroy an already destroyed pool')
+ )
+ })
+
it('Verify that pool can be started after initialization', async () => {
const pool = new FixedClusterPool(
numberOfWorkers,
)
expect(pool.info.started).toBe(false)
expect(pool.info.ready).toBe(false)
+ expect(pool.readyEventEmitted).toBe(false)
expect(pool.workerNodes).toStrictEqual([])
- await expect(pool.execute()).rejects.toThrowError(
+ await expect(pool.execute()).rejects.toThrow(
new Error('Cannot execute a task on not started pool')
)
pool.start()
expect(pool.info.started).toBe(true)
expect(pool.info.ready).toBe(true)
+ await waitPoolEvents(pool, PoolEvents.ready, 1)
+ expect(pool.readyEventEmitted).toBe(true)
expect(pool.workerNodes.length).toBe(numberOfWorkers)
for (const workerNode of pool.workerNodes) {
expect(workerNode).toBeInstanceOf(WorkerNode)
numberOfWorkers,
'./tests/worker-files/cluster/testWorker.js'
)
- await expect(pool.execute(undefined, 0)).rejects.toThrowError(
+ await expect(pool.execute(undefined, 0)).rejects.toThrow(
new TypeError('name argument must be a string')
)
- await expect(pool.execute(undefined, '')).rejects.toThrowError(
+ await expect(pool.execute(undefined, '')).rejects.toThrow(
new TypeError('name argument must not be an empty string')
)
- await expect(pool.execute(undefined, undefined, {})).rejects.toThrowError(
+ await expect(pool.execute(undefined, undefined, {})).rejects.toThrow(
new TypeError('transferList argument must be an array')
)
await expect(pool.execute(undefined, 'unknown')).rejects.toBe(
"Task function 'unknown' not found"
)
await pool.destroy()
- await expect(pool.execute()).rejects.toThrowError(
+ await expect(pool.execute()).rejects.toThrow(
new Error('Cannot execute a task on not started pool')
)
})
executing: maxMultiplier,
queued: 0,
maxQueued: 0,
+ sequentiallyStolen: 0,
stolen: 0,
failed: 0
},
executing: 0,
queued: 0,
maxQueued: 0,
+ sequentiallyStolen: 0,
stolen: 0,
failed: 0
},
executing: 0,
queued: 0,
maxQueued: 0,
+ sequentiallyStolen: 0,
stolen: 0,
failed: 0
},
executing: 0,
queued: 0,
maxQueued: 0,
+ sequentiallyStolen: 0,
stolen: 0,
failed: 0
},
stolenTasks: expect.any(Number),
failedTasks: expect.any(Number)
})
- expect(pool.hasBackPressure.called).toBe(true)
+ expect(pool.hasBackPressure.callCount).toBe(5)
+ await pool.destroy()
+ })
+
+ it('Verify that destroy() waits for queued tasks to finish', async () => {
+ const tasksFinishedTimeout = 2500
+ const pool = new FixedThreadPool(
+ numberOfWorkers,
+ './tests/worker-files/thread/asyncWorker.mjs',
+ {
+ enableTasksQueue: true,
+ tasksQueueOptions: { tasksFinishedTimeout }
+ }
+ )
+ const maxMultiplier = 4
+ let tasksFinished = 0
+ for (const workerNode of pool.workerNodes) {
+ workerNode.on('taskFinished', () => {
+ ++tasksFinished
+ })
+ }
+ for (let i = 0; i < numberOfWorkers * maxMultiplier; i++) {
+ pool.execute()
+ }
+ expect(pool.info.queuedTasks).toBeGreaterThan(0)
+ const startTime = performance.now()
+ await pool.destroy()
+ const elapsedTime = performance.now() - startTime
+ expect(tasksFinished).toBe(numberOfWorkers * maxMultiplier)
+ expect(elapsedTime).toBeGreaterThanOrEqual(2000)
+ expect(elapsedTime).toBeLessThanOrEqual(tasksFinishedTimeout + 100)
+ })
+
+ it('Verify that destroy() waits until the tasks finished timeout is reached', async () => {
+ const tasksFinishedTimeout = 1000
+ const pool = new FixedThreadPool(
+ numberOfWorkers,
+ './tests/worker-files/thread/asyncWorker.mjs',
+ {
+ enableTasksQueue: true,
+ tasksQueueOptions: { tasksFinishedTimeout }
+ }
+ )
+ const maxMultiplier = 4
+ let tasksFinished = 0
+ for (const workerNode of pool.workerNodes) {
+ workerNode.on('taskFinished', () => {
+ ++tasksFinished
+ })
+ }
+ for (let i = 0; i < numberOfWorkers * maxMultiplier; i++) {
+ pool.execute()
+ }
+ expect(pool.info.queuedTasks).toBeGreaterThan(0)
+ const startTime = performance.now()
+ await pool.destroy()
+ const elapsedTime = performance.now() - startTime
+ expect(tasksFinished).toBe(0)
+ expect(elapsedTime).toBeLessThanOrEqual(tasksFinishedTimeout + 600)
+ })
+
+ it('Verify that pool asynchronous resource track tasks execution', async () => {
+ let taskAsyncId
+ let initCalls = 0
+ let beforeCalls = 0
+ let afterCalls = 0
+ let resolveCalls = 0
+ const hook = createHook({
+ init (asyncId, type) {
+ if (type === 'poolifier:task') {
+ initCalls++
+ taskAsyncId = asyncId
+ }
+ },
+ before (asyncId) {
+ if (asyncId === taskAsyncId) beforeCalls++
+ },
+ after (asyncId) {
+ if (asyncId === taskAsyncId) afterCalls++
+ },
+ promiseResolve () {
+ if (executionAsyncId() === taskAsyncId) resolveCalls++
+ }
+ })
+ const pool = new FixedThreadPool(
+ numberOfWorkers,
+ './tests/worker-files/thread/testWorker.mjs'
+ )
+ hook.enable()
+ await pool.execute()
+ hook.disable()
+ expect(initCalls).toBe(1)
+ expect(beforeCalls).toBe(1)
+ expect(afterCalls).toBe(1)
+ expect(resolveCalls).toBe(1)
await pool.destroy()
})
await waitPoolEvents(dynamicThreadPool, PoolEvents.ready, 1)
await expect(
dynamicThreadPool.addTaskFunction(0, () => {})
- ).rejects.toThrowError(new TypeError('name argument must be a string'))
+ ).rejects.toThrow(new TypeError('name argument must be a string'))
await expect(
dynamicThreadPool.addTaskFunction('', () => {})
- ).rejects.toThrowError(
+ ).rejects.toThrow(
new TypeError('name argument must not be an empty string')
)
- await expect(
- dynamicThreadPool.addTaskFunction('test', 0)
- ).rejects.toThrowError(new TypeError('fn argument must be a function'))
- await expect(
- dynamicThreadPool.addTaskFunction('test', '')
- ).rejects.toThrowError(new TypeError('fn argument must be a function'))
+ await expect(dynamicThreadPool.addTaskFunction('test', 0)).rejects.toThrow(
+ new TypeError('fn argument must be a function')
+ )
+ await expect(dynamicThreadPool.addTaskFunction('test', '')).rejects.toThrow(
+ new TypeError('fn argument must be a function')
+ )
expect(dynamicThreadPool.listTaskFunctionNames()).toStrictEqual([
DEFAULT_TASK_NAME,
'test'
executed: expect.any(Number),
executing: 0,
queued: 0,
+ sequentiallyStolen: 0,
stolen: 0,
failed: 0
},
DEFAULT_TASK_NAME,
'test'
])
- await expect(
- dynamicThreadPool.removeTaskFunction('test')
- ).rejects.toThrowError(
+ await expect(dynamicThreadPool.removeTaskFunction('test')).rejects.toThrow(
new Error('Cannot remove a task function not handled on the pool side')
)
const echoTaskFunction = data => {
'./tests/worker-files/thread/testMultipleTaskFunctionsWorker.mjs'
)
await waitPoolEvents(dynamicThreadPool, PoolEvents.ready, 1)
- await expect(
- dynamicThreadPool.setDefaultTaskFunction(0)
- ).rejects.toThrowError(
+ const workerId = dynamicThreadPool.workerNodes[0].info.id
+ await expect(dynamicThreadPool.setDefaultTaskFunction(0)).rejects.toThrow(
new Error(
- "Task function operation 'default' failed on worker 31 with error: 'TypeError: name parameter is not a string'"
+ `Task function operation 'default' failed on worker ${workerId} with error: 'TypeError: name parameter is not a string'`
)
)
await expect(
dynamicThreadPool.setDefaultTaskFunction(DEFAULT_TASK_NAME)
- ).rejects.toThrowError(
+ ).rejects.toThrow(
new Error(
- "Task function operation 'default' failed on worker 31 with error: 'Error: Cannot set the default task function reserved name as the default task function'"
+ `Task function operation 'default' failed on worker ${workerId} with error: 'Error: Cannot set the default task function reserved name as the default task function'`
)
)
await expect(
dynamicThreadPool.setDefaultTaskFunction('unknown')
- ).rejects.toThrowError(
+ ).rejects.toThrow(
new Error(
- "Task function operation 'default' failed on worker 31 with error: 'Error: Cannot set the default task function to a non-existing task function'"
+ `Task function operation 'default' failed on worker ${workerId} with error: 'Error: Cannot set the default task function to a non-existing task function'`
)
)
expect(dynamicThreadPool.listTaskFunctionNames()).toStrictEqual([
'jsonIntegerSerialization',
'factorial'
])
+ await dynamicThreadPool.destroy()
})
it('Verify that multiple task functions worker is working', async () => {
executing: 0,
failed: 0,
queued: 0,
+ sequentiallyStolen: 0,
stolen: 0
},
runTime: {
await expect(
pool.sendKillMessageToWorker(workerNodeKey)
).resolves.toBeUndefined()
+ await expect(
+ pool.sendKillMessageToWorker(numberOfWorkers)
+ ).rejects.toStrictEqual(
+ new Error(`Invalid worker node key '${numberOfWorkers}'`)
+ )
await pool.destroy()
})