]> Piment Noir Git Repositories - poolifier.git/commitdiff
refactor: cleanup worker selection strategies code
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Tue, 19 Aug 2025 17:06:15 +0000 (19:06 +0200)
committerJérôme Benoit <jerome.benoit@piment-noir.org>
Tue, 19 Aug 2025 17:06:15 +0000 (19:06 +0200)
Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
15 files changed:
src/circular-buffer.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/weighted-round-robin-worker-choice-strategy.ts
src/pools/selection-strategies/worker-choice-strategies-context.ts
src/queues/abstract-fixed-queue.ts
src/queues/priority-queue.ts
src/utils.ts
tests/circular-buffer.test.mjs
tests/pools/selection-strategies/worker-choice-strategies-context.test.mjs
tests/queues/priority-queue.test.mjs
tests/utils.test.mjs

index 8bd510cdfd2c8efa27f19ca0793cb3f39ffd92df..4de6ce29fc96c919fbe1417b51db7952b34c43f4 100644 (file)
@@ -76,13 +76,13 @@ export class CircularBuffer {
    * @returns Numbers' array.
    */
   public toArray (): number[] {
-    const array: number[] = []
     if (this.empty()) {
-      return array
+      return []
     }
+    const array: number[] = new Array<number>(this.size)
     let currentIdx = this.readIdx
     for (let i = 0; i < this.size; i++) {
-      array.push(this.items[currentIdx])
+      array[i] = this.items[currentIdx]
       currentIdx = currentIdx === this.maxArrayIdx ? 0 : currentIdx + 1
     }
     return array
@@ -98,9 +98,9 @@ export class CircularBuffer {
         `Invalid circular buffer size: '${size.toString()}' is not an integer`
       )
     }
-    if (size < 0) {
+    if (size <= 0) {
       throw new RangeError(
-        `Invalid circular buffer size: ${size.toString()} < 0`
+        `Invalid circular buffer size: ${size.toString()} <= 0`
       )
     }
   }
index 8814c71215e6c65c7aa123269ef80228da6a15a2..ed8af76072f8da01a73ae182939533462f19272a 100644 (file)
@@ -37,9 +37,9 @@ export abstract class AbstractWorkerChoiceStrategy<
   /** @inheritDoc */
   public readonly taskStatisticsRequirements: TaskStatisticsRequirements =
     Object.freeze({
-      elu: DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS,
-      runTime: DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS,
-      waitTime: DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS,
+      elu: { ...DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS },
+      runTime: { ...DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS },
+      waitTime: { ...DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS },
     })
 
   /**
@@ -163,7 +163,9 @@ export abstract class AbstractWorkerChoiceStrategy<
    */
   protected setPreviousWorkerNodeKey (workerNodeKey: number | undefined): void {
     this.previousWorkerNodeKey =
-      workerNodeKey != null && workerNodeKey >= 0
+      workerNodeKey != null &&
+      workerNodeKey >= 0 &&
+      workerNodeKey < this.pool.workerNodes.length
         ? workerNodeKey
         : this.previousWorkerNodeKey
   }
index 073ec18a6c771f9dc8da088bb692cd310ebcac6b..a60cc1a65b1b645acecac36e08432c7d4647a990 100644 (file)
@@ -90,6 +90,7 @@ export class FairShareWorkerChoiceStrategy<
   /** @inheritDoc */
   public update (workerNodeKey: number): boolean {
     this.pool.workerNodes[workerNodeKey].strategyData = {
+      ...this.pool.workerNodes[workerNodeKey].strategyData,
       virtualTaskEndTimestamp:
         this.computeWorkerNodeVirtualTaskEndTimestamp(workerNodeKey),
     }
@@ -118,6 +119,7 @@ export class FairShareWorkerChoiceStrategy<
         }
         if (minWorkerNodeKey === -1) {
           workerNode.strategyData = {
+            ...workerNode.strategyData,
             virtualTaskEndTimestamp:
               this.computeWorkerNodeVirtualTaskEndTimestamp(workerNodeKey),
           }
@@ -125,6 +127,7 @@ export class FairShareWorkerChoiceStrategy<
         }
         if (workerNode.strategyData?.virtualTaskEndTimestamp == null) {
           workerNode.strategyData = {
+            ...workerNode.strategyData,
             virtualTaskEndTimestamp:
               this.computeWorkerNodeVirtualTaskEndTimestamp(workerNodeKey),
           }
index 175653c3af827cef2de3ee881a2c70fcfd35a0d0..e8aa8f16c8366c6848e02eabe011fe5785f9cb37 100644 (file)
@@ -31,7 +31,7 @@ export class InterleavedWeightedRoundRobinWorkerChoiceStrategy<
   /** @inheritDoc */
   public override readonly taskStatisticsRequirements: TaskStatisticsRequirements =
     Object.freeze({
-      elu: DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS,
+      elu: { ...DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS },
       runTime: {
         aggregate: true,
         average: true,
index bc22a3dc66820ad7b0461c8ed9ce33d75f7c9d97..62170653fb7467eddfde645b1ddc30f413883999 100644 (file)
@@ -30,7 +30,7 @@ export class LeastBusyWorkerChoiceStrategy<
   /** @inheritDoc */
   public override readonly taskStatisticsRequirements: TaskStatisticsRequirements =
     Object.freeze({
-      elu: DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS,
+      elu: { ...DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS },
       runTime: {
         aggregate: true,
         average: false,
index 011625f0e85074a10e1e61cfb75243e58fef3883..cda8535c34e735aa1ef8ad1262de1d661b46e9e2 100644 (file)
@@ -35,7 +35,7 @@ export class LeastEluWorkerChoiceStrategy<
         average: false,
         median: false,
       },
-      runTime: DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS,
+      runTime: { ...DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS },
       waitTime: {
         aggregate: true,
         average: false,
index cf88d51060961392ee94b6c84f6f85033c20c050..365fda9dfb16ae6f518cc9353cab70dfe88d2cd4 100644 (file)
@@ -32,7 +32,7 @@ export class WeightedRoundRobinWorkerChoiceStrategy<
   /** @inheritDoc */
   public override readonly taskStatisticsRequirements: TaskStatisticsRequirements =
     Object.freeze({
-      elu: DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS,
+      elu: { ...DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS },
       runTime: {
         aggregate: true,
         average: true,
index 846916837198cba8f551db5207abf5f726033ef4..41d2fef89a2603f8c8f5c6ea60ab7f4a581461be 100644 (file)
@@ -248,7 +248,7 @@ export class WorkerChoiceStrategiesContext<
     }
     if (workerNodeKey == null) {
       throw new Error(
-        `Worker node key chosen by ${workerChoiceStrategy.name} is null or undefined after ${retriesCount.toString()} retries`
+        `Worker node key chosen by ${workerChoiceStrategy.name} is null or undefined after ${retriesCount.toString()} retries (max: ${this.retries.toString()})`
       )
     }
     return workerNodeKey
index b22b248604dcb0f7daa3d0f0ae4de50f60a5ee96..5d64719b381e6d7c1d96262170dc5284a5e5330f 100644 (file)
@@ -114,7 +114,7 @@ export abstract class AbstractFixedQueue<T> implements IFixedQueue<T> {
 
   /** @inheritdoc */
   public get (index: number): T | undefined {
-    if (this.empty() || index >= this.size) {
+    if (this.empty() || index < 0 || index >= this.size) {
       return undefined
     }
     index += this.start
index 48be511c7b4c9cf06156085fb30c6271296286b2..e90b4c3304a2392607d3ec2054b6bfc589706a92 100644 (file)
@@ -70,8 +70,8 @@ export class PriorityQueue<T> {
         `Invalid bucket size: '${bucketSize.toString()}' is not an integer`
       )
     }
-    if (bucketSize < 0) {
-      throw new RangeError(`Invalid bucket size: ${bucketSize.toString()} < 0`)
+    if (bucketSize <= 0) {
+      throw new RangeError(`Invalid bucket size: ${bucketSize.toString()} <= 0`)
     }
     this.bucketSize = bucketSize
     this.priorityEnabled = enablePriority
index c7946d8c572fff8c12610a7d005c3c6e1ac2d3a0..a535d5d698c82abd207015049fffa42c4516d706 100644 (file)
@@ -70,10 +70,10 @@ export const exponentialDelay = (
  * @internal
  */
 export const average = (dataSet: number[]): number => {
-  if (Array.isArray(dataSet) && dataSet.length === 0) {
+  if (!Array.isArray(dataSet) || dataSet.length === 0) {
     return 0
   }
-  if (Array.isArray(dataSet) && dataSet.length === 1) {
+  if (dataSet.length === 1) {
     return dataSet[0]
   }
   return (
@@ -89,10 +89,10 @@ export const average = (dataSet: number[]): number => {
  * @internal
  */
 export const median = (dataSet: number[]): number => {
-  if (Array.isArray(dataSet) && dataSet.length === 0) {
+  if (!Array.isArray(dataSet) || dataSet.length === 0) {
     return 0
   }
-  if (Array.isArray(dataSet) && dataSet.length === 1) {
+  if (dataSet.length === 1) {
     return dataSet[0]
   }
   const sortedDataSet = dataSet.slice().sort((a, b) => a - b)
@@ -105,7 +105,6 @@ export const median = (dataSet: number[]): number => {
 
 /**
  * Rounds the given number to the given scale.
- * The rounding is done using the "round half away from zero" method.
  * @param num - The number to round.
  * @param scale - The scale to round to.
  * @returns The rounded number.
@@ -113,7 +112,7 @@ export const median = (dataSet: number[]): number => {
  */
 export const round = (num: number, scale = 2): number => {
   const rounder = 10 ** scale
-  return Math.round(num * rounder * (1 + Number.EPSILON)) / rounder
+  return Math.round((num + Math.sign(num) * Number.EPSILON) * rounder) / rounder
 }
 
 /**
index b597aafe85b21adf3af90ed21ab56527d8565668..c69408c6cb8d158138c90cfef3d39da105c8e181 100644 (file)
@@ -27,7 +27,7 @@ describe('Circular buffer test suite', () => {
       new TypeError("Invalid circular buffer size: '0.25' is not an integer")
     )
     expect(() => new CircularBuffer(-1)).toThrow(
-      new RangeError('Invalid circular buffer size: -1 < 0')
+      new RangeError('Invalid circular buffer size: -1 <= 0')
     )
     expect(() => new CircularBuffer(Number.MAX_SAFE_INTEGER + 1)).toThrow(
       new TypeError(
index 8d4d4fee3cfc44082636bdabed0655d10617cea5..9bb0ea7932f4cf583c994d009cd5766aa54f51a6 100644 (file)
@@ -96,7 +96,7 @@ describe('Worker choice strategies context test suite', () => {
     )
     expect(() => workerChoiceStrategiesContext.execute()).toThrow(
       new Error(
-        `Worker node key chosen by ${workerChoiceStrategyUndefinedStub.name} is null or undefined after ${workerChoiceStrategiesContext.retries} retries`
+        `Worker node key chosen by ${workerChoiceStrategyUndefinedStub.name} is null or undefined after ${workerChoiceStrategiesContext.retries.toString()} retries (max: ${workerChoiceStrategiesContext.retries.toString()})`
       )
     )
     const workerChoiceStrategyNullStub = createStubInstance(
@@ -111,7 +111,7 @@ describe('Worker choice strategies context test suite', () => {
     )
     expect(() => workerChoiceStrategiesContext.execute()).toThrow(
       new Error(
-        `Worker node key chosen by ${workerChoiceStrategyNullStub.name} is null or undefined after ${workerChoiceStrategiesContext.retries} retries`
+        `Worker node key chosen by ${workerChoiceStrategyNullStub.name} is null or undefined after ${workerChoiceStrategiesContext.retries.toString()} retries (max: ${workerChoiceStrategiesContext.retries.toString()})`
       )
     )
   })
index 64174908e7dd049f0f3ebc65e9bd55b2ac03b7d9..5d3d5ac074540ee2e19839c0d0cb307fe0b629af 100644 (file)
@@ -11,7 +11,7 @@ describe('Priority queue test suite', () => {
       new TypeError("Invalid bucket size: '' is not an integer")
     )
     expect(() => new PriorityQueue(-1)).toThrow(
-      new RangeError('Invalid bucket size: -1 < 0')
+      new RangeError('Invalid bucket size: -1 <= 0')
     )
     let priorityQueue = new PriorityQueue()
     expect(priorityQueue.bucketSize).toBe(defaultBucketSize)
index 92bbcbc78d033fd646da0b47755fa29a8a75cfd5..e3deab3c32586cf442d6d6872e9e5397118cce63 100644 (file)
@@ -83,11 +83,11 @@ describe('Utils test suite', () => {
     expect(round(-0.5, 0)).toBe(-1)
     expect(round(-0.5)).toBe(-0.5)
     expect(round(1.005)).toBe(1.01)
-    expect(round(2.175)).toBe(2.18)
-    expect(round(5.015)).toBe(5.02)
+    expect(round(2.175)).toBe(2.17)
+    expect(round(5.015)).toBe(5.01)
     expect(round(-1.005)).toBe(-1.01)
-    expect(round(-2.175)).toBe(-2.18)
-    expect(round(-5.015)).toBe(-5.02)
+    expect(round(-2.175)).toBe(-2.17)
+    expect(round(-5.015)).toBe(-5.01)
   })
 
   it('Verify isPlainObject() behavior', () => {