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: true,
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: true,
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: true,
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: true,
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,
826 pool = new DynamicThreadPool(
827 Math.floor(numberOfWorkers / 2),
829 './tests/worker-files/thread/testWorker.mjs'
831 for (const workerNode of pool.workerNodes) {
832 expect(workerNode).toBeInstanceOf(WorkerNode)
833 expect(workerNode.info).toStrictEqual({
834 id: expect.any(Number),
835 type: WorkerTypes.thread,
844 it('Verify that pool statuses are checked at start or destroy', async () => {
845 const pool = new FixedThreadPool(
847 './tests/worker-files/thread/testWorker.mjs'
849 expect(pool.info.started).toBe(true)
850 expect(pool.info.ready).toBe(true)
851 expect(() => pool.start()).toThrow(
852 new Error('Cannot start an already started pool')
855 expect(pool.info.started).toBe(false)
856 expect(pool.info.ready).toBe(false)
857 await expect(pool.destroy()).rejects.toThrow(
858 new Error('Cannot destroy an already destroyed pool')
862 it('Verify that pool can be started after initialization', async () => {
863 const pool = new FixedClusterPool(
865 './tests/worker-files/cluster/testWorker.cjs',
870 expect(pool.info.started).toBe(false)
871 expect(pool.info.ready).toBe(false)
872 expect(pool.workerNodes).toStrictEqual([])
873 expect(pool.readyEventEmitted).toBe(false)
874 await expect(pool.execute()).rejects.toThrow(
875 new Error('Cannot execute a task on not started pool')
878 expect(pool.info.started).toBe(true)
879 expect(pool.info.ready).toBe(true)
880 await waitPoolEvents(pool, PoolEvents.ready, 1)
881 expect(pool.readyEventEmitted).toBe(true)
882 expect(pool.workerNodes.length).toBe(numberOfWorkers)
883 for (const workerNode of pool.workerNodes) {
884 expect(workerNode).toBeInstanceOf(WorkerNode)
889 it('Verify that pool execute() arguments are checked', async () => {
890 const pool = new FixedClusterPool(
892 './tests/worker-files/cluster/testWorker.cjs'
894 await expect(pool.execute(undefined, 0)).rejects.toThrow(
895 new TypeError('name argument must be a string')
897 await expect(pool.execute(undefined, '')).rejects.toThrow(
898 new TypeError('name argument must not be an empty string')
900 await expect(pool.execute(undefined, undefined, {})).rejects.toThrow(
901 new TypeError('transferList argument must be an array')
903 await expect(pool.execute(undefined, 'unknown')).rejects.toBe(
904 "Task function 'unknown' not found"
907 await expect(pool.execute()).rejects.toThrow(
908 new Error('Cannot execute a task on not started pool')
912 it('Verify that pool worker tasks usage are computed', async () => {
913 const pool = new FixedClusterPool(
915 './tests/worker-files/cluster/testWorker.cjs'
917 const promises = new Set()
918 const maxMultiplier = 2
919 for (let i = 0; i < numberOfWorkers * maxMultiplier; i++) {
920 promises.add(pool.execute())
922 for (const workerNode of pool.workerNodes) {
923 expect(workerNode.usage).toStrictEqual({
926 executing: maxMultiplier,
929 sequentiallyStolen: 0,
934 history: expect.any(CircularArray)
937 history: expect.any(CircularArray)
941 history: expect.any(CircularArray)
944 history: expect.any(CircularArray)
949 await Promise.all(promises)
950 for (const workerNode of pool.workerNodes) {
951 expect(workerNode.usage).toStrictEqual({
953 executed: maxMultiplier,
957 sequentiallyStolen: 0,
962 history: expect.any(CircularArray)
965 history: expect.any(CircularArray)
969 history: expect.any(CircularArray)
972 history: expect.any(CircularArray)
980 it("Verify that pool worker tasks usage aren't reset at worker choice strategy change", async () => {
981 const pool = new DynamicThreadPool(
982 Math.floor(numberOfWorkers / 2),
984 './tests/worker-files/thread/testWorker.mjs'
986 const promises = new Set()
987 const maxMultiplier = 2
988 for (let i = 0; i < numberOfWorkers * maxMultiplier; i++) {
989 promises.add(pool.execute())
991 await Promise.all(promises)
992 for (const workerNode of pool.workerNodes) {
993 expect(workerNode.usage).toStrictEqual({
995 executed: expect.any(Number),
999 sequentiallyStolen: 0,
1004 history: expect.any(CircularArray)
1007 history: expect.any(CircularArray)
1011 history: expect.any(CircularArray)
1014 history: expect.any(CircularArray)
1018 expect(workerNode.usage.tasks.executed).toBeGreaterThan(0)
1019 expect(workerNode.usage.tasks.executed).toBeLessThanOrEqual(
1020 numberOfWorkers * maxMultiplier
1022 expect(workerNode.usage.runTime.history.length).toBe(0)
1023 expect(workerNode.usage.waitTime.history.length).toBe(0)
1024 expect(workerNode.usage.elu.idle.history.length).toBe(0)
1025 expect(workerNode.usage.elu.active.history.length).toBe(0)
1027 pool.setWorkerChoiceStrategy(WorkerChoiceStrategies.FAIR_SHARE)
1028 for (const workerNode of pool.workerNodes) {
1029 expect(workerNode.usage).toStrictEqual({
1031 executed: expect.any(Number),
1035 sequentiallyStolen: 0,
1040 history: expect.any(CircularArray)
1043 history: expect.any(CircularArray)
1047 history: expect.any(CircularArray)
1050 history: expect.any(CircularArray)
1054 expect(workerNode.usage.tasks.executed).toBeGreaterThan(0)
1055 expect(workerNode.usage.tasks.executed).toBeLessThanOrEqual(
1056 numberOfWorkers * maxMultiplier
1058 expect(workerNode.usage.runTime.history.length).toBe(0)
1059 expect(workerNode.usage.waitTime.history.length).toBe(0)
1060 expect(workerNode.usage.elu.idle.history.length).toBe(0)
1061 expect(workerNode.usage.elu.active.history.length).toBe(0)
1063 await pool.destroy()
1066 it("Verify that pool event emitter 'ready' event can register a callback", async () => {
1067 const pool = new DynamicClusterPool(
1068 Math.floor(numberOfWorkers / 2),
1070 './tests/worker-files/cluster/testWorker.cjs'
1072 expect(pool.emitter.eventNames()).toStrictEqual([])
1075 pool.emitter.on(PoolEvents.ready, info => {
1079 await waitPoolEvents(pool, PoolEvents.ready, 1)
1080 expect(pool.emitter.eventNames()).toStrictEqual([PoolEvents.ready])
1081 expect(poolReady).toBe(1)
1082 expect(poolInfo).toStrictEqual({
1084 type: PoolTypes.dynamic,
1085 worker: WorkerTypes.cluster,
1088 defaultStrategy: WorkerChoiceStrategies.ROUND_ROBIN,
1089 strategyRetries: expect.any(Number),
1090 minSize: expect.any(Number),
1091 maxSize: expect.any(Number),
1092 workerNodes: expect.any(Number),
1093 idleWorkerNodes: expect.any(Number),
1094 busyWorkerNodes: expect.any(Number),
1095 executedTasks: expect.any(Number),
1096 executingTasks: expect.any(Number),
1097 failedTasks: expect.any(Number)
1099 await pool.destroy()
1102 it("Verify that pool event emitter 'busy' event can register a callback", async () => {
1103 const pool = new FixedThreadPool(
1105 './tests/worker-files/thread/testWorker.mjs'
1107 expect(pool.emitter.eventNames()).toStrictEqual([])
1108 const promises = new Set()
1111 pool.emitter.on(PoolEvents.busy, info => {
1115 expect(pool.emitter.eventNames()).toStrictEqual([PoolEvents.busy])
1116 for (let i = 0; i < numberOfWorkers * 2; i++) {
1117 promises.add(pool.execute())
1119 await Promise.all(promises)
1120 // The `busy` event is triggered when the number of submitted tasks at once reach the number of fixed pool workers.
1121 // So in total numberOfWorkers + 1 times for a loop submitting up to numberOfWorkers * 2 tasks to the fixed pool.
1122 expect(poolBusy).toBe(numberOfWorkers + 1)
1123 expect(poolInfo).toStrictEqual({
1125 type: PoolTypes.fixed,
1126 worker: WorkerTypes.thread,
1129 defaultStrategy: WorkerChoiceStrategies.ROUND_ROBIN,
1130 strategyRetries: expect.any(Number),
1131 minSize: expect.any(Number),
1132 maxSize: expect.any(Number),
1133 workerNodes: expect.any(Number),
1134 idleWorkerNodes: expect.any(Number),
1135 busyWorkerNodes: expect.any(Number),
1136 executedTasks: expect.any(Number),
1137 executingTasks: expect.any(Number),
1138 failedTasks: expect.any(Number)
1140 await pool.destroy()
1143 it("Verify that pool event emitter 'full' event can register a callback", async () => {
1144 const pool = new DynamicThreadPool(
1145 Math.floor(numberOfWorkers / 2),
1147 './tests/worker-files/thread/testWorker.mjs'
1149 expect(pool.emitter.eventNames()).toStrictEqual([])
1150 const promises = new Set()
1153 pool.emitter.on(PoolEvents.full, info => {
1157 expect(pool.emitter.eventNames()).toStrictEqual([PoolEvents.full])
1158 for (let i = 0; i < numberOfWorkers * 2; i++) {
1159 promises.add(pool.execute())
1161 await Promise.all(promises)
1162 expect(poolFull).toBe(1)
1163 expect(poolInfo).toStrictEqual({
1165 type: PoolTypes.dynamic,
1166 worker: WorkerTypes.thread,
1169 defaultStrategy: WorkerChoiceStrategies.ROUND_ROBIN,
1170 strategyRetries: expect.any(Number),
1171 minSize: expect.any(Number),
1172 maxSize: expect.any(Number),
1173 workerNodes: expect.any(Number),
1174 idleWorkerNodes: expect.any(Number),
1175 busyWorkerNodes: expect.any(Number),
1176 executedTasks: expect.any(Number),
1177 executingTasks: expect.any(Number),
1178 failedTasks: expect.any(Number)
1180 await pool.destroy()
1183 it("Verify that pool event emitter 'backPressure' event can register a callback", async () => {
1184 const pool = new FixedThreadPool(
1186 './tests/worker-files/thread/testWorker.mjs',
1188 enableTasksQueue: true
1191 stub(pool, 'hasBackPressure').returns(true)
1192 expect(pool.emitter.eventNames()).toStrictEqual([])
1193 const promises = new Set()
1194 let poolBackPressure = 0
1196 pool.emitter.on(PoolEvents.backPressure, info => {
1200 expect(pool.emitter.eventNames()).toStrictEqual([PoolEvents.backPressure])
1201 for (let i = 0; i < numberOfWorkers + 1; i++) {
1202 promises.add(pool.execute())
1204 await Promise.all(promises)
1205 expect(poolBackPressure).toBe(1)
1206 expect(poolInfo).toStrictEqual({
1208 type: PoolTypes.fixed,
1209 worker: WorkerTypes.thread,
1212 defaultStrategy: WorkerChoiceStrategies.ROUND_ROBIN,
1213 strategyRetries: expect.any(Number),
1214 minSize: expect.any(Number),
1215 maxSize: expect.any(Number),
1216 workerNodes: expect.any(Number),
1217 idleWorkerNodes: expect.any(Number),
1218 stealingWorkerNodes: expect.any(Number),
1219 busyWorkerNodes: expect.any(Number),
1220 executedTasks: expect.any(Number),
1221 executingTasks: expect.any(Number),
1222 maxQueuedTasks: expect.any(Number),
1223 queuedTasks: expect.any(Number),
1225 stolenTasks: expect.any(Number),
1226 failedTasks: expect.any(Number)
1228 expect(pool.hasBackPressure.callCount).toBeGreaterThanOrEqual(7)
1229 await pool.destroy()
1232 it('Verify that destroy() waits for queued tasks to finish', async () => {
1233 const tasksFinishedTimeout = 2500
1234 const pool = new FixedThreadPool(
1236 './tests/worker-files/thread/asyncWorker.mjs',
1238 enableTasksQueue: true,
1239 tasksQueueOptions: { tasksFinishedTimeout }
1242 const maxMultiplier = 4
1243 let tasksFinished = 0
1244 for (const workerNode of pool.workerNodes) {
1245 workerNode.on('taskFinished', () => {
1249 for (let i = 0; i < numberOfWorkers * maxMultiplier; i++) {
1252 expect(pool.info.queuedTasks).toBeGreaterThan(0)
1253 const startTime = performance.now()
1254 await pool.destroy()
1255 const elapsedTime = performance.now() - startTime
1256 expect(tasksFinished).toBeLessThanOrEqual(numberOfWorkers * maxMultiplier)
1257 expect(elapsedTime).toBeGreaterThanOrEqual(2000)
1258 expect(elapsedTime).toBeLessThanOrEqual(tasksFinishedTimeout + 800)
1261 it('Verify that destroy() waits until the tasks finished timeout is reached', async () => {
1262 const tasksFinishedTimeout = 1000
1263 const pool = new FixedThreadPool(
1265 './tests/worker-files/thread/asyncWorker.mjs',
1267 enableTasksQueue: true,
1268 tasksQueueOptions: { tasksFinishedTimeout }
1271 const maxMultiplier = 4
1272 let tasksFinished = 0
1273 for (const workerNode of pool.workerNodes) {
1274 workerNode.on('taskFinished', () => {
1278 for (let i = 0; i < numberOfWorkers * maxMultiplier; i++) {
1281 expect(pool.info.queuedTasks).toBeGreaterThan(0)
1282 const startTime = performance.now()
1283 await pool.destroy()
1284 const elapsedTime = performance.now() - startTime
1285 expect(tasksFinished).toBe(0)
1286 expect(elapsedTime).toBeLessThanOrEqual(tasksFinishedTimeout + 800)
1289 it('Verify that pool asynchronous resource track tasks execution', async () => {
1294 let resolveCalls = 0
1295 const hook = createHook({
1296 init (asyncId, type) {
1297 if (type === 'poolifier:task') {
1299 taskAsyncId = asyncId
1303 if (asyncId === taskAsyncId) beforeCalls++
1306 if (asyncId === taskAsyncId) afterCalls++
1309 if (executionAsyncId() === taskAsyncId) resolveCalls++
1312 const pool = new FixedThreadPool(
1314 './tests/worker-files/thread/testWorker.mjs'
1317 await pool.execute()
1319 expect(initCalls).toBe(1)
1320 expect(beforeCalls).toBe(1)
1321 expect(afterCalls).toBe(1)
1322 expect(resolveCalls).toBe(1)
1323 await pool.destroy()
1326 it('Verify that hasTaskFunction() is working', async () => {
1327 const dynamicThreadPool = new DynamicThreadPool(
1328 Math.floor(numberOfWorkers / 2),
1330 './tests/worker-files/thread/testMultipleTaskFunctionsWorker.mjs'
1332 await waitPoolEvents(dynamicThreadPool, PoolEvents.ready, 1)
1333 expect(dynamicThreadPool.hasTaskFunction(DEFAULT_TASK_NAME)).toBe(true)
1334 expect(dynamicThreadPool.hasTaskFunction('jsonIntegerSerialization')).toBe(
1337 expect(dynamicThreadPool.hasTaskFunction('factorial')).toBe(true)
1338 expect(dynamicThreadPool.hasTaskFunction('fibonacci')).toBe(true)
1339 expect(dynamicThreadPool.hasTaskFunction('unknown')).toBe(false)
1340 await dynamicThreadPool.destroy()
1341 const fixedClusterPool = new FixedClusterPool(
1343 './tests/worker-files/cluster/testMultipleTaskFunctionsWorker.cjs'
1345 await waitPoolEvents(fixedClusterPool, PoolEvents.ready, 1)
1346 expect(fixedClusterPool.hasTaskFunction(DEFAULT_TASK_NAME)).toBe(true)
1347 expect(fixedClusterPool.hasTaskFunction('jsonIntegerSerialization')).toBe(
1350 expect(fixedClusterPool.hasTaskFunction('factorial')).toBe(true)
1351 expect(fixedClusterPool.hasTaskFunction('fibonacci')).toBe(true)
1352 expect(fixedClusterPool.hasTaskFunction('unknown')).toBe(false)
1353 await fixedClusterPool.destroy()
1356 it('Verify that addTaskFunction() is working', async () => {
1357 const dynamicThreadPool = new DynamicThreadPool(
1358 Math.floor(numberOfWorkers / 2),
1360 './tests/worker-files/thread/testWorker.mjs'
1362 await waitPoolEvents(dynamicThreadPool, PoolEvents.ready, 1)
1364 dynamicThreadPool.addTaskFunction(0, () => {})
1365 ).rejects.toThrow(new TypeError('name argument must be a string'))
1367 dynamicThreadPool.addTaskFunction('', () => {})
1369 new TypeError('name argument must not be an empty string')
1371 await expect(dynamicThreadPool.addTaskFunction('test', 0)).rejects.toThrow(
1372 new TypeError('taskFunction property must be a function')
1374 await expect(dynamicThreadPool.addTaskFunction('test', '')).rejects.toThrow(
1375 new TypeError('taskFunction property must be a function')
1378 dynamicThreadPool.addTaskFunction('test', { taskFunction: 0 })
1379 ).rejects.toThrow(new TypeError('taskFunction property must be a function'))
1381 dynamicThreadPool.addTaskFunction('test', { taskFunction: '' })
1382 ).rejects.toThrow(new TypeError('taskFunction property must be a function'))
1384 dynamicThreadPool.addTaskFunction('test', {
1385 taskFunction: () => {},
1389 new RangeError("Property 'priority' must be between -20 and 19")
1392 dynamicThreadPool.addTaskFunction('test', {
1393 taskFunction: () => {},
1397 new RangeError("Property 'priority' must be between -20 and 19")
1400 dynamicThreadPool.addTaskFunction('test', {
1401 taskFunction: () => {},
1402 strategy: 'invalidStrategy'
1405 new Error("Invalid worker choice strategy 'invalidStrategy'")
1407 expect(dynamicThreadPool.listTaskFunctionsProperties()).toStrictEqual([
1408 { name: DEFAULT_TASK_NAME },
1412 ...dynamicThreadPool.workerChoiceStrategiesContext.workerChoiceStrategies.keys()
1413 ]).toStrictEqual([WorkerChoiceStrategies.ROUND_ROBIN])
1414 const echoTaskFunction = data => {
1418 dynamicThreadPool.addTaskFunction('echo', {
1419 taskFunction: echoTaskFunction,
1420 strategy: WorkerChoiceStrategies.LEAST_ELU
1422 ).resolves.toBe(true)
1423 expect(dynamicThreadPool.taskFunctions.size).toBe(1)
1424 expect(dynamicThreadPool.taskFunctions.get('echo')).toStrictEqual({
1425 taskFunction: echoTaskFunction,
1426 strategy: WorkerChoiceStrategies.LEAST_ELU
1429 ...dynamicThreadPool.workerChoiceStrategiesContext.workerChoiceStrategies.keys()
1431 WorkerChoiceStrategies.ROUND_ROBIN,
1432 WorkerChoiceStrategies.LEAST_ELU
1434 expect(dynamicThreadPool.listTaskFunctionsProperties()).toStrictEqual([
1435 { name: DEFAULT_TASK_NAME },
1437 { name: 'echo', strategy: WorkerChoiceStrategies.LEAST_ELU }
1439 const taskFunctionData = { test: 'test' }
1440 const echoResult = await dynamicThreadPool.execute(taskFunctionData, 'echo')
1441 expect(echoResult).toStrictEqual(taskFunctionData)
1442 for (const workerNode of dynamicThreadPool.workerNodes) {
1443 expect(workerNode.getTaskFunctionWorkerUsage('echo')).toStrictEqual({
1445 executed: expect.any(Number),
1448 sequentiallyStolen: 0,
1453 history: new CircularArray()
1456 history: new CircularArray()
1458 elu: expect.objectContaining({
1459 idle: expect.objectContaining({
1460 history: expect.any(CircularArray)
1462 active: expect.objectContaining({
1463 history: expect.any(CircularArray)
1468 workerNode.getTaskFunctionWorkerUsage('echo').tasks.executed
1469 ).toBeGreaterThan(0)
1471 workerNode.getTaskFunctionWorkerUsage('echo').elu.active.aggregate ==
1475 workerNode.getTaskFunctionWorkerUsage('echo').elu.active.aggregate
1479 workerNode.getTaskFunctionWorkerUsage('echo').elu.active.aggregate
1480 ).toBeGreaterThan(0)
1483 workerNode.getTaskFunctionWorkerUsage('echo').elu.idle.aggregate == null
1486 workerNode.getTaskFunctionWorkerUsage('echo').elu.idle.aggregate
1490 workerNode.getTaskFunctionWorkerUsage('echo').elu.idle.aggregate
1491 ).toBeGreaterThanOrEqual(0)
1494 workerNode.getTaskFunctionWorkerUsage('echo').elu.utilization == null
1497 workerNode.getTaskFunctionWorkerUsage('echo').elu.utilization
1501 workerNode.getTaskFunctionWorkerUsage('echo').elu.utilization
1502 ).toBeGreaterThanOrEqual(0)
1504 workerNode.getTaskFunctionWorkerUsage('echo').elu.utilization
1505 ).toBeLessThanOrEqual(1)
1508 await dynamicThreadPool.destroy()
1511 it('Verify that removeTaskFunction() is working', async () => {
1512 const dynamicThreadPool = new DynamicThreadPool(
1513 Math.floor(numberOfWorkers / 2),
1515 './tests/worker-files/thread/testWorker.mjs'
1517 await waitPoolEvents(dynamicThreadPool, PoolEvents.ready, 1)
1518 expect(dynamicThreadPool.listTaskFunctionsProperties()).toStrictEqual([
1519 { name: DEFAULT_TASK_NAME },
1522 await expect(dynamicThreadPool.removeTaskFunction('test')).rejects.toThrow(
1523 new Error('Cannot remove a task function not handled on the pool side')
1525 const echoTaskFunction = data => {
1528 await dynamicThreadPool.addTaskFunction('echo', {
1529 taskFunction: echoTaskFunction,
1530 strategy: WorkerChoiceStrategies.LEAST_ELU
1532 expect(dynamicThreadPool.taskFunctions.size).toBe(1)
1533 expect(dynamicThreadPool.taskFunctions.get('echo')).toStrictEqual({
1534 taskFunction: echoTaskFunction,
1535 strategy: WorkerChoiceStrategies.LEAST_ELU
1538 ...dynamicThreadPool.workerChoiceStrategiesContext.workerChoiceStrategies.keys()
1540 WorkerChoiceStrategies.ROUND_ROBIN,
1541 WorkerChoiceStrategies.LEAST_ELU
1543 expect(dynamicThreadPool.listTaskFunctionsProperties()).toStrictEqual([
1544 { name: DEFAULT_TASK_NAME },
1546 { name: 'echo', strategy: WorkerChoiceStrategies.LEAST_ELU }
1548 await expect(dynamicThreadPool.removeTaskFunction('echo')).resolves.toBe(
1551 expect(dynamicThreadPool.taskFunctions.size).toBe(0)
1552 expect(dynamicThreadPool.taskFunctions.get('echo')).toBeUndefined()
1554 ...dynamicThreadPool.workerChoiceStrategiesContext.workerChoiceStrategies.keys()
1555 ]).toStrictEqual([WorkerChoiceStrategies.ROUND_ROBIN])
1556 expect(dynamicThreadPool.listTaskFunctionsProperties()).toStrictEqual([
1557 { name: DEFAULT_TASK_NAME },
1560 await dynamicThreadPool.destroy()
1563 it('Verify that listTaskFunctionsProperties() is working', async () => {
1564 const dynamicThreadPool = new DynamicThreadPool(
1565 Math.floor(numberOfWorkers / 2),
1567 './tests/worker-files/thread/testMultipleTaskFunctionsWorker.mjs'
1569 await waitPoolEvents(dynamicThreadPool, PoolEvents.ready, 1)
1570 expect(dynamicThreadPool.listTaskFunctionsProperties()).toStrictEqual([
1571 { name: DEFAULT_TASK_NAME },
1572 { name: 'jsonIntegerSerialization' },
1573 { name: 'factorial' },
1574 { name: 'fibonacci' }
1576 await dynamicThreadPool.destroy()
1577 const fixedClusterPool = new FixedClusterPool(
1579 './tests/worker-files/cluster/testMultipleTaskFunctionsWorker.cjs'
1581 await waitPoolEvents(fixedClusterPool, PoolEvents.ready, 1)
1582 expect(fixedClusterPool.listTaskFunctionsProperties()).toStrictEqual([
1583 { name: DEFAULT_TASK_NAME },
1584 { name: 'jsonIntegerSerialization' },
1585 { name: 'factorial' },
1586 { name: 'fibonacci' }
1588 await fixedClusterPool.destroy()
1591 it('Verify that setDefaultTaskFunction() is working', async () => {
1592 const dynamicThreadPool = new DynamicThreadPool(
1593 Math.floor(numberOfWorkers / 2),
1595 './tests/worker-files/thread/testMultipleTaskFunctionsWorker.mjs'
1597 await waitPoolEvents(dynamicThreadPool, PoolEvents.ready, 1)
1598 const workerId = dynamicThreadPool.workerNodes[0].info.id
1599 await expect(dynamicThreadPool.setDefaultTaskFunction(0)).rejects.toThrow(
1601 `Task function operation 'default' failed on worker ${workerId} with error: 'TypeError: name parameter is not a string'`
1605 dynamicThreadPool.setDefaultTaskFunction(DEFAULT_TASK_NAME)
1608 `Task function operation 'default' failed on worker ${workerId} with error: 'Error: Cannot set the default task function reserved name as the default task function'`
1612 dynamicThreadPool.setDefaultTaskFunction('unknown')
1615 `Task function operation 'default' failed on worker ${workerId} with error: 'Error: Cannot set the default task function to a non-existing task function'`
1618 expect(dynamicThreadPool.listTaskFunctionsProperties()).toStrictEqual([
1619 { name: DEFAULT_TASK_NAME },
1620 { name: 'jsonIntegerSerialization' },
1621 { name: 'factorial' },
1622 { name: 'fibonacci' }
1625 dynamicThreadPool.setDefaultTaskFunction('factorial')
1626 ).resolves.toBe(true)
1627 expect(dynamicThreadPool.listTaskFunctionsProperties()).toStrictEqual([
1628 { name: DEFAULT_TASK_NAME },
1629 { name: 'factorial' },
1630 { name: 'jsonIntegerSerialization' },
1631 { name: 'fibonacci' }
1634 dynamicThreadPool.setDefaultTaskFunction('fibonacci')
1635 ).resolves.toBe(true)
1636 expect(dynamicThreadPool.listTaskFunctionsProperties()).toStrictEqual([
1637 { name: DEFAULT_TASK_NAME },
1638 { name: 'fibonacci' },
1639 { name: 'jsonIntegerSerialization' },
1640 { name: 'factorial' }
1642 await dynamicThreadPool.destroy()
1645 it('Verify that multiple task functions worker is working', async () => {
1646 const pool = new DynamicClusterPool(
1647 Math.floor(numberOfWorkers / 2),
1649 './tests/worker-files/cluster/testMultipleTaskFunctionsWorker.cjs'
1651 const data = { n: 10 }
1652 const result0 = await pool.execute(data)
1653 expect(result0).toStrictEqual({ ok: 1 })
1654 const result1 = await pool.execute(data, 'jsonIntegerSerialization')
1655 expect(result1).toStrictEqual({ ok: 1 })
1656 const result2 = await pool.execute(data, 'factorial')
1657 expect(result2).toBe(3628800)
1658 const result3 = await pool.execute(data, 'fibonacci')
1659 expect(result3).toBe(55)
1660 expect(pool.info.executingTasks).toBe(0)
1661 expect(pool.info.executedTasks).toBe(4)
1662 for (const workerNode of pool.workerNodes) {
1663 expect(workerNode.info.taskFunctionsProperties).toStrictEqual([
1664 { name: DEFAULT_TASK_NAME },
1665 { name: 'jsonIntegerSerialization' },
1666 { name: 'factorial' },
1667 { name: 'fibonacci' }
1669 expect(workerNode.taskFunctionsUsage.size).toBe(3)
1670 for (const taskFunctionProperties of pool.listTaskFunctionsProperties()) {
1672 workerNode.getTaskFunctionWorkerUsage(taskFunctionProperties.name)
1675 executed: expect.any(Number),
1679 sequentiallyStolen: 0,
1683 history: expect.any(CircularArray)
1686 history: expect.any(CircularArray)
1690 history: expect.any(CircularArray)
1693 history: expect.any(CircularArray)
1698 workerNode.getTaskFunctionWorkerUsage(taskFunctionProperties.name)
1700 ).toBeGreaterThan(0)
1703 workerNode.getTaskFunctionWorkerUsage(DEFAULT_TASK_NAME)
1705 workerNode.getTaskFunctionWorkerUsage(
1706 workerNode.info.taskFunctionsProperties[1].name
1710 await pool.destroy()
1713 it('Verify sendKillMessageToWorker()', async () => {
1714 const pool = new DynamicClusterPool(
1715 Math.floor(numberOfWorkers / 2),
1717 './tests/worker-files/cluster/testWorker.cjs'
1719 const workerNodeKey = 0
1721 pool.sendKillMessageToWorker(workerNodeKey)
1722 ).resolves.toBeUndefined()
1723 await pool.destroy()
1726 it('Verify sendTaskFunctionOperationToWorker()', async () => {
1727 const pool = new DynamicClusterPool(
1728 Math.floor(numberOfWorkers / 2),
1730 './tests/worker-files/cluster/testWorker.cjs'
1732 const workerNodeKey = 0
1734 pool.sendTaskFunctionOperationToWorker(workerNodeKey, {
1735 taskFunctionOperation: 'add',
1736 taskFunctionProperties: { name: 'empty' },
1737 taskFunction: (() => {}).toString()
1739 ).resolves.toBe(true)
1741 pool.workerNodes[workerNodeKey].info.taskFunctionsProperties
1743 { name: DEFAULT_TASK_NAME },
1747 await pool.destroy()
1750 it('Verify sendTaskFunctionOperationToWorkers()', async () => {
1751 const pool = new DynamicClusterPool(
1752 Math.floor(numberOfWorkers / 2),
1754 './tests/worker-files/cluster/testWorker.cjs'
1757 pool.sendTaskFunctionOperationToWorkers({
1758 taskFunctionOperation: 'add',
1759 taskFunctionProperties: { name: 'empty' },
1760 taskFunction: (() => {}).toString()
1762 ).resolves.toBe(true)
1763 for (const workerNode of pool.workerNodes) {
1764 expect(workerNode.info.taskFunctionsProperties).toStrictEqual([
1765 { name: DEFAULT_TASK_NAME },
1770 await pool.destroy()