fix: fix worker choice strategy retries mechanism on some edge cases
authorJérôme Benoit <jerome.benoit@sap.com>
Sat, 19 Aug 2023 15:40:41 +0000 (17:40 +0200)
committerJérôme Benoit <jerome.benoit@sap.com>
Sat, 19 Aug 2023 15:40:41 +0000 (17:40 +0200)
Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
13 files changed:
CHANGELOG.md
src/pools/abstract-pool.ts
src/pools/selection-strategies/abstract-worker-choice-strategy.ts
src/pools/selection-strategies/fair-share-worker-choice-strategy.ts
src/pools/selection-strategies/interleaved-weighted-round-robin-worker-choice-strategy.ts
src/pools/selection-strategies/least-busy-worker-choice-strategy.ts
src/pools/selection-strategies/least-elu-worker-choice-strategy.ts
src/pools/selection-strategies/least-used-worker-choice-strategy.ts
src/pools/selection-strategies/round-robin-worker-choice-strategy.ts
src/pools/selection-strategies/selection-strategies-types.ts
src/pools/selection-strategies/weighted-round-robin-worker-choice-strategy.ts
tests/pools/abstract/abstract-pool.test.js
tests/pools/selection-strategies/selection-strategies.test.js

index 61d55fbf2eda3183dccf3f2ee39e0a4350494411..3128d3fd4e9a39b6f1062c620af3b735950fc51c 100644 (file)
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
+### Fixed
+
+- Fix worker choice strategy retries mechanism on some edge cases.
+
 ## [2.6.30] - 2023-08-19
 
 ### Fixed
index 183344f84b42a2a55411788d1d414fb82383f9dc..59466274bd8c5740047b91e0c8b9936e3c4b3c8b 100644 (file)
@@ -952,7 +952,7 @@ export abstract class AbstractPool<
     if (this.shallCreateDynamicWorker()) {
       const workerNodeKey = this.createAndSetupDynamicWorkerNode()
       if (
-        this.workerChoiceStrategyContext.getStrategyPolicy().useDynamicWorker
+        this.workerChoiceStrategyContext.getStrategyPolicy().dynamicWorkerUsage
       ) {
         return workerNodeKey
       }
@@ -1062,7 +1062,10 @@ export abstract class AbstractPool<
       workerId: workerInfo.id as number
     })
     workerInfo.dynamic = true
-    if (this.workerChoiceStrategyContext.getStrategyPolicy().useDynamicWorker) {
+    if (
+      this.workerChoiceStrategyContext.getStrategyPolicy().dynamicWorkerReady ||
+      this.workerChoiceStrategyContext.getStrategyPolicy().dynamicWorkerUsage
+    ) {
       workerInfo.ready = true
     }
     this.checkAndEmitDynamicWorkerCreationEvents()
index e6269422abb7b2e89d5021fc82ade5a443268b5d..4ed7dcae2b0dea882d1eb15a1cb0f88310435e7f 100644 (file)
@@ -28,11 +28,12 @@ export abstract class AbstractWorkerChoiceStrategy<
   /**
    * The next worker node key.
    */
-  protected nextWorkerNodeKey: number = 0
+  protected nextWorkerNodeKey: number | undefined = 0
 
   /** @inheritDoc */
   public readonly strategyPolicy: StrategyPolicy = {
-    useDynamicWorker: false
+    dynamicWorkerUsage: false,
+    dynamicWorkerReady: false
   }
 
   /** @inheritDoc */
@@ -94,7 +95,7 @@ export abstract class AbstractWorkerChoiceStrategy<
   public abstract update (workerNodeKey: number): boolean
 
   /** @inheritDoc */
-  public abstract choose (): number
+  public abstract choose (): number | undefined
 
   /** @inheritDoc */
   public abstract remove (workerNodeKey: number): boolean
@@ -183,6 +184,21 @@ export abstract class AbstractWorkerChoiceStrategy<
       : this.pool.workerNodes[workerNodeKey].usage.elu.active?.average ?? 0
   }
 
+  /**
+   * Assign to nextWorkerNodeKey property the chosen worker node key.
+   *
+   * @param chosenWorkerNodeKey - The chosen worker node key.
+   */
+  protected assignChosenWorkerNodeKey (
+    chosenWorkerNodeKey: number | undefined
+  ): void {
+    if (chosenWorkerNodeKey != null) {
+      this.nextWorkerNodeKey = chosenWorkerNodeKey
+    } else {
+      this.nextWorkerNodeKey = undefined
+    }
+  }
+
   protected computeDefaultWorkerWeight (): number {
     let cpusCycleTimeWeight = 0
     for (const cpu of cpus()) {
index 36d1cd4c407c89de4231911bd22cb57b23350781..cfda1b0b92b500c86fa0f61cdb49cb0f90c10370 100644 (file)
@@ -8,6 +8,7 @@ import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy'
 import {
   type IWorkerChoiceStrategy,
   Measurements,
+  type StrategyPolicy,
   type TaskStatisticsRequirements,
   type WorkerChoiceStrategyOptions
 } from './selection-strategies-types'
@@ -27,6 +28,12 @@ export class FairShareWorkerChoiceStrategy<
   >
   extends AbstractWorkerChoiceStrategy<Worker, Data, Response>
   implements IWorkerChoiceStrategy {
+  /** @inheritDoc */
+  public readonly strategyPolicy: StrategyPolicy = {
+    dynamicWorkerUsage: false,
+    dynamicWorkerReady: true
+  }
+
   /** @inheritDoc */
   public readonly taskStatisticsRequirements: TaskStatisticsRequirements = {
     runTime: {
@@ -69,8 +76,10 @@ export class FairShareWorkerChoiceStrategy<
   }
 
   /** @inheritDoc */
-  public choose (): number {
-    return this.fairShareNextWorkerNodeKey()
+  public choose (): number | undefined {
+    const chosenWorkerNodeKey = this.fairShareNextWorkerNodeKey()
+    this.assignChosenWorkerNodeKey(chosenWorkerNodeKey)
+    return this.nextWorkerNodeKey
   }
 
   /** @inheritDoc */
@@ -79,8 +88,9 @@ export class FairShareWorkerChoiceStrategy<
     return true
   }
 
-  private fairShareNextWorkerNodeKey (): number {
+  private fairShareNextWorkerNodeKey (): number | undefined {
     let minWorkerVirtualTaskEndTimestamp = Infinity
+    let chosenWorkerNodeKey: number | undefined
     for (const [workerNodeKey] of this.pool.workerNodes.entries()) {
       if (this.workersVirtualTaskEndTimestamp[workerNodeKey] == null) {
         this.computeWorkerVirtualTaskEndTimestamp(workerNodeKey)
@@ -92,10 +102,10 @@ export class FairShareWorkerChoiceStrategy<
         workerVirtualTaskEndTimestamp < minWorkerVirtualTaskEndTimestamp
       ) {
         minWorkerVirtualTaskEndTimestamp = workerVirtualTaskEndTimestamp
-        this.nextWorkerNodeKey = workerNodeKey
+        chosenWorkerNodeKey = workerNodeKey
       }
     }
-    return this.nextWorkerNodeKey
+    return chosenWorkerNodeKey
   }
 
   /**
index c45d6e0bf606f07a0ce792a5f68c2fbe017e2208..cdbfbee5bb0b73e6436f5aad3d94b45e764d3713 100644 (file)
@@ -24,7 +24,8 @@ export class InterleavedWeightedRoundRobinWorkerChoiceStrategy<
   implements IWorkerChoiceStrategy {
   /** @inheritDoc */
   public readonly strategyPolicy: StrategyPolicy = {
-    useDynamicWorker: true
+    dynamicWorkerUsage: false,
+    dynamicWorkerReady: true
   }
 
   /**
@@ -65,7 +66,7 @@ export class InterleavedWeightedRoundRobinWorkerChoiceStrategy<
   }
 
   /** @inheritDoc */
-  public choose (): number {
+  public choose (): number | undefined {
     let roundId: number | undefined
     let workerNodeId: number | undefined
     for (
@@ -74,7 +75,7 @@ export class InterleavedWeightedRoundRobinWorkerChoiceStrategy<
       roundIndex++
     ) {
       for (
-        let workerNodeKey = this.nextWorkerNodeKey;
+        let workerNodeKey = this.nextWorkerNodeKey ?? 0;
         workerNodeKey < this.pool.workerNodes.length;
         workerNodeKey++
       ) {
@@ -90,15 +91,15 @@ export class InterleavedWeightedRoundRobinWorkerChoiceStrategy<
         }
       }
     }
-    this.roundId = roundId ?? 0
-    this.nextWorkerNodeKey = workerNodeId ?? 0
+    this.roundId = roundId as number
+    this.nextWorkerNodeKey = workerNodeId
     const chosenWorkerNodeKey = this.nextWorkerNodeKey
     if (this.nextWorkerNodeKey === this.pool.workerNodes.length - 1) {
       this.nextWorkerNodeKey = 0
       this.roundId =
         this.roundId === this.roundWeights.length - 1 ? 0 : this.roundId + 1
     } else {
-      this.nextWorkerNodeKey = this.nextWorkerNodeKey + 1
+      this.nextWorkerNodeKey = (this.nextWorkerNodeKey ?? 0) + 1
     }
     return chosenWorkerNodeKey
   }
index b3e9e6baf9b977500c0c7e846283fefdba5357df..9ea64cb5a426bcccc0b3d1ddd8fc38de5612ee13 100644 (file)
@@ -7,6 +7,7 @@ import type { IWorker } from '../worker'
 import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy'
 import type {
   IWorkerChoiceStrategy,
+  StrategyPolicy,
   TaskStatisticsRequirements,
   WorkerChoiceStrategyOptions
 } from './selection-strategies-types'
@@ -25,6 +26,12 @@ export class LeastBusyWorkerChoiceStrategy<
   >
   extends AbstractWorkerChoiceStrategy<Worker, Data, Response>
   implements IWorkerChoiceStrategy {
+  /** @inheritDoc */
+  public readonly strategyPolicy: StrategyPolicy = {
+    dynamicWorkerUsage: false,
+    dynamicWorkerReady: true
+  }
+
   /** @inheritDoc */
   public readonly taskStatisticsRequirements: TaskStatisticsRequirements = {
     runTime: {
@@ -60,8 +67,10 @@ export class LeastBusyWorkerChoiceStrategy<
   }
 
   /** @inheritDoc */
-  public choose (): number {
-    return this.leastBusyNextWorkerNodeKey()
+  public choose (): number | undefined {
+    const chosenWorkerNodeKey = this.leastBusyNextWorkerNodeKey()
+    this.assignChosenWorkerNodeKey(chosenWorkerNodeKey)
+    return this.nextWorkerNodeKey
   }
 
   /** @inheritDoc */
@@ -69,23 +78,24 @@ export class LeastBusyWorkerChoiceStrategy<
     return true
   }
 
-  private leastBusyNextWorkerNodeKey (): number {
+  private leastBusyNextWorkerNodeKey (): number | undefined {
     let minTime = Infinity
+    let chosenWorkerNodeKey: number | undefined
     for (const [workerNodeKey, workerNode] of this.pool.workerNodes.entries()) {
       const workerTime =
         (workerNode.usage.runTime?.aggregate ?? 0) +
         (workerNode.usage.waitTime?.aggregate ?? 0)
       if (this.isWorkerNodeEligible(workerNodeKey) && workerTime === 0) {
-        this.nextWorkerNodeKey = workerNodeKey
+        chosenWorkerNodeKey = workerNodeKey
         break
       } else if (
         this.isWorkerNodeEligible(workerNodeKey) &&
         workerTime < minTime
       ) {
         minTime = workerTime
-        this.nextWorkerNodeKey = workerNodeKey
+        chosenWorkerNodeKey = workerNodeKey
       }
     }
-    return this.nextWorkerNodeKey
+    return chosenWorkerNodeKey
   }
 }
index 03e6ca0448e108b919cdd4912a74a61edc719995..4cf2b3e43253fb1221652f5265fbc958841ff561 100644 (file)
@@ -7,6 +7,7 @@ import type { IWorker } from '../worker'
 import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy'
 import type {
   IWorkerChoiceStrategy,
+  StrategyPolicy,
   TaskStatisticsRequirements,
   WorkerChoiceStrategyOptions
 } from './selection-strategies-types'
@@ -25,6 +26,12 @@ export class LeastEluWorkerChoiceStrategy<
   >
   extends AbstractWorkerChoiceStrategy<Worker, Data, Response>
   implements IWorkerChoiceStrategy {
+  /** @inheritDoc */
+  public readonly strategyPolicy: StrategyPolicy = {
+    dynamicWorkerUsage: false,
+    dynamicWorkerReady: true
+  }
+
   /** @inheritDoc */
   public readonly taskStatisticsRequirements: TaskStatisticsRequirements = {
     runTime: DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS,
@@ -56,8 +63,10 @@ export class LeastEluWorkerChoiceStrategy<
   }
 
   /** @inheritDoc */
-  public choose (): number {
-    return this.leastEluNextWorkerNodeKey()
+  public choose (): number | undefined {
+    const chosenWorkerNodeKey = this.leastEluNextWorkerNodeKey()
+    this.assignChosenWorkerNodeKey(chosenWorkerNodeKey)
+    return this.nextWorkerNodeKey
   }
 
   /** @inheritDoc */
@@ -65,22 +74,23 @@ export class LeastEluWorkerChoiceStrategy<
     return true
   }
 
-  private leastEluNextWorkerNodeKey (): number {
+  private leastEluNextWorkerNodeKey (): number | undefined {
     let minWorkerElu = Infinity
+    let chosenWorkerNodeKey: number | undefined
     for (const [workerNodeKey, workerNode] of this.pool.workerNodes.entries()) {
       const workerUsage = workerNode.usage
       const workerElu = workerUsage.elu?.active?.aggregate ?? 0
       if (this.isWorkerNodeEligible(workerNodeKey) && workerElu === 0) {
-        this.nextWorkerNodeKey = workerNodeKey
+        chosenWorkerNodeKey = workerNodeKey
         break
       } else if (
         this.isWorkerNodeEligible(workerNodeKey) &&
         workerElu < minWorkerElu
       ) {
         minWorkerElu = workerElu
-        this.nextWorkerNodeKey = workerNodeKey
+        chosenWorkerNodeKey = workerNodeKey
       }
     }
-    return this.nextWorkerNodeKey
+    return chosenWorkerNodeKey
   }
 }
index e72efda2eb7db69411cc324e2e003c64fa08f529..1f7033214d6a4735c3064df290acc2ce48cb3e72 100644 (file)
@@ -4,6 +4,7 @@ import type { IWorker } from '../worker'
 import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy'
 import type {
   IWorkerChoiceStrategy,
+  StrategyPolicy,
   WorkerChoiceStrategyOptions
 } from './selection-strategies-types'
 
@@ -21,6 +22,12 @@ export class LeastUsedWorkerChoiceStrategy<
   >
   extends AbstractWorkerChoiceStrategy<Worker, Data, Response>
   implements IWorkerChoiceStrategy {
+  /** @inheritDoc */
+  public readonly strategyPolicy: StrategyPolicy = {
+    dynamicWorkerUsage: false,
+    dynamicWorkerReady: true
+  }
+
   /** @inheritDoc */
   public constructor (
     pool: IPool<Worker, Data, Response>,
@@ -41,8 +48,10 @@ export class LeastUsedWorkerChoiceStrategy<
   }
 
   /** @inheritDoc */
-  public choose (): number {
-    return this.leastUsedNextWorkerNodeKey()
+  public choose (): number | undefined {
+    const chosenWorkerNodeKey = this.leastUsedNextWorkerNodeKey()
+    this.assignChosenWorkerNodeKey(chosenWorkerNodeKey)
+    return this.nextWorkerNodeKey
   }
 
   /** @inheritDoc */
@@ -50,8 +59,9 @@ export class LeastUsedWorkerChoiceStrategy<
     return true
   }
 
-  private leastUsedNextWorkerNodeKey (): number {
+  private leastUsedNextWorkerNodeKey (): number | undefined {
     let minNumberOfTasks = Infinity
+    let chosenWorkerNodeKey: number | undefined
     for (const [workerNodeKey, workerNode] of this.pool.workerNodes.entries()) {
       const workerTaskStatistics = workerNode.usage.tasks
       const workerTasks =
@@ -59,16 +69,16 @@ export class LeastUsedWorkerChoiceStrategy<
         workerTaskStatistics.executing +
         workerTaskStatistics.queued
       if (this.isWorkerNodeEligible(workerNodeKey) && workerTasks === 0) {
-        this.nextWorkerNodeKey = workerNodeKey
+        chosenWorkerNodeKey = workerNodeKey
         break
       } else if (
         this.isWorkerNodeEligible(workerNodeKey) &&
         workerTasks < minNumberOfTasks
       ) {
         minNumberOfTasks = workerTasks
-        this.nextWorkerNodeKey = workerNodeKey
+        chosenWorkerNodeKey = workerNodeKey
       }
     }
-    return this.nextWorkerNodeKey
+    return chosenWorkerNodeKey
   }
 }
index 55fa8ad73d4c7b7713a490cf07265e3634803779..10bdf8714b8bbd23eaf1fab96051ed11e7094bdd 100644 (file)
@@ -24,7 +24,8 @@ export class RoundRobinWorkerChoiceStrategy<
   implements IWorkerChoiceStrategy {
   /** @inheritDoc */
   public readonly strategyPolicy: StrategyPolicy = {
-    useDynamicWorker: true
+    dynamicWorkerUsage: true,
+    dynamicWorkerReady: true
   }
 
   /** @inheritDoc */
@@ -48,11 +49,12 @@ export class RoundRobinWorkerChoiceStrategy<
   }
 
   /** @inheritDoc */
-  public choose (): number {
+  public choose (): number | undefined {
     const chosenWorkerNodeKey = this.nextWorkerNodeKey
-    do {
-      this.roundRobinNextWorkerNodeKey()
-    } while (!this.isWorkerNodeEligible(this.nextWorkerNodeKey))
+    this.roundRobinNextWorkerNodeKey()
+    if (!this.isWorkerNodeEligible(this.nextWorkerNodeKey as number)) {
+      this.nextWorkerNodeKey = undefined
+    }
     return chosenWorkerNodeKey
   }
 
@@ -68,11 +70,11 @@ export class RoundRobinWorkerChoiceStrategy<
     return true
   }
 
-  private roundRobinNextWorkerNodeKey (): number {
+  private roundRobinNextWorkerNodeKey (): number | undefined {
     this.nextWorkerNodeKey =
       this.nextWorkerNodeKey === this.pool.workerNodes.length - 1
         ? 0
-        : this.nextWorkerNodeKey + 1
+        : (this.nextWorkerNodeKey ?? 0) + 1
     return this.nextWorkerNodeKey
   }
 }
index 58f36328f98e78dacc1f87fb42914e008f5b7f67..4377624e97a1d316dba60687870ac6308d44b158 100644 (file)
@@ -153,9 +153,13 @@ export interface TaskStatisticsRequirements {
  */
 export interface StrategyPolicy {
   /**
-   * Expects direct usage of the newly created dynamic worker.
+   * Expects tasks execution on the newly created dynamic worker.
    */
-  readonly useDynamicWorker: boolean
+  readonly dynamicWorkerUsage: boolean
+  /**
+   * Expects the newly created dynamic worker to be flagged as ready.
+   */
+  readonly dynamicWorkerReady: boolean
 }
 
 /**
@@ -186,10 +190,11 @@ export interface IWorkerChoiceStrategy {
   readonly update: (workerNodeKey: number) => boolean
   /**
    * Chooses a worker node in the pool and returns its key.
+   * If the worker node is not eligible, `undefined` is returned.
    *
-   * @returns The worker node key.
+   * @returns The worker node key or `undefined`.
    */
-  readonly choose: () => number
+  readonly choose: () => number | undefined
   /**
    * Removes the worker node key from strategy internals.
    *
index b5c566d51b2e8e0d41261c11acc9956674be9b5b..1aab3d1c6fdfd82ace0147aa27a6ce67be7b2cf8 100644 (file)
@@ -29,7 +29,8 @@ export class WeightedRoundRobinWorkerChoiceStrategy<
   implements IWorkerChoiceStrategy {
   /** @inheritDoc */
   public readonly strategyPolicy: StrategyPolicy = {
-    useDynamicWorker: true
+    dynamicWorkerUsage: false,
+    dynamicWorkerReady: true
   }
 
   /** @inheritDoc */
@@ -75,11 +76,12 @@ export class WeightedRoundRobinWorkerChoiceStrategy<
   }
 
   /** @inheritDoc */
-  public choose (): number {
+  public choose (): number | undefined {
     const chosenWorkerNodeKey = this.nextWorkerNodeKey
-    do {
-      this.weightedRoundRobinNextWorkerNodeKey()
-    } while (!this.isWorkerNodeEligible(this.nextWorkerNodeKey))
+    this.weightedRoundRobinNextWorkerNodeKey()
+    if (!this.isWorkerNodeEligible(this.nextWorkerNodeKey as number)) {
+      this.nextWorkerNodeKey = undefined
+    }
     return chosenWorkerNodeKey
   }
 
@@ -96,19 +98,20 @@ export class WeightedRoundRobinWorkerChoiceStrategy<
     return true
   }
 
-  private weightedRoundRobinNextWorkerNodeKey (): number {
+  private weightedRoundRobinNextWorkerNodeKey (): number | undefined {
     const workerVirtualTaskRunTime = this.workerVirtualTaskRunTime
     const workerWeight =
-      this.opts.weights?.[this.nextWorkerNodeKey] ?? this.defaultWorkerWeight
+      this.opts.weights?.[this.nextWorkerNodeKey ?? 0] ??
+      this.defaultWorkerWeight
     if (workerVirtualTaskRunTime < workerWeight) {
       this.workerVirtualTaskRunTime =
         workerVirtualTaskRunTime +
-        this.getWorkerTaskRunTime(this.nextWorkerNodeKey)
+        this.getWorkerTaskRunTime(this.nextWorkerNodeKey ?? 0)
     } else {
       this.nextWorkerNodeKey =
         this.nextWorkerNodeKey === this.pool.workerNodes.length - 1
           ? 0
-          : this.nextWorkerNodeKey + 1
+          : (this.nextWorkerNodeKey ?? 0) + 1
       this.workerVirtualTaskRunTime = 0
     }
     return this.nextWorkerNodeKey
index 4fd76d03585bd7f76966591ff0f54bfc2300594f..03bcce050f35a2f8907f0974ddf8c1c8c828937d 100644 (file)
@@ -1,4 +1,5 @@
 const { expect } = require('expect')
+const sinon = require('sinon')
 const {
   DynamicClusterPool,
   DynamicThreadPool,
@@ -900,14 +901,20 @@ describe('Abstract pool test suite', () => {
   })
 
   it.skip("Verify that pool event emitter 'backPressure' event can register a callback", async () => {
-    const pool = new DynamicThreadPool(
-      Math.floor(numberOfWorkers / 2),
+    const pool = new FixedThreadPool(
       numberOfWorkers,
       './tests/worker-files/thread/testWorker.js',
       {
         enableTasksQueue: true
       }
     )
+    for (const workerNode of pool.workerNodes) {
+      workerNode.hasBackPressure = sinon
+        .stub()
+        .onFirstCall()
+        .returns(true)
+        .returns(false)
+    }
     const promises = new Set()
     let poolBackPressure = 0
     let poolInfo
@@ -915,10 +922,12 @@ describe('Abstract pool test suite', () => {
       ++poolBackPressure
       poolInfo = info
     })
-    for (let i = 0; i < Math.pow(numberOfWorkers, 2); i++) {
+    for (let i = 0; i < numberOfWorkers * 2; i++) {
       promises.add(pool.execute())
     }
+    // console.log(pool.info.backPressure)
     await Promise.all(promises)
+    // console.log(pool.info.backPressure)
     expect(poolBackPressure).toBe(1)
     expect(poolInfo).toStrictEqual({
       version,
index 6671b57781b1f51c15bdf0717f3c96cafcf0ca76..236d34efc8ca7fea74462c80892561f5dcb5cfe2 100644 (file)
@@ -123,7 +123,8 @@ describe('Selection strategies test suite', () => {
       { workerChoiceStrategy }
     )
     expect(pool.workerChoiceStrategyContext.getStrategyPolicy()).toStrictEqual({
-      useDynamicWorker: true
+      dynamicWorkerUsage: true,
+      dynamicWorkerReady: true
     })
     await pool.destroy()
     pool = new DynamicThreadPool(
@@ -133,7 +134,8 @@ describe('Selection strategies test suite', () => {
       { workerChoiceStrategy }
     )
     expect(pool.workerChoiceStrategyContext.getStrategyPolicy()).toStrictEqual({
-      useDynamicWorker: true
+      dynamicWorkerUsage: true,
+      dynamicWorkerReady: true
     })
     // We need to clean up the resources after our test
     await pool.destroy()
@@ -364,7 +366,8 @@ describe('Selection strategies test suite', () => {
       { workerChoiceStrategy }
     )
     expect(pool.workerChoiceStrategyContext.getStrategyPolicy()).toStrictEqual({
-      useDynamicWorker: false
+      dynamicWorkerUsage: false,
+      dynamicWorkerReady: true
     })
     await pool.destroy()
     pool = new DynamicThreadPool(
@@ -374,7 +377,8 @@ describe('Selection strategies test suite', () => {
       { workerChoiceStrategy }
     )
     expect(pool.workerChoiceStrategyContext.getStrategyPolicy()).toStrictEqual({
-      useDynamicWorker: false
+      dynamicWorkerUsage: false,
+      dynamicWorkerReady: true
     })
     // We need to clean up the resources after our test
     await pool.destroy()
@@ -537,7 +541,8 @@ describe('Selection strategies test suite', () => {
       { workerChoiceStrategy }
     )
     expect(pool.workerChoiceStrategyContext.getStrategyPolicy()).toStrictEqual({
-      useDynamicWorker: false
+      dynamicWorkerUsage: false,
+      dynamicWorkerReady: true
     })
     await pool.destroy()
     pool = new DynamicThreadPool(
@@ -547,7 +552,8 @@ describe('Selection strategies test suite', () => {
       { workerChoiceStrategy }
     )
     expect(pool.workerChoiceStrategyContext.getStrategyPolicy()).toStrictEqual({
-      useDynamicWorker: false
+      dynamicWorkerUsage: false,
+      dynamicWorkerReady: true
     })
     // We need to clean up the resources after our test
     await pool.destroy()
@@ -730,7 +736,8 @@ describe('Selection strategies test suite', () => {
       { workerChoiceStrategy }
     )
     expect(pool.workerChoiceStrategyContext.getStrategyPolicy()).toStrictEqual({
-      useDynamicWorker: false
+      dynamicWorkerUsage: false,
+      dynamicWorkerReady: true
     })
     await pool.destroy()
     pool = new DynamicThreadPool(
@@ -740,7 +747,8 @@ describe('Selection strategies test suite', () => {
       { workerChoiceStrategy }
     )
     expect(pool.workerChoiceStrategyContext.getStrategyPolicy()).toStrictEqual({
-      useDynamicWorker: false
+      dynamicWorkerUsage: false,
+      dynamicWorkerReady: true
     })
     // We need to clean up the resources after our test
     await pool.destroy()
@@ -915,7 +923,8 @@ describe('Selection strategies test suite', () => {
       { workerChoiceStrategy }
     )
     expect(pool.workerChoiceStrategyContext.getStrategyPolicy()).toStrictEqual({
-      useDynamicWorker: false
+      dynamicWorkerUsage: false,
+      dynamicWorkerReady: true
     })
     await pool.destroy()
     pool = new DynamicThreadPool(
@@ -925,7 +934,8 @@ describe('Selection strategies test suite', () => {
       { workerChoiceStrategy }
     )
     expect(pool.workerChoiceStrategyContext.getStrategyPolicy()).toStrictEqual({
-      useDynamicWorker: false
+      dynamicWorkerUsage: false,
+      dynamicWorkerReady: true
     })
     // We need to clean up the resources after our test
     await pool.destroy()
@@ -1277,7 +1287,8 @@ describe('Selection strategies test suite', () => {
       { workerChoiceStrategy }
     )
     expect(pool.workerChoiceStrategyContext.getStrategyPolicy()).toStrictEqual({
-      useDynamicWorker: true
+      dynamicWorkerUsage: false,
+      dynamicWorkerReady: true
     })
     await pool.destroy()
     pool = new DynamicThreadPool(
@@ -1287,7 +1298,8 @@ describe('Selection strategies test suite', () => {
       { workerChoiceStrategy }
     )
     expect(pool.workerChoiceStrategyContext.getStrategyPolicy()).toStrictEqual({
-      useDynamicWorker: true
+      dynamicWorkerUsage: false,
+      dynamicWorkerReady: true
     })
     // We need to clean up the resources after our test
     await pool.destroy()
@@ -1438,13 +1450,9 @@ describe('Selection strategies test suite', () => {
           maxQueued: 0,
           failed: 0
         },
-        runTime: {
-          aggregate: expect.any(Number),
-          maximum: expect.any(Number),
-          minimum: expect.any(Number),
-          average: expect.any(Number),
+        runTime: expect.objectContaining({
           history: expect.any(CircularArray)
-        },
+        }),
         waitTime: {
           history: expect.any(CircularArray)
         },
@@ -1461,8 +1469,16 @@ describe('Selection strategies test suite', () => {
       expect(workerNode.usage.tasks.executed).toBeLessThanOrEqual(
         max * maxMultiplier
       )
-      expect(workerNode.usage.runTime.aggregate).toBeGreaterThan(0)
-      expect(workerNode.usage.runTime.average).toBeGreaterThan(0)
+      if (workerNode.usage.runTime.aggregate == null) {
+        expect(workerNode.usage.runTime.aggregate).toBeUndefined()
+      } else {
+        expect(workerNode.usage.runTime.aggregate).toBeGreaterThan(0)
+      }
+      if (workerNode.usage.runTime.average == null) {
+        expect(workerNode.usage.runTime.average).toBeUndefined()
+      } else {
+        expect(workerNode.usage.runTime.average).toBeGreaterThan(0)
+      }
     }
     expect(
       pool.workerChoiceStrategyContext.workerChoiceStrategies.get(
@@ -1506,13 +1522,9 @@ describe('Selection strategies test suite', () => {
           maxQueued: 0,
           failed: 0
         },
-        runTime: {
-          aggregate: expect.any(Number),
-          maximum: expect.any(Number),
-          minimum: expect.any(Number),
-          median: expect.any(Number),
+        runTime: expect.objectContaining({
           history: expect.any(CircularArray)
-        },
+        }),
         waitTime: {
           history: expect.any(CircularArray)
         },
@@ -1529,8 +1541,16 @@ describe('Selection strategies test suite', () => {
       expect(workerNode.usage.tasks.executed).toBeLessThanOrEqual(
         max * maxMultiplier
       )
-      expect(workerNode.usage.runTime.aggregate).toBeGreaterThan(0)
-      expect(workerNode.usage.runTime.median).toBeGreaterThan(0)
+      if (workerNode.usage.runTime.aggregate == null) {
+        expect(workerNode.usage.runTime.aggregate).toBeUndefined()
+      } else {
+        expect(workerNode.usage.runTime.aggregate).toBeGreaterThan(0)
+      }
+      if (workerNode.usage.runTime.median == null) {
+        expect(workerNode.usage.runTime.median).toBeUndefined()
+      } else {
+        expect(workerNode.usage.runTime.median).toBeGreaterThan(0)
+      }
     }
     expect(
       pool.workerChoiceStrategyContext.workerChoiceStrategies.get(
@@ -1633,7 +1653,8 @@ describe('Selection strategies test suite', () => {
       { workerChoiceStrategy }
     )
     expect(pool.workerChoiceStrategyContext.getStrategyPolicy()).toStrictEqual({
-      useDynamicWorker: true
+      dynamicWorkerUsage: false,
+      dynamicWorkerReady: true
     })
     await pool.destroy()
     pool = new DynamicThreadPool(
@@ -1643,7 +1664,8 @@ describe('Selection strategies test suite', () => {
       { workerChoiceStrategy }
     )
     expect(pool.workerChoiceStrategyContext.getStrategyPolicy()).toStrictEqual({
-      useDynamicWorker: true
+      dynamicWorkerUsage: false,
+      dynamicWorkerReady: true
     })
     // We need to clean up the resources after our test
     await pool.destroy()
@@ -1795,7 +1817,7 @@ describe('Selection strategies test suite', () => {
     for (const workerNode of pool.workerNodes) {
       expect(workerNode.usage).toStrictEqual({
         tasks: {
-          executed: maxMultiplier,
+          executed: expect.any(Number),
           executing: 0,
           queued: 0,
           maxQueued: 0,