fix: ensure worker choice is retried at least the pool max size
[poolifier.git] / tests / pools / selection-strategies / worker-choice-strategy-context.test.mjs
1 import { expect } from 'expect'
2 import { createStubInstance, restore, stub } from 'sinon'
3 import {
4 DynamicThreadPool,
5 FixedThreadPool,
6 WorkerChoiceStrategies
7 } from '../../../lib/index.js'
8 import { WorkerChoiceStrategyContext } from '../../../lib/pools/selection-strategies/worker-choice-strategy-context.js'
9 import { RoundRobinWorkerChoiceStrategy } from '../../../lib/pools/selection-strategies/round-robin-worker-choice-strategy.js'
10 import { LeastUsedWorkerChoiceStrategy } from '../../../lib/pools/selection-strategies/least-used-worker-choice-strategy.js'
11 import { LeastBusyWorkerChoiceStrategy } from '../../../lib/pools/selection-strategies/least-busy-worker-choice-strategy.js'
12 import { LeastEluWorkerChoiceStrategy } from '../../../lib/pools/selection-strategies/least-elu-worker-choice-strategy.js'
13 import { FairShareWorkerChoiceStrategy } from '../../../lib/pools/selection-strategies/fair-share-worker-choice-strategy.js'
14 import { WeightedRoundRobinWorkerChoiceStrategy } from '../../../lib/pools/selection-strategies/weighted-round-robin-worker-choice-strategy.js'
15 import { InterleavedWeightedRoundRobinWorkerChoiceStrategy } from '../../../lib/pools/selection-strategies/interleaved-weighted-round-robin-worker-choice-strategy.js'
16
17 describe('Worker choice strategy context test suite', () => {
18 const min = 1
19 const max = 3
20 let fixedPool, dynamicPool
21
22 before(() => {
23 fixedPool = new FixedThreadPool(
24 max,
25 './tests/worker-files/thread/testWorker.mjs'
26 )
27 dynamicPool = new DynamicThreadPool(
28 min,
29 max,
30 './tests/worker-files/thread/testWorker.mjs'
31 )
32 })
33
34 afterEach(() => {
35 restore()
36 })
37
38 after(async () => {
39 await fixedPool.destroy()
40 await dynamicPool.destroy()
41 })
42
43 it('Verify that constructor() initializes the context with all the available worker choice strategies', () => {
44 const workerChoiceStrategyContext = new WorkerChoiceStrategyContext(
45 fixedPool
46 )
47 expect(workerChoiceStrategyContext.workerChoiceStrategies.size).toBe(
48 Object.keys(WorkerChoiceStrategies).length
49 )
50 })
51
52 it('Verify that execute() return the worker node key chosen by the strategy with fixed pool', () => {
53 const workerChoiceStrategyContext = new WorkerChoiceStrategyContext(
54 fixedPool
55 )
56 const workerChoiceStrategyStub = createStubInstance(
57 RoundRobinWorkerChoiceStrategy,
58 {
59 hasPoolWorkerNodesReady: stub().returns(true),
60 choose: stub().returns(0)
61 }
62 )
63 expect(workerChoiceStrategyContext.workerChoiceStrategy).toBe(
64 WorkerChoiceStrategies.ROUND_ROBIN
65 )
66 workerChoiceStrategyContext.workerChoiceStrategies.set(
67 workerChoiceStrategyContext.workerChoiceStrategy,
68 workerChoiceStrategyStub
69 )
70 const chosenWorkerKey = workerChoiceStrategyContext.execute()
71 expect(
72 workerChoiceStrategyContext.workerChoiceStrategies.get(
73 workerChoiceStrategyContext.workerChoiceStrategy
74 ).choose.calledOnce
75 ).toBe(true)
76 expect(chosenWorkerKey).toBe(0)
77 })
78
79 it('Verify that execute() throws error if null or undefined is returned after retries', () => {
80 const workerChoiceStrategyContext = new WorkerChoiceStrategyContext(
81 fixedPool
82 )
83 expect(workerChoiceStrategyContext.workerChoiceStrategy).toBe(
84 WorkerChoiceStrategies.ROUND_ROBIN
85 )
86 const workerChoiceStrategyUndefinedStub = createStubInstance(
87 RoundRobinWorkerChoiceStrategy,
88 {
89 hasPoolWorkerNodesReady: stub().returns(true),
90 choose: stub().returns(undefined)
91 }
92 )
93 workerChoiceStrategyContext.workerChoiceStrategies.set(
94 workerChoiceStrategyContext.workerChoiceStrategy,
95 workerChoiceStrategyUndefinedStub
96 )
97 expect(() => workerChoiceStrategyContext.execute()).toThrow(
98 new Error(
99 `Worker node key chosen is null or undefined after ${fixedPool.info.maxSize} retries`
100 )
101 )
102 const workerChoiceStrategyNullStub = createStubInstance(
103 RoundRobinWorkerChoiceStrategy,
104 {
105 hasPoolWorkerNodesReady: stub().returns(true),
106 choose: stub().returns(null)
107 }
108 )
109 workerChoiceStrategyContext.workerChoiceStrategies.set(
110 workerChoiceStrategyContext.workerChoiceStrategy,
111 workerChoiceStrategyNullStub
112 )
113 expect(() => workerChoiceStrategyContext.execute()).toThrow(
114 new Error(
115 `Worker node key chosen is null or undefined after ${fixedPool.info.maxSize} retries`
116 )
117 )
118 })
119
120 it('Verify that execute() retry until a worker node is ready and chosen', () => {
121 const workerChoiceStrategyContext = new WorkerChoiceStrategyContext(
122 fixedPool
123 )
124 const workerChoiceStrategyStub = createStubInstance(
125 RoundRobinWorkerChoiceStrategy,
126 {
127 hasPoolWorkerNodesReady: stub()
128 .onCall(0)
129 .returns(false)
130 .onCall(1)
131 .returns(false)
132 .onCall(2)
133 .returns(false)
134 .onCall(3)
135 .returns(false)
136 .onCall(4)
137 .returns(false)
138 .returns(true),
139 choose: stub().returns(1)
140 }
141 )
142 expect(workerChoiceStrategyContext.workerChoiceStrategy).toBe(
143 WorkerChoiceStrategies.ROUND_ROBIN
144 )
145 workerChoiceStrategyContext.workerChoiceStrategies.set(
146 workerChoiceStrategyContext.workerChoiceStrategy,
147 workerChoiceStrategyStub
148 )
149 const chosenWorkerKey = workerChoiceStrategyContext.execute()
150 expect(
151 workerChoiceStrategyContext.workerChoiceStrategies.get(
152 workerChoiceStrategyContext.workerChoiceStrategy
153 ).hasPoolWorkerNodesReady.callCount
154 ).toBe(6)
155 expect(
156 workerChoiceStrategyContext.workerChoiceStrategies.get(
157 workerChoiceStrategyContext.workerChoiceStrategy
158 ).choose.callCount
159 ).toBe(1)
160 expect(chosenWorkerKey).toBe(1)
161 })
162
163 it('Verify that execute() throws error if worker choice strategy recursion reach the maximum depth', () => {
164 const workerChoiceStrategyContext = new WorkerChoiceStrategyContext(
165 fixedPool
166 )
167 const workerChoiceStrategyStub = createStubInstance(
168 RoundRobinWorkerChoiceStrategy,
169 {
170 hasPoolWorkerNodesReady: stub().returns(false),
171 choose: stub().returns(0)
172 }
173 )
174 expect(workerChoiceStrategyContext.workerChoiceStrategy).toBe(
175 WorkerChoiceStrategies.ROUND_ROBIN
176 )
177 workerChoiceStrategyContext.workerChoiceStrategies.set(
178 workerChoiceStrategyContext.workerChoiceStrategy,
179 workerChoiceStrategyStub
180 )
181 expect(() => workerChoiceStrategyContext.execute()).toThrow(
182 new RangeError('Maximum call stack size exceeded')
183 )
184 })
185
186 it('Verify that execute() return the worker node key chosen by the strategy with dynamic pool', () => {
187 const workerChoiceStrategyContext = new WorkerChoiceStrategyContext(
188 dynamicPool
189 )
190 const workerChoiceStrategyStub = createStubInstance(
191 RoundRobinWorkerChoiceStrategy,
192 {
193 hasPoolWorkerNodesReady: stub().returns(true),
194 choose: stub().returns(0)
195 }
196 )
197 expect(workerChoiceStrategyContext.workerChoiceStrategy).toBe(
198 WorkerChoiceStrategies.ROUND_ROBIN
199 )
200 workerChoiceStrategyContext.workerChoiceStrategies.set(
201 workerChoiceStrategyContext.workerChoiceStrategy,
202 workerChoiceStrategyStub
203 )
204 const chosenWorkerKey = workerChoiceStrategyContext.execute()
205 expect(
206 workerChoiceStrategyContext.workerChoiceStrategies.get(
207 workerChoiceStrategyContext.workerChoiceStrategy
208 ).choose.calledOnce
209 ).toBe(true)
210 expect(chosenWorkerKey).toBe(0)
211 })
212
213 it('Verify that setWorkerChoiceStrategy() works with ROUND_ROBIN and fixed pool', () => {
214 const workerChoiceStrategy = WorkerChoiceStrategies.ROUND_ROBIN
215 const workerChoiceStrategyContext = new WorkerChoiceStrategyContext(
216 fixedPool
217 )
218 expect(
219 workerChoiceStrategyContext.workerChoiceStrategies.get(
220 workerChoiceStrategy
221 )
222 ).toBeInstanceOf(RoundRobinWorkerChoiceStrategy)
223 expect(workerChoiceStrategyContext.workerChoiceStrategy).toBe(
224 workerChoiceStrategy
225 )
226 workerChoiceStrategyContext.setWorkerChoiceStrategy(workerChoiceStrategy)
227 expect(
228 workerChoiceStrategyContext.workerChoiceStrategies.get(
229 workerChoiceStrategy
230 )
231 ).toBeInstanceOf(RoundRobinWorkerChoiceStrategy)
232 expect(workerChoiceStrategyContext.workerChoiceStrategy).toBe(
233 workerChoiceStrategy
234 )
235 })
236
237 it('Verify that setWorkerChoiceStrategy() works with ROUND_ROBIN and dynamic pool', () => {
238 const workerChoiceStrategy = WorkerChoiceStrategies.ROUND_ROBIN
239 const workerChoiceStrategyContext = new WorkerChoiceStrategyContext(
240 dynamicPool
241 )
242 expect(
243 workerChoiceStrategyContext.workerChoiceStrategies.get(
244 workerChoiceStrategy
245 )
246 ).toBeInstanceOf(RoundRobinWorkerChoiceStrategy)
247 expect(workerChoiceStrategyContext.workerChoiceStrategy).toBe(
248 workerChoiceStrategy
249 )
250 workerChoiceStrategyContext.setWorkerChoiceStrategy(workerChoiceStrategy)
251 expect(
252 workerChoiceStrategyContext.workerChoiceStrategies.get(
253 workerChoiceStrategy
254 )
255 ).toBeInstanceOf(RoundRobinWorkerChoiceStrategy)
256 expect(workerChoiceStrategyContext.workerChoiceStrategy).toBe(
257 workerChoiceStrategy
258 )
259 })
260
261 it('Verify that setWorkerChoiceStrategy() works with LEAST_USED and fixed pool', () => {
262 const workerChoiceStrategy = WorkerChoiceStrategies.LEAST_USED
263 const workerChoiceStrategyContext = new WorkerChoiceStrategyContext(
264 fixedPool
265 )
266 workerChoiceStrategyContext.setWorkerChoiceStrategy(workerChoiceStrategy)
267 expect(
268 workerChoiceStrategyContext.workerChoiceStrategies.get(
269 workerChoiceStrategy
270 )
271 ).toBeInstanceOf(LeastUsedWorkerChoiceStrategy)
272 expect(workerChoiceStrategyContext.workerChoiceStrategy).toBe(
273 workerChoiceStrategy
274 )
275 })
276
277 it('Verify that setWorkerChoiceStrategy() works with LEAST_USED and dynamic pool', () => {
278 const workerChoiceStrategy = WorkerChoiceStrategies.LEAST_USED
279 const workerChoiceStrategyContext = new WorkerChoiceStrategyContext(
280 dynamicPool
281 )
282 workerChoiceStrategyContext.setWorkerChoiceStrategy(workerChoiceStrategy)
283 expect(
284 workerChoiceStrategyContext.workerChoiceStrategies.get(
285 workerChoiceStrategy
286 )
287 ).toBeInstanceOf(LeastUsedWorkerChoiceStrategy)
288 expect(workerChoiceStrategyContext.workerChoiceStrategy).toBe(
289 workerChoiceStrategy
290 )
291 })
292
293 it('Verify that setWorkerChoiceStrategy() works with LEAST_BUSY and fixed pool', () => {
294 const workerChoiceStrategy = WorkerChoiceStrategies.LEAST_BUSY
295 const workerChoiceStrategyContext = new WorkerChoiceStrategyContext(
296 fixedPool
297 )
298 workerChoiceStrategyContext.setWorkerChoiceStrategy(workerChoiceStrategy)
299 expect(
300 workerChoiceStrategyContext.workerChoiceStrategies.get(
301 workerChoiceStrategy
302 )
303 ).toBeInstanceOf(LeastBusyWorkerChoiceStrategy)
304 expect(workerChoiceStrategyContext.workerChoiceStrategy).toBe(
305 workerChoiceStrategy
306 )
307 })
308
309 it('Verify that setWorkerChoiceStrategy() works with LEAST_BUSY and dynamic pool', () => {
310 const workerChoiceStrategy = WorkerChoiceStrategies.LEAST_BUSY
311 const workerChoiceStrategyContext = new WorkerChoiceStrategyContext(
312 dynamicPool
313 )
314 workerChoiceStrategyContext.setWorkerChoiceStrategy(workerChoiceStrategy)
315 expect(
316 workerChoiceStrategyContext.workerChoiceStrategies.get(
317 workerChoiceStrategy
318 )
319 ).toBeInstanceOf(LeastBusyWorkerChoiceStrategy)
320 expect(workerChoiceStrategyContext.workerChoiceStrategy).toBe(
321 workerChoiceStrategy
322 )
323 })
324
325 it('Verify that setWorkerChoiceStrategy() works with LEAST_ELU and fixed pool', () => {
326 const workerChoiceStrategy = WorkerChoiceStrategies.LEAST_ELU
327 const workerChoiceStrategyContext = new WorkerChoiceStrategyContext(
328 fixedPool
329 )
330 workerChoiceStrategyContext.setWorkerChoiceStrategy(workerChoiceStrategy)
331 expect(
332 workerChoiceStrategyContext.workerChoiceStrategies.get(
333 workerChoiceStrategy
334 )
335 ).toBeInstanceOf(LeastEluWorkerChoiceStrategy)
336 expect(workerChoiceStrategyContext.workerChoiceStrategy).toBe(
337 workerChoiceStrategy
338 )
339 })
340
341 it('Verify that setWorkerChoiceStrategy() works with LEAST_ELU and dynamic pool', () => {
342 const workerChoiceStrategy = WorkerChoiceStrategies.LEAST_ELU
343 const workerChoiceStrategyContext = new WorkerChoiceStrategyContext(
344 dynamicPool
345 )
346 workerChoiceStrategyContext.setWorkerChoiceStrategy(workerChoiceStrategy)
347 expect(
348 workerChoiceStrategyContext.workerChoiceStrategies.get(
349 workerChoiceStrategy
350 )
351 ).toBeInstanceOf(LeastEluWorkerChoiceStrategy)
352 expect(workerChoiceStrategyContext.workerChoiceStrategy).toBe(
353 workerChoiceStrategy
354 )
355 })
356
357 it('Verify that setWorkerChoiceStrategy() works with FAIR_SHARE and fixed pool', () => {
358 const workerChoiceStrategy = WorkerChoiceStrategies.FAIR_SHARE
359 const workerChoiceStrategyContext = new WorkerChoiceStrategyContext(
360 fixedPool
361 )
362 workerChoiceStrategyContext.setWorkerChoiceStrategy(workerChoiceStrategy)
363 expect(
364 workerChoiceStrategyContext.workerChoiceStrategies.get(
365 workerChoiceStrategy
366 )
367 ).toBeInstanceOf(FairShareWorkerChoiceStrategy)
368 expect(workerChoiceStrategyContext.workerChoiceStrategy).toBe(
369 workerChoiceStrategy
370 )
371 })
372
373 it('Verify that setWorkerChoiceStrategy() works with FAIR_SHARE and dynamic pool', () => {
374 const workerChoiceStrategy = WorkerChoiceStrategies.FAIR_SHARE
375 const workerChoiceStrategyContext = new WorkerChoiceStrategyContext(
376 dynamicPool
377 )
378 workerChoiceStrategyContext.setWorkerChoiceStrategy(workerChoiceStrategy)
379 expect(
380 workerChoiceStrategyContext.workerChoiceStrategies.get(
381 workerChoiceStrategy
382 )
383 ).toBeInstanceOf(FairShareWorkerChoiceStrategy)
384 expect(workerChoiceStrategyContext.workerChoiceStrategy).toBe(
385 workerChoiceStrategy
386 )
387 })
388
389 it('Verify that setWorkerChoiceStrategy() works with WEIGHTED_ROUND_ROBIN and fixed pool', () => {
390 const workerChoiceStrategy = WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN
391 const workerChoiceStrategyContext = new WorkerChoiceStrategyContext(
392 fixedPool
393 )
394 workerChoiceStrategyContext.setWorkerChoiceStrategy(workerChoiceStrategy)
395 expect(
396 workerChoiceStrategyContext.workerChoiceStrategies.get(
397 workerChoiceStrategy
398 )
399 ).toBeInstanceOf(WeightedRoundRobinWorkerChoiceStrategy)
400 expect(workerChoiceStrategyContext.workerChoiceStrategy).toBe(
401 workerChoiceStrategy
402 )
403 })
404
405 it('Verify that setWorkerChoiceStrategy() works with WEIGHTED_ROUND_ROBIN and dynamic pool', () => {
406 const workerChoiceStrategy = WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN
407 const workerChoiceStrategyContext = new WorkerChoiceStrategyContext(
408 dynamicPool
409 )
410 workerChoiceStrategyContext.setWorkerChoiceStrategy(workerChoiceStrategy)
411 expect(
412 workerChoiceStrategyContext.workerChoiceStrategies.get(
413 workerChoiceStrategy
414 )
415 ).toBeInstanceOf(WeightedRoundRobinWorkerChoiceStrategy)
416 expect(workerChoiceStrategyContext.workerChoiceStrategy).toBe(
417 workerChoiceStrategy
418 )
419 })
420
421 it('Verify that setWorkerChoiceStrategy() works with INTERLEAVED_WEIGHTED_ROUND_ROBIN and fixed pool', () => {
422 const workerChoiceStrategy =
423 WorkerChoiceStrategies.INTERLEAVED_WEIGHTED_ROUND_ROBIN
424 const workerChoiceStrategyContext = new WorkerChoiceStrategyContext(
425 fixedPool
426 )
427 workerChoiceStrategyContext.setWorkerChoiceStrategy(workerChoiceStrategy)
428 expect(
429 workerChoiceStrategyContext.workerChoiceStrategies.get(
430 workerChoiceStrategy
431 )
432 ).toBeInstanceOf(InterleavedWeightedRoundRobinWorkerChoiceStrategy)
433 expect(workerChoiceStrategyContext.workerChoiceStrategy).toBe(
434 workerChoiceStrategy
435 )
436 })
437
438 it('Verify that setWorkerChoiceStrategy() works with INTERLEAVED_WEIGHTED_ROUND_ROBIN and dynamic pool', () => {
439 const workerChoiceStrategy =
440 WorkerChoiceStrategies.INTERLEAVED_WEIGHTED_ROUND_ROBIN
441 const workerChoiceStrategyContext = new WorkerChoiceStrategyContext(
442 dynamicPool
443 )
444 workerChoiceStrategyContext.setWorkerChoiceStrategy(workerChoiceStrategy)
445 expect(
446 workerChoiceStrategyContext.workerChoiceStrategies.get(
447 workerChoiceStrategy
448 )
449 ).toBeInstanceOf(InterleavedWeightedRoundRobinWorkerChoiceStrategy)
450 expect(workerChoiceStrategyContext.workerChoiceStrategy).toBe(
451 workerChoiceStrategy
452 )
453 })
454
455 it('Verify that worker choice strategy options enable median runtime pool statistics', () => {
456 const wwrWorkerChoiceStrategy = WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN
457 let workerChoiceStrategyContext = new WorkerChoiceStrategyContext(
458 fixedPool,
459 wwrWorkerChoiceStrategy,
460 {
461 runTime: { median: true }
462 }
463 )
464 expect(
465 workerChoiceStrategyContext.getTaskStatisticsRequirements().runTime
466 .average
467 ).toBe(false)
468 expect(
469 workerChoiceStrategyContext.getTaskStatisticsRequirements().runTime.median
470 ).toBe(true)
471 workerChoiceStrategyContext = new WorkerChoiceStrategyContext(
472 dynamicPool,
473 wwrWorkerChoiceStrategy,
474 {
475 runTime: { median: true }
476 }
477 )
478 expect(
479 workerChoiceStrategyContext.getTaskStatisticsRequirements().runTime
480 .average
481 ).toBe(false)
482 expect(
483 workerChoiceStrategyContext.getTaskStatisticsRequirements().runTime.median
484 ).toBe(true)
485 const fsWorkerChoiceStrategy = WorkerChoiceStrategies.FAIR_SHARE
486 workerChoiceStrategyContext = new WorkerChoiceStrategyContext(
487 fixedPool,
488 fsWorkerChoiceStrategy,
489 {
490 runTime: { median: true }
491 }
492 )
493 expect(
494 workerChoiceStrategyContext.getTaskStatisticsRequirements().runTime
495 .average
496 ).toBe(false)
497 expect(
498 workerChoiceStrategyContext.getTaskStatisticsRequirements().runTime.median
499 ).toBe(true)
500 workerChoiceStrategyContext = new WorkerChoiceStrategyContext(
501 dynamicPool,
502 fsWorkerChoiceStrategy,
503 {
504 runTime: { median: true }
505 }
506 )
507 expect(
508 workerChoiceStrategyContext.getTaskStatisticsRequirements().runTime
509 .average
510 ).toBe(false)
511 expect(
512 workerChoiceStrategyContext.getTaskStatisticsRequirements().runTime.median
513 ).toBe(true)
514 })
515 })