1 // eslint-disable-next-line n/no-unsupported-features/node-builtins
2 import { createHook, executionAsyncId } from 'node:async_hooks'
3 import { EventEmitterAsyncResource } from 'node:events'
4 import { readFileSync } from 'node:fs'
5 import { dirname, join } from 'node:path'
6 import { fileURLToPath } from 'node:url'
8 import { expect } from 'expect'
9 import { restore, stub } from 'sinon'
11 import { CircularArray } from '../../lib/circular-array.cjs'
19 WorkerChoiceStrategies,
21 } from '../../lib/index.cjs'
22 import { WorkerNode } from '../../lib/pools/worker-node.cjs'
23 import { PriorityQueue } from '../../lib/priority-queue.cjs'
24 import { DEFAULT_TASK_NAME } from '../../lib/utils.cjs'
25 import { waitPoolEvents } from '../test-utils.cjs'
27 describe('Abstract pool test suite', () => {
28 const version = JSON.parse(
30 join(dirname(fileURLToPath(import.meta.url)), '../..', 'package.json'),
34 const numberOfWorkers = 2
35 class StubPoolWithIsMain extends FixedThreadPool {
45 it('Verify that pool can be created and destroyed', async () => {
46 const pool = new FixedThreadPool(
48 './tests/worker-files/thread/testWorker.mjs'
50 expect(pool).toBeInstanceOf(FixedThreadPool)
54 it('Verify that pool cannot be created from a non main thread/process', () => {
57 new StubPoolWithIsMain(
59 './tests/worker-files/thread/testWorker.mjs',
61 errorHandler: e => console.error(e)
66 'Cannot start a pool from a worker with the same type as the pool'
71 it('Verify that pool statuses properties are set', async () => {
72 const pool = new FixedThreadPool(
74 './tests/worker-files/thread/testWorker.mjs'
76 expect(pool.started).toBe(true)
77 expect(pool.starting).toBe(false)
78 expect(pool.destroying).toBe(false)
82 it('Verify that filePath is checked', () => {
83 expect(() => new FixedThreadPool(numberOfWorkers)).toThrow(
84 new TypeError('The worker file path must be specified')
86 expect(() => new FixedThreadPool(numberOfWorkers, 0)).toThrow(
87 new TypeError('The worker file path must be a string')
90 () => new FixedThreadPool(numberOfWorkers, './dummyWorker.ts')
91 ).toThrow(new Error("Cannot find the worker file './dummyWorker.ts'"))
94 it('Verify that numberOfWorkers is checked', () => {
99 './tests/worker-files/thread/testWorker.mjs'
103 'Cannot instantiate a pool without specifying the number of workers'
108 it('Verify that a negative number of workers is checked', () => {
111 new FixedClusterPool(-1, './tests/worker-files/cluster/testWorker.cjs')
114 'Cannot instantiate a pool with a negative number of workers'
119 it('Verify that a non integer number of workers is checked', () => {
122 new FixedThreadPool(0.25, './tests/worker-files/thread/testWorker.mjs')
125 'Cannot instantiate a pool with a non safe integer number of workers'
130 it('Verify that pool arguments number and pool type are checked', () => {
135 './tests/worker-files/thread/testWorker.mjs',
141 'Cannot instantiate a fixed pool with a maximum number of workers specified at initialization'
146 it('Verify that dynamic pool sizing is checked', () => {
149 new DynamicClusterPool(
152 './tests/worker-files/cluster/testWorker.cjs'
156 'Cannot instantiate a dynamic pool without specifying the maximum pool size'
161 new DynamicThreadPool(
164 './tests/worker-files/thread/testWorker.mjs'
168 'Cannot instantiate a pool with a non safe integer number of workers'
173 new DynamicClusterPool(
176 './tests/worker-files/cluster/testWorker.cjs'
180 'Cannot instantiate a dynamic pool with a non safe integer maximum pool size'
185 new DynamicThreadPool(
188 './tests/worker-files/thread/testWorker.mjs'
192 'Cannot instantiate a dynamic pool with a maximum pool size inferior to the minimum pool size'
197 new DynamicThreadPool(
200 './tests/worker-files/thread/testWorker.mjs'
204 'Cannot instantiate a dynamic pool with a maximum pool size equal to zero'
209 new DynamicClusterPool(
212 './tests/worker-files/cluster/testWorker.cjs'
216 'Cannot instantiate a dynamic pool with a minimum pool size equal to the maximum pool size. Use a fixed pool instead'
221 it('Verify that pool options are checked', async () => {
222 let pool = new FixedThreadPool(
224 './tests/worker-files/thread/testWorker.mjs'
226 expect(pool.emitter).toBeInstanceOf(EventEmitterAsyncResource)
227 expect(pool.emitter.eventNames()).toStrictEqual([])
228 expect(pool.opts).toStrictEqual({
231 restartWorkerOnError: true,
232 enableTasksQueue: false,
233 workerChoiceStrategy: WorkerChoiceStrategies.ROUND_ROBIN
235 for (const [, workerChoiceStrategy] of pool.workerChoiceStrategiesContext
236 .workerChoiceStrategies) {
237 expect(workerChoiceStrategy.opts).toStrictEqual({
238 runTime: { median: false },
239 waitTime: { median: false },
240 elu: { median: false },
241 weights: expect.objectContaining({
242 0: expect.any(Number),
243 [pool.info.maxSize - 1]: expect.any(Number)
248 const testHandler = () => console.info('test handler executed')
249 pool = new FixedThreadPool(
251 './tests/worker-files/thread/testWorker.mjs',
253 workerChoiceStrategy: WorkerChoiceStrategies.LEAST_USED,
254 workerChoiceStrategyOptions: {
255 runTime: { median: true },
256 weights: { 0: 300, 1: 200 }
259 restartWorkerOnError: false,
260 enableTasksQueue: true,
261 tasksQueueOptions: { concurrency: 2 },
262 messageHandler: testHandler,
263 errorHandler: testHandler,
264 onlineHandler: testHandler,
265 exitHandler: testHandler
268 expect(pool.emitter).toBeUndefined()
269 expect(pool.opts).toStrictEqual({
272 restartWorkerOnError: false,
273 enableTasksQueue: true,
276 size: Math.pow(numberOfWorkers, 2),
278 tasksStealingOnBackPressure: false,
279 tasksFinishedTimeout: 2000
281 workerChoiceStrategy: WorkerChoiceStrategies.LEAST_USED,
282 workerChoiceStrategyOptions: {
283 runTime: { median: true },
284 weights: { 0: 300, 1: 200 }
286 onlineHandler: testHandler,
287 messageHandler: testHandler,
288 errorHandler: testHandler,
289 exitHandler: testHandler
291 for (const [, workerChoiceStrategy] of pool.workerChoiceStrategiesContext
292 .workerChoiceStrategies) {
293 expect(workerChoiceStrategy.opts).toStrictEqual({
294 runTime: { median: true },
295 waitTime: { median: false },
296 elu: { median: false },
297 weights: { 0: 300, 1: 200 }
303 it('Verify that pool options are validated', () => {
308 './tests/worker-files/thread/testWorker.mjs',
310 workerChoiceStrategy: 'invalidStrategy'
313 ).toThrow(new Error("Invalid worker choice strategy 'invalidStrategy'"))
318 './tests/worker-files/thread/testWorker.mjs',
320 workerChoiceStrategyOptions: { weights: {} }
325 'Invalid worker choice strategy options: must have a weight for each worker node'
332 './tests/worker-files/thread/testWorker.mjs',
334 workerChoiceStrategyOptions: { measurement: 'invalidMeasurement' }
339 "Invalid worker choice strategy options: invalid measurement 'invalidMeasurement'"
346 './tests/worker-files/thread/testWorker.mjs',
348 enableTasksQueue: true,
349 tasksQueueOptions: 'invalidTasksQueueOptions'
353 new TypeError('Invalid tasks queue options: must be a plain object')
359 './tests/worker-files/thread/testWorker.mjs',
361 enableTasksQueue: true,
362 tasksQueueOptions: { concurrency: 0 }
367 'Invalid worker node tasks concurrency: 0 is a negative integer or zero'
374 './tests/worker-files/thread/testWorker.mjs',
376 enableTasksQueue: true,
377 tasksQueueOptions: { concurrency: -1 }
382 'Invalid worker node tasks concurrency: -1 is a negative integer or zero'
389 './tests/worker-files/thread/testWorker.mjs',
391 enableTasksQueue: true,
392 tasksQueueOptions: { concurrency: 0.2 }
396 new TypeError('Invalid worker node tasks concurrency: must be an integer')
402 './tests/worker-files/thread/testWorker.mjs',
404 enableTasksQueue: true,
405 tasksQueueOptions: { size: 0 }
410 'Invalid worker node tasks queue size: 0 is a negative integer or zero'
417 './tests/worker-files/thread/testWorker.mjs',
419 enableTasksQueue: true,
420 tasksQueueOptions: { size: -1 }
425 'Invalid worker node tasks queue size: -1 is a negative integer or zero'
432 './tests/worker-files/thread/testWorker.mjs',
434 enableTasksQueue: true,
435 tasksQueueOptions: { size: 0.2 }
439 new TypeError('Invalid worker node tasks queue size: must be an integer')
443 it('Verify that pool worker choice strategy options can be set', async () => {
444 const pool = new FixedThreadPool(
446 './tests/worker-files/thread/testWorker.mjs',
447 { workerChoiceStrategy: WorkerChoiceStrategies.FAIR_SHARE }
449 expect(pool.opts.workerChoiceStrategyOptions).toBeUndefined()
450 for (const [, workerChoiceStrategy] of pool.workerChoiceStrategiesContext
451 .workerChoiceStrategies) {
452 expect(workerChoiceStrategy.opts).toStrictEqual({
453 runTime: { median: false },
454 waitTime: { median: false },
455 elu: { median: false },
456 weights: expect.objectContaining({
457 0: expect.any(Number),
458 [pool.info.maxSize - 1]: expect.any(Number)
463 pool.workerChoiceStrategiesContext.getTaskStatisticsRequirements()
481 pool.setWorkerChoiceStrategyOptions({
482 runTime: { median: true },
483 elu: { median: true }
485 expect(pool.opts.workerChoiceStrategyOptions).toStrictEqual({
486 runTime: { median: true },
487 elu: { median: true }
489 for (const [, workerChoiceStrategy] of pool.workerChoiceStrategiesContext
490 .workerChoiceStrategies) {
491 expect(workerChoiceStrategy.opts).toStrictEqual({
492 runTime: { median: true },
493 waitTime: { median: false },
494 elu: { median: true },
495 weights: expect.objectContaining({
496 0: expect.any(Number),
497 [pool.info.maxSize - 1]: expect.any(Number)
502 pool.workerChoiceStrategiesContext.getTaskStatisticsRequirements()
520 pool.setWorkerChoiceStrategyOptions({
521 runTime: { median: false },
522 elu: { median: false }
524 expect(pool.opts.workerChoiceStrategyOptions).toStrictEqual({
525 runTime: { median: false },
526 elu: { median: false }
528 for (const [, workerChoiceStrategy] of pool.workerChoiceStrategiesContext
529 .workerChoiceStrategies) {
530 expect(workerChoiceStrategy.opts).toStrictEqual({
531 runTime: { median: false },
532 waitTime: { median: false },
533 elu: { median: false },
534 weights: expect.objectContaining({
535 0: expect.any(Number),
536 [pool.info.maxSize - 1]: expect.any(Number)
541 pool.workerChoiceStrategiesContext.getTaskStatisticsRequirements()
560 pool.setWorkerChoiceStrategyOptions('invalidWorkerChoiceStrategyOptions')
563 'Invalid worker choice strategy options: must be a plain object'
566 expect(() => pool.setWorkerChoiceStrategyOptions({ weights: {} })).toThrow(
568 'Invalid worker choice strategy options: must have a weight for each worker node'
572 pool.setWorkerChoiceStrategyOptions({ measurement: 'invalidMeasurement' })
575 "Invalid worker choice strategy options: invalid measurement 'invalidMeasurement'"
581 it('Verify that pool tasks queue can be enabled/disabled', async () => {
582 const pool = new FixedThreadPool(
584 './tests/worker-files/thread/testWorker.mjs'
586 expect(pool.opts.enableTasksQueue).toBe(false)
587 expect(pool.opts.tasksQueueOptions).toBeUndefined()
588 pool.enableTasksQueue(true)
589 expect(pool.opts.enableTasksQueue).toBe(true)
590 expect(pool.opts.tasksQueueOptions).toStrictEqual({
592 size: Math.pow(numberOfWorkers, 2),
594 tasksStealingOnBackPressure: false,
595 tasksFinishedTimeout: 2000
597 pool.enableTasksQueue(true, { concurrency: 2 })
598 expect(pool.opts.enableTasksQueue).toBe(true)
599 expect(pool.opts.tasksQueueOptions).toStrictEqual({
601 size: Math.pow(numberOfWorkers, 2),
603 tasksStealingOnBackPressure: false,
604 tasksFinishedTimeout: 2000
606 pool.enableTasksQueue(false)
607 expect(pool.opts.enableTasksQueue).toBe(false)
608 expect(pool.opts.tasksQueueOptions).toBeUndefined()
612 it('Verify that pool tasks queue options can be set', async () => {
613 const pool = new FixedThreadPool(
615 './tests/worker-files/thread/testWorker.mjs',
616 { enableTasksQueue: true }
618 expect(pool.opts.tasksQueueOptions).toStrictEqual({
620 size: Math.pow(numberOfWorkers, 2),
622 tasksStealingOnBackPressure: false,
623 tasksFinishedTimeout: 2000
625 for (const workerNode of pool.workerNodes) {
626 expect(workerNode.tasksQueueBackPressureSize).toBe(
627 pool.opts.tasksQueueOptions.size
630 pool.setTasksQueueOptions({
634 tasksStealingOnBackPressure: false,
635 tasksFinishedTimeout: 3000
637 expect(pool.opts.tasksQueueOptions).toStrictEqual({
641 tasksStealingOnBackPressure: false,
642 tasksFinishedTimeout: 3000
644 for (const workerNode of pool.workerNodes) {
645 expect(workerNode.tasksQueueBackPressureSize).toBe(
646 pool.opts.tasksQueueOptions.size
649 pool.setTasksQueueOptions({
652 tasksStealingOnBackPressure: true
654 expect(pool.opts.tasksQueueOptions).toStrictEqual({
656 size: Math.pow(numberOfWorkers, 2),
658 tasksStealingOnBackPressure: true,
659 tasksFinishedTimeout: 2000
661 for (const workerNode of pool.workerNodes) {
662 expect(workerNode.tasksQueueBackPressureSize).toBe(
663 pool.opts.tasksQueueOptions.size
666 expect(() => pool.setTasksQueueOptions('invalidTasksQueueOptions')).toThrow(
667 new TypeError('Invalid tasks queue options: must be a plain object')
669 expect(() => pool.setTasksQueueOptions({ concurrency: 0 })).toThrow(
671 'Invalid worker node tasks concurrency: 0 is a negative integer or zero'
674 expect(() => pool.setTasksQueueOptions({ concurrency: -1 })).toThrow(
676 'Invalid worker node tasks concurrency: -1 is a negative integer or zero'
679 expect(() => pool.setTasksQueueOptions({ concurrency: 0.2 })).toThrow(
680 new TypeError('Invalid worker node tasks concurrency: must be an integer')
682 expect(() => pool.setTasksQueueOptions({ size: 0 })).toThrow(
684 'Invalid worker node tasks queue size: 0 is a negative integer or zero'
687 expect(() => pool.setTasksQueueOptions({ size: -1 })).toThrow(
689 'Invalid worker node tasks queue size: -1 is a negative integer or zero'
692 expect(() => pool.setTasksQueueOptions({ size: 0.2 })).toThrow(
693 new TypeError('Invalid worker node tasks queue size: must be an integer')
698 it('Verify that pool info is set', async () => {
699 let pool = new FixedThreadPool(
701 './tests/worker-files/thread/testWorker.mjs'
703 expect(pool.info).toStrictEqual({
705 type: PoolTypes.fixed,
706 worker: WorkerTypes.thread,
709 defaultStrategy: WorkerChoiceStrategies.ROUND_ROBIN,
711 minSize: numberOfWorkers,
712 maxSize: numberOfWorkers,
713 workerNodes: numberOfWorkers,
714 idleWorkerNodes: numberOfWorkers,
721 pool = new DynamicClusterPool(
722 Math.floor(numberOfWorkers / 2),
724 './tests/worker-files/cluster/testWorker.cjs'
726 expect(pool.info).toStrictEqual({
728 type: PoolTypes.dynamic,
729 worker: WorkerTypes.cluster,
732 defaultStrategy: WorkerChoiceStrategies.ROUND_ROBIN,
734 minSize: Math.floor(numberOfWorkers / 2),
735 maxSize: numberOfWorkers,
736 workerNodes: Math.floor(numberOfWorkers / 2),
737 idleWorkerNodes: Math.floor(numberOfWorkers / 2),
746 it('Verify that pool worker tasks usage are initialized', async () => {
747 const pool = new FixedClusterPool(
749 './tests/worker-files/cluster/testWorker.cjs'
751 for (const workerNode of pool.workerNodes) {
752 expect(workerNode).toBeInstanceOf(WorkerNode)
753 expect(workerNode.usage).toStrictEqual({
759 sequentiallyStolen: 0,
764 history: new CircularArray()
767 history: new CircularArray()
771 history: new CircularArray()
774 history: new CircularArray()
782 it('Verify that pool worker tasks queue are initialized', async () => {
783 let pool = new FixedClusterPool(
785 './tests/worker-files/cluster/testWorker.cjs'
787 for (const workerNode of pool.workerNodes) {
788 expect(workerNode).toBeInstanceOf(WorkerNode)
789 expect(workerNode.tasksQueue).toBeInstanceOf(PriorityQueue)
790 expect(workerNode.tasksQueue.size).toBe(0)
791 expect(workerNode.tasksQueue.maxSize).toBe(0)
792 expect(workerNode.tasksQueue.k).toBe(numberOfWorkers * 2)
795 pool = new DynamicThreadPool(
796 Math.floor(numberOfWorkers / 2),
798 './tests/worker-files/thread/testWorker.mjs'
800 for (const workerNode of pool.workerNodes) {
801 expect(workerNode).toBeInstanceOf(WorkerNode)
802 expect(workerNode.tasksQueue).toBeInstanceOf(PriorityQueue)
803 expect(workerNode.tasksQueue.size).toBe(0)
804 expect(workerNode.tasksQueue.maxSize).toBe(0)
805 expect(workerNode.tasksQueue.k).toBe(numberOfWorkers * 2)
810 it('Verify that pool worker info are initialized', async () => {
811 let pool = new FixedClusterPool(
813 './tests/worker-files/cluster/testWorker.cjs'
815 for (const workerNode of pool.workerNodes) {
816 expect(workerNode).toBeInstanceOf(WorkerNode)
817 expect(workerNode.info).toStrictEqual({
818 id: expect.any(Number),
819 type: WorkerTypes.cluster,
827 pool = new DynamicThreadPool(
828 Math.floor(numberOfWorkers / 2),
830 './tests/worker-files/thread/testWorker.mjs'
832 for (const workerNode of pool.workerNodes) {
833 expect(workerNode).toBeInstanceOf(WorkerNode)
834 expect(workerNode.info).toStrictEqual({
835 id: expect.any(Number),
836 type: WorkerTypes.thread,
846 it('Verify that pool statuses are checked at start or destroy', async () => {
847 const pool = new FixedThreadPool(
849 './tests/worker-files/thread/testWorker.mjs'
851 expect(pool.info.started).toBe(true)
852 expect(pool.info.ready).toBe(true)
853 expect(() => pool.start()).toThrow(
854 new Error('Cannot start an already started pool')
857 expect(pool.info.started).toBe(false)
858 expect(pool.info.ready).toBe(false)
859 await expect(pool.destroy()).rejects.toThrow(
860 new Error('Cannot destroy an already destroyed pool')
864 it('Verify that pool can be started after initialization', async () => {
865 const pool = new FixedClusterPool(
867 './tests/worker-files/cluster/testWorker.cjs',
872 expect(pool.info.started).toBe(false)
873 expect(pool.info.ready).toBe(false)
874 expect(pool.workerNodes).toStrictEqual([])
875 expect(pool.readyEventEmitted).toBe(false)
876 await expect(pool.execute()).rejects.toThrow(
877 new Error('Cannot execute a task on not started pool')
880 expect(pool.info.started).toBe(true)
881 expect(pool.info.ready).toBe(true)
882 await waitPoolEvents(pool, PoolEvents.ready, 1)
883 expect(pool.readyEventEmitted).toBe(true)
884 expect(pool.workerNodes.length).toBe(numberOfWorkers)
885 for (const workerNode of pool.workerNodes) {
886 expect(workerNode).toBeInstanceOf(WorkerNode)
891 it('Verify that pool execute() arguments are checked', async () => {
892 const pool = new FixedClusterPool(
894 './tests/worker-files/cluster/testWorker.cjs'
896 await expect(pool.execute(undefined, 0)).rejects.toThrow(
897 new TypeError('name argument must be a string')
899 await expect(pool.execute(undefined, '')).rejects.toThrow(
900 new TypeError('name argument must not be an empty string')
902 await expect(pool.execute(undefined, undefined, {})).rejects.toThrow(
903 new TypeError('transferList argument must be an array')
905 await expect(pool.execute(undefined, 'unknown')).rejects.toBe(
906 "Task function 'unknown' not found"
909 await expect(pool.execute()).rejects.toThrow(
910 new Error('Cannot execute a task on not started pool')
914 it('Verify that pool worker tasks usage are computed', async () => {
915 const pool = new FixedClusterPool(
917 './tests/worker-files/cluster/testWorker.cjs'
919 const promises = new Set()
920 const maxMultiplier = 2
921 for (let i = 0; i < numberOfWorkers * maxMultiplier; i++) {
922 promises.add(pool.execute())
924 for (const workerNode of pool.workerNodes) {
925 expect(workerNode.usage).toStrictEqual({
928 executing: maxMultiplier,
931 sequentiallyStolen: 0,
936 history: expect.any(CircularArray)
939 history: expect.any(CircularArray)
943 history: expect.any(CircularArray)
946 history: expect.any(CircularArray)
951 await Promise.all(promises)
952 for (const workerNode of pool.workerNodes) {
953 expect(workerNode.usage).toStrictEqual({
955 executed: maxMultiplier,
959 sequentiallyStolen: 0,
964 history: expect.any(CircularArray)
967 history: expect.any(CircularArray)
971 history: expect.any(CircularArray)
974 history: expect.any(CircularArray)
982 it("Verify that pool worker tasks usage aren't reset at worker choice strategy change", async () => {
983 const pool = new DynamicThreadPool(
984 Math.floor(numberOfWorkers / 2),
986 './tests/worker-files/thread/testWorker.mjs'
988 const promises = new Set()
989 const maxMultiplier = 2
990 for (let i = 0; i < numberOfWorkers * maxMultiplier; i++) {
991 promises.add(pool.execute())
993 await Promise.all(promises)
994 for (const workerNode of pool.workerNodes) {
995 expect(workerNode.usage).toStrictEqual({
997 executed: expect.any(Number),
1001 sequentiallyStolen: 0,
1006 history: expect.any(CircularArray)
1009 history: expect.any(CircularArray)
1013 history: expect.any(CircularArray)
1016 history: expect.any(CircularArray)
1020 expect(workerNode.usage.tasks.executed).toBeGreaterThan(0)
1021 expect(workerNode.usage.tasks.executed).toBeLessThanOrEqual(
1022 numberOfWorkers * maxMultiplier
1024 expect(workerNode.usage.runTime.history.length).toBe(0)
1025 expect(workerNode.usage.waitTime.history.length).toBe(0)
1026 expect(workerNode.usage.elu.idle.history.length).toBe(0)
1027 expect(workerNode.usage.elu.active.history.length).toBe(0)
1029 pool.setWorkerChoiceStrategy(WorkerChoiceStrategies.FAIR_SHARE)
1030 for (const workerNode of pool.workerNodes) {
1031 expect(workerNode.usage).toStrictEqual({
1033 executed: expect.any(Number),
1037 sequentiallyStolen: 0,
1042 history: expect.any(CircularArray)
1045 history: expect.any(CircularArray)
1049 history: expect.any(CircularArray)
1052 history: expect.any(CircularArray)
1056 expect(workerNode.usage.tasks.executed).toBeGreaterThan(0)
1057 expect(workerNode.usage.tasks.executed).toBeLessThanOrEqual(
1058 numberOfWorkers * maxMultiplier
1060 expect(workerNode.usage.runTime.history.length).toBe(0)
1061 expect(workerNode.usage.waitTime.history.length).toBe(0)
1062 expect(workerNode.usage.elu.idle.history.length).toBe(0)
1063 expect(workerNode.usage.elu.active.history.length).toBe(0)
1065 await pool.destroy()
1068 it("Verify that pool event emitter 'ready' event can register a callback", async () => {
1069 const pool = new DynamicClusterPool(
1070 Math.floor(numberOfWorkers / 2),
1072 './tests/worker-files/cluster/testWorker.cjs'
1074 expect(pool.emitter.eventNames()).toStrictEqual([])
1077 pool.emitter.on(PoolEvents.ready, info => {
1081 await waitPoolEvents(pool, PoolEvents.ready, 1)
1082 expect(pool.emitter.eventNames()).toStrictEqual([PoolEvents.ready])
1083 expect(poolReady).toBe(1)
1084 expect(poolInfo).toStrictEqual({
1086 type: PoolTypes.dynamic,
1087 worker: WorkerTypes.cluster,
1090 defaultStrategy: WorkerChoiceStrategies.ROUND_ROBIN,
1091 strategyRetries: expect.any(Number),
1092 minSize: expect.any(Number),
1093 maxSize: expect.any(Number),
1094 workerNodes: expect.any(Number),
1095 idleWorkerNodes: expect.any(Number),
1096 busyWorkerNodes: expect.any(Number),
1097 executedTasks: expect.any(Number),
1098 executingTasks: expect.any(Number),
1099 failedTasks: expect.any(Number)
1101 await pool.destroy()
1104 it("Verify that pool event emitter 'busy' event can register a callback", async () => {
1105 const pool = new FixedThreadPool(
1107 './tests/worker-files/thread/testWorker.mjs'
1109 expect(pool.emitter.eventNames()).toStrictEqual([])
1110 const promises = new Set()
1113 pool.emitter.on(PoolEvents.busy, info => {
1117 expect(pool.emitter.eventNames()).toStrictEqual([PoolEvents.busy])
1118 for (let i = 0; i < numberOfWorkers * 2; i++) {
1119 promises.add(pool.execute())
1121 await Promise.all(promises)
1122 // The `busy` event is triggered when the number of submitted tasks at once reach the number of fixed pool workers.
1123 // So in total numberOfWorkers + 1 times for a loop submitting up to numberOfWorkers * 2 tasks to the fixed pool.
1124 expect(poolBusy).toBe(numberOfWorkers + 1)
1125 expect(poolInfo).toStrictEqual({
1127 type: PoolTypes.fixed,
1128 worker: WorkerTypes.thread,
1131 defaultStrategy: WorkerChoiceStrategies.ROUND_ROBIN,
1132 strategyRetries: expect.any(Number),
1133 minSize: expect.any(Number),
1134 maxSize: expect.any(Number),
1135 workerNodes: expect.any(Number),
1136 idleWorkerNodes: expect.any(Number),
1137 busyWorkerNodes: expect.any(Number),
1138 executedTasks: expect.any(Number),
1139 executingTasks: expect.any(Number),
1140 failedTasks: expect.any(Number)
1142 await pool.destroy()
1145 it("Verify that pool event emitter 'full' event can register a callback", async () => {
1146 const pool = new DynamicThreadPool(
1147 Math.floor(numberOfWorkers / 2),
1149 './tests/worker-files/thread/testWorker.mjs'
1151 expect(pool.emitter.eventNames()).toStrictEqual([])
1152 const promises = new Set()
1155 pool.emitter.on(PoolEvents.full, info => {
1159 expect(pool.emitter.eventNames()).toStrictEqual([PoolEvents.full])
1160 for (let i = 0; i < numberOfWorkers * 2; i++) {
1161 promises.add(pool.execute())
1163 await Promise.all(promises)
1164 expect(poolFull).toBe(1)
1165 expect(poolInfo).toStrictEqual({
1167 type: PoolTypes.dynamic,
1168 worker: WorkerTypes.thread,
1171 defaultStrategy: WorkerChoiceStrategies.ROUND_ROBIN,
1172 strategyRetries: expect.any(Number),
1173 minSize: expect.any(Number),
1174 maxSize: expect.any(Number),
1175 workerNodes: expect.any(Number),
1176 idleWorkerNodes: expect.any(Number),
1177 busyWorkerNodes: expect.any(Number),
1178 executedTasks: expect.any(Number),
1179 executingTasks: expect.any(Number),
1180 failedTasks: expect.any(Number)
1182 await pool.destroy()
1185 it("Verify that pool event emitter 'backPressure' event can register a callback", async () => {
1186 const pool = new FixedThreadPool(
1188 './tests/worker-files/thread/testWorker.mjs',
1190 enableTasksQueue: true
1193 stub(pool, 'hasBackPressure').returns(true)
1194 expect(pool.emitter.eventNames()).toStrictEqual([])
1195 const promises = new Set()
1196 let poolBackPressure = 0
1198 pool.emitter.on(PoolEvents.backPressure, info => {
1202 expect(pool.emitter.eventNames()).toStrictEqual([PoolEvents.backPressure])
1203 for (let i = 0; i < numberOfWorkers + 1; i++) {
1204 promises.add(pool.execute())
1206 await Promise.all(promises)
1207 expect(poolBackPressure).toBe(1)
1208 expect(poolInfo).toStrictEqual({
1210 type: PoolTypes.fixed,
1211 worker: WorkerTypes.thread,
1214 defaultStrategy: WorkerChoiceStrategies.ROUND_ROBIN,
1215 strategyRetries: expect.any(Number),
1216 minSize: expect.any(Number),
1217 maxSize: expect.any(Number),
1218 workerNodes: expect.any(Number),
1219 idleWorkerNodes: expect.any(Number),
1220 stealingWorkerNodes: expect.any(Number),
1221 busyWorkerNodes: expect.any(Number),
1222 executedTasks: expect.any(Number),
1223 executingTasks: expect.any(Number),
1224 maxQueuedTasks: expect.any(Number),
1225 queuedTasks: expect.any(Number),
1227 stolenTasks: expect.any(Number),
1228 failedTasks: expect.any(Number)
1230 expect(pool.hasBackPressure.callCount).toBeGreaterThanOrEqual(7)
1231 await pool.destroy()
1234 it('Verify that destroy() waits for queued tasks to finish', async () => {
1235 const tasksFinishedTimeout = 2500
1236 const pool = new FixedThreadPool(
1238 './tests/worker-files/thread/asyncWorker.mjs',
1240 enableTasksQueue: true,
1241 tasksQueueOptions: { tasksFinishedTimeout }
1244 const maxMultiplier = 4
1245 let tasksFinished = 0
1246 for (const workerNode of pool.workerNodes) {
1247 workerNode.on('taskFinished', () => {
1251 for (let i = 0; i < numberOfWorkers * maxMultiplier; i++) {
1254 expect(pool.info.queuedTasks).toBeGreaterThan(0)
1255 const startTime = performance.now()
1256 await pool.destroy()
1257 const elapsedTime = performance.now() - startTime
1258 expect(tasksFinished).toBeLessThanOrEqual(numberOfWorkers * maxMultiplier)
1259 expect(elapsedTime).toBeGreaterThanOrEqual(2000)
1260 expect(elapsedTime).toBeLessThanOrEqual(tasksFinishedTimeout + 800)
1263 it('Verify that destroy() waits until the tasks finished timeout is reached', async () => {
1264 const tasksFinishedTimeout = 1000
1265 const pool = new FixedThreadPool(
1267 './tests/worker-files/thread/asyncWorker.mjs',
1269 enableTasksQueue: true,
1270 tasksQueueOptions: { tasksFinishedTimeout }
1273 const maxMultiplier = 4
1274 let tasksFinished = 0
1275 for (const workerNode of pool.workerNodes) {
1276 workerNode.on('taskFinished', () => {
1280 for (let i = 0; i < numberOfWorkers * maxMultiplier; i++) {
1283 expect(pool.info.queuedTasks).toBeGreaterThan(0)
1284 const startTime = performance.now()
1285 await pool.destroy()
1286 const elapsedTime = performance.now() - startTime
1287 expect(tasksFinished).toBe(0)
1288 expect(elapsedTime).toBeLessThanOrEqual(tasksFinishedTimeout + 800)
1291 it('Verify that pool asynchronous resource track tasks execution', async () => {
1296 let resolveCalls = 0
1297 const hook = createHook({
1298 init (asyncId, type) {
1299 if (type === 'poolifier:task') {
1301 taskAsyncId = asyncId
1305 if (asyncId === taskAsyncId) beforeCalls++
1308 if (asyncId === taskAsyncId) afterCalls++
1311 if (executionAsyncId() === taskAsyncId) resolveCalls++
1314 const pool = new FixedThreadPool(
1316 './tests/worker-files/thread/testWorker.mjs'
1319 await pool.execute()
1321 expect(initCalls).toBe(1)
1322 expect(beforeCalls).toBe(1)
1323 expect(afterCalls).toBe(1)
1324 expect(resolveCalls).toBe(1)
1325 await pool.destroy()
1328 it('Verify that hasTaskFunction() is working', async () => {
1329 const dynamicThreadPool = new DynamicThreadPool(
1330 Math.floor(numberOfWorkers / 2),
1332 './tests/worker-files/thread/testMultipleTaskFunctionsWorker.mjs'
1334 await waitPoolEvents(dynamicThreadPool, PoolEvents.ready, 1)
1335 expect(dynamicThreadPool.hasTaskFunction(DEFAULT_TASK_NAME)).toBe(true)
1336 expect(dynamicThreadPool.hasTaskFunction('jsonIntegerSerialization')).toBe(
1339 expect(dynamicThreadPool.hasTaskFunction('factorial')).toBe(true)
1340 expect(dynamicThreadPool.hasTaskFunction('fibonacci')).toBe(true)
1341 expect(dynamicThreadPool.hasTaskFunction('unknown')).toBe(false)
1342 await dynamicThreadPool.destroy()
1343 const fixedClusterPool = new FixedClusterPool(
1345 './tests/worker-files/cluster/testMultipleTaskFunctionsWorker.cjs'
1347 await waitPoolEvents(fixedClusterPool, PoolEvents.ready, 1)
1348 expect(fixedClusterPool.hasTaskFunction(DEFAULT_TASK_NAME)).toBe(true)
1349 expect(fixedClusterPool.hasTaskFunction('jsonIntegerSerialization')).toBe(
1352 expect(fixedClusterPool.hasTaskFunction('factorial')).toBe(true)
1353 expect(fixedClusterPool.hasTaskFunction('fibonacci')).toBe(true)
1354 expect(fixedClusterPool.hasTaskFunction('unknown')).toBe(false)
1355 await fixedClusterPool.destroy()
1358 it('Verify that addTaskFunction() is working', async () => {
1359 const dynamicThreadPool = new DynamicThreadPool(
1360 Math.floor(numberOfWorkers / 2),
1362 './tests/worker-files/thread/testWorker.mjs'
1364 await waitPoolEvents(dynamicThreadPool, PoolEvents.ready, 1)
1366 dynamicThreadPool.addTaskFunction(0, () => {})
1367 ).rejects.toThrow(new TypeError('name argument must be a string'))
1369 dynamicThreadPool.addTaskFunction('', () => {})
1371 new TypeError('name argument must not be an empty string')
1373 await expect(dynamicThreadPool.addTaskFunction('test', 0)).rejects.toThrow(
1374 new TypeError('taskFunction property must be a function')
1376 await expect(dynamicThreadPool.addTaskFunction('test', '')).rejects.toThrow(
1377 new TypeError('taskFunction property must be a function')
1380 dynamicThreadPool.addTaskFunction('test', { taskFunction: 0 })
1381 ).rejects.toThrow(new TypeError('taskFunction property must be a function'))
1383 dynamicThreadPool.addTaskFunction('test', { taskFunction: '' })
1384 ).rejects.toThrow(new TypeError('taskFunction property must be a function'))
1386 dynamicThreadPool.addTaskFunction('test', {
1387 taskFunction: () => {},
1391 new RangeError("Property 'priority' must be between -20 and 19")
1394 dynamicThreadPool.addTaskFunction('test', {
1395 taskFunction: () => {},
1399 new RangeError("Property 'priority' must be between -20 and 19")
1402 dynamicThreadPool.addTaskFunction('test', {
1403 taskFunction: () => {},
1404 strategy: 'invalidStrategy'
1407 new Error("Invalid worker choice strategy 'invalidStrategy'")
1409 expect(dynamicThreadPool.listTaskFunctionsProperties()).toStrictEqual([
1410 { name: DEFAULT_TASK_NAME },
1414 ...dynamicThreadPool.workerChoiceStrategiesContext.workerChoiceStrategies.keys()
1415 ]).toStrictEqual([WorkerChoiceStrategies.ROUND_ROBIN])
1416 const echoTaskFunction = data => {
1420 dynamicThreadPool.addTaskFunction('echo', {
1421 taskFunction: echoTaskFunction,
1422 strategy: WorkerChoiceStrategies.LEAST_ELU
1424 ).resolves.toBe(true)
1425 expect(dynamicThreadPool.taskFunctions.size).toBe(1)
1426 expect(dynamicThreadPool.taskFunctions.get('echo')).toStrictEqual({
1427 taskFunction: echoTaskFunction,
1428 strategy: WorkerChoiceStrategies.LEAST_ELU
1431 ...dynamicThreadPool.workerChoiceStrategiesContext.workerChoiceStrategies.keys()
1433 WorkerChoiceStrategies.ROUND_ROBIN,
1434 WorkerChoiceStrategies.LEAST_ELU
1436 expect(dynamicThreadPool.listTaskFunctionsProperties()).toStrictEqual([
1437 { name: DEFAULT_TASK_NAME },
1439 { name: 'echo', strategy: WorkerChoiceStrategies.LEAST_ELU }
1441 const taskFunctionData = { test: 'test' }
1442 const echoResult = await dynamicThreadPool.execute(taskFunctionData, 'echo')
1443 expect(echoResult).toStrictEqual(taskFunctionData)
1444 for (const workerNode of dynamicThreadPool.workerNodes) {
1445 expect(workerNode.getTaskFunctionWorkerUsage('echo')).toStrictEqual({
1447 executed: expect.any(Number),
1450 sequentiallyStolen: 0,
1455 history: new CircularArray()
1458 history: new CircularArray()
1460 elu: expect.objectContaining({
1461 idle: expect.objectContaining({
1462 history: expect.any(CircularArray)
1464 active: expect.objectContaining({
1465 history: expect.any(CircularArray)
1470 workerNode.getTaskFunctionWorkerUsage('echo').tasks.executed
1471 ).toBeGreaterThan(0)
1473 workerNode.getTaskFunctionWorkerUsage('echo').elu.active.aggregate ==
1477 workerNode.getTaskFunctionWorkerUsage('echo').elu.active.aggregate
1481 workerNode.getTaskFunctionWorkerUsage('echo').elu.active.aggregate
1482 ).toBeGreaterThan(0)
1485 workerNode.getTaskFunctionWorkerUsage('echo').elu.idle.aggregate == null
1488 workerNode.getTaskFunctionWorkerUsage('echo').elu.idle.aggregate
1492 workerNode.getTaskFunctionWorkerUsage('echo').elu.idle.aggregate
1493 ).toBeGreaterThanOrEqual(0)
1496 workerNode.getTaskFunctionWorkerUsage('echo').elu.utilization == null
1499 workerNode.getTaskFunctionWorkerUsage('echo').elu.utilization
1503 workerNode.getTaskFunctionWorkerUsage('echo').elu.utilization
1504 ).toBeGreaterThanOrEqual(0)
1506 workerNode.getTaskFunctionWorkerUsage('echo').elu.utilization
1507 ).toBeLessThanOrEqual(1)
1510 await dynamicThreadPool.destroy()
1513 it('Verify that removeTaskFunction() is working', async () => {
1514 const dynamicThreadPool = new DynamicThreadPool(
1515 Math.floor(numberOfWorkers / 2),
1517 './tests/worker-files/thread/testWorker.mjs'
1519 await waitPoolEvents(dynamicThreadPool, PoolEvents.ready, 1)
1520 expect(dynamicThreadPool.listTaskFunctionsProperties()).toStrictEqual([
1521 { name: DEFAULT_TASK_NAME },
1524 await expect(dynamicThreadPool.removeTaskFunction('test')).rejects.toThrow(
1525 new Error('Cannot remove a task function not handled on the pool side')
1527 const echoTaskFunction = data => {
1530 await dynamicThreadPool.addTaskFunction('echo', {
1531 taskFunction: echoTaskFunction,
1532 strategy: WorkerChoiceStrategies.LEAST_ELU
1534 expect(dynamicThreadPool.taskFunctions.size).toBe(1)
1535 expect(dynamicThreadPool.taskFunctions.get('echo')).toStrictEqual({
1536 taskFunction: echoTaskFunction,
1537 strategy: WorkerChoiceStrategies.LEAST_ELU
1540 ...dynamicThreadPool.workerChoiceStrategiesContext.workerChoiceStrategies.keys()
1542 WorkerChoiceStrategies.ROUND_ROBIN,
1543 WorkerChoiceStrategies.LEAST_ELU
1545 expect(dynamicThreadPool.listTaskFunctionsProperties()).toStrictEqual([
1546 { name: DEFAULT_TASK_NAME },
1548 { name: 'echo', strategy: WorkerChoiceStrategies.LEAST_ELU }
1550 await expect(dynamicThreadPool.removeTaskFunction('echo')).resolves.toBe(
1553 expect(dynamicThreadPool.taskFunctions.size).toBe(0)
1554 expect(dynamicThreadPool.taskFunctions.get('echo')).toBeUndefined()
1556 ...dynamicThreadPool.workerChoiceStrategiesContext.workerChoiceStrategies.keys()
1557 ]).toStrictEqual([WorkerChoiceStrategies.ROUND_ROBIN])
1558 expect(dynamicThreadPool.listTaskFunctionsProperties()).toStrictEqual([
1559 { name: DEFAULT_TASK_NAME },
1562 await dynamicThreadPool.destroy()
1565 it('Verify that listTaskFunctionsProperties() is working', async () => {
1566 const dynamicThreadPool = new DynamicThreadPool(
1567 Math.floor(numberOfWorkers / 2),
1569 './tests/worker-files/thread/testMultipleTaskFunctionsWorker.mjs'
1571 await waitPoolEvents(dynamicThreadPool, PoolEvents.ready, 1)
1572 expect(dynamicThreadPool.listTaskFunctionsProperties()).toStrictEqual([
1573 { name: DEFAULT_TASK_NAME },
1574 { name: 'jsonIntegerSerialization' },
1575 { name: 'factorial' },
1576 { name: 'fibonacci' }
1578 await dynamicThreadPool.destroy()
1579 const fixedClusterPool = new FixedClusterPool(
1581 './tests/worker-files/cluster/testMultipleTaskFunctionsWorker.cjs'
1583 await waitPoolEvents(fixedClusterPool, PoolEvents.ready, 1)
1584 expect(fixedClusterPool.listTaskFunctionsProperties()).toStrictEqual([
1585 { name: DEFAULT_TASK_NAME },
1586 { name: 'jsonIntegerSerialization' },
1587 { name: 'factorial' },
1588 { name: 'fibonacci' }
1590 await fixedClusterPool.destroy()
1593 it('Verify that setDefaultTaskFunction() is working', async () => {
1594 const dynamicThreadPool = new DynamicThreadPool(
1595 Math.floor(numberOfWorkers / 2),
1597 './tests/worker-files/thread/testMultipleTaskFunctionsWorker.mjs'
1599 await waitPoolEvents(dynamicThreadPool, PoolEvents.ready, 1)
1600 const workerId = dynamicThreadPool.workerNodes[0].info.id
1601 await expect(dynamicThreadPool.setDefaultTaskFunction(0)).rejects.toThrow(
1603 `Task function operation 'default' failed on worker ${workerId} with error: 'TypeError: name parameter is not a string'`
1607 dynamicThreadPool.setDefaultTaskFunction(DEFAULT_TASK_NAME)
1610 `Task function operation 'default' failed on worker ${workerId} with error: 'Error: Cannot set the default task function reserved name as the default task function'`
1614 dynamicThreadPool.setDefaultTaskFunction('unknown')
1617 `Task function operation 'default' failed on worker ${workerId} with error: 'Error: Cannot set the default task function to a non-existing task function'`
1620 expect(dynamicThreadPool.listTaskFunctionsProperties()).toStrictEqual([
1621 { name: DEFAULT_TASK_NAME },
1622 { name: 'jsonIntegerSerialization' },
1623 { name: 'factorial' },
1624 { name: 'fibonacci' }
1627 dynamicThreadPool.setDefaultTaskFunction('factorial')
1628 ).resolves.toBe(true)
1629 expect(dynamicThreadPool.listTaskFunctionsProperties()).toStrictEqual([
1630 { name: DEFAULT_TASK_NAME },
1631 { name: 'factorial' },
1632 { name: 'jsonIntegerSerialization' },
1633 { name: 'fibonacci' }
1636 dynamicThreadPool.setDefaultTaskFunction('fibonacci')
1637 ).resolves.toBe(true)
1638 expect(dynamicThreadPool.listTaskFunctionsProperties()).toStrictEqual([
1639 { name: DEFAULT_TASK_NAME },
1640 { name: 'fibonacci' },
1641 { name: 'jsonIntegerSerialization' },
1642 { name: 'factorial' }
1644 await dynamicThreadPool.destroy()
1647 it('Verify that multiple task functions worker is working', async () => {
1648 const pool = new DynamicClusterPool(
1649 Math.floor(numberOfWorkers / 2),
1651 './tests/worker-files/cluster/testMultipleTaskFunctionsWorker.cjs'
1653 const data = { n: 10 }
1654 const result0 = await pool.execute(data)
1655 expect(result0).toStrictEqual({ ok: 1 })
1656 const result1 = await pool.execute(data, 'jsonIntegerSerialization')
1657 expect(result1).toStrictEqual({ ok: 1 })
1658 const result2 = await pool.execute(data, 'factorial')
1659 expect(result2).toBe(3628800)
1660 const result3 = await pool.execute(data, 'fibonacci')
1661 expect(result3).toBe(55)
1662 expect(pool.info.executingTasks).toBe(0)
1663 expect(pool.info.executedTasks).toBe(4)
1664 for (const workerNode of pool.workerNodes) {
1665 expect(workerNode.info.taskFunctionsProperties).toStrictEqual([
1666 { name: DEFAULT_TASK_NAME },
1667 { name: 'jsonIntegerSerialization' },
1668 { name: 'factorial' },
1669 { name: 'fibonacci' }
1671 expect(workerNode.taskFunctionsUsage.size).toBe(3)
1672 for (const taskFunctionProperties of pool.listTaskFunctionsProperties()) {
1674 workerNode.getTaskFunctionWorkerUsage(taskFunctionProperties.name)
1677 executed: expect.any(Number),
1681 sequentiallyStolen: 0,
1685 history: expect.any(CircularArray)
1688 history: expect.any(CircularArray)
1692 history: expect.any(CircularArray)
1695 history: expect.any(CircularArray)
1700 workerNode.getTaskFunctionWorkerUsage(taskFunctionProperties.name)
1702 ).toBeGreaterThan(0)
1705 workerNode.getTaskFunctionWorkerUsage(DEFAULT_TASK_NAME)
1707 workerNode.getTaskFunctionWorkerUsage(
1708 workerNode.info.taskFunctionsProperties[1].name
1712 await pool.destroy()
1715 it('Verify sendKillMessageToWorker()', async () => {
1716 const pool = new DynamicClusterPool(
1717 Math.floor(numberOfWorkers / 2),
1719 './tests/worker-files/cluster/testWorker.cjs'
1721 const workerNodeKey = 0
1723 pool.sendKillMessageToWorker(workerNodeKey)
1724 ).resolves.toBeUndefined()
1725 await pool.destroy()
1728 it('Verify sendTaskFunctionOperationToWorker()', async () => {
1729 const pool = new DynamicClusterPool(
1730 Math.floor(numberOfWorkers / 2),
1732 './tests/worker-files/cluster/testWorker.cjs'
1734 const workerNodeKey = 0
1736 pool.sendTaskFunctionOperationToWorker(workerNodeKey, {
1737 taskFunctionOperation: 'add',
1738 taskFunctionProperties: { name: 'empty' },
1739 taskFunction: (() => {}).toString()
1741 ).resolves.toBe(true)
1743 pool.workerNodes[workerNodeKey].info.taskFunctionsProperties
1745 { name: DEFAULT_TASK_NAME },
1749 await pool.destroy()
1752 it('Verify sendTaskFunctionOperationToWorkers()', async () => {
1753 const pool = new DynamicClusterPool(
1754 Math.floor(numberOfWorkers / 2),
1756 './tests/worker-files/cluster/testWorker.cjs'
1759 pool.sendTaskFunctionOperationToWorkers({
1760 taskFunctionOperation: 'add',
1761 taskFunctionProperties: { name: 'empty' },
1762 taskFunction: (() => {}).toString()
1764 ).resolves.toBe(true)
1765 for (const workerNode of pool.workerNodes) {
1766 expect(workerNode.info.taskFunctionsProperties).toStrictEqual([
1767 { name: DEFAULT_TASK_NAME },
1772 await pool.destroy()