Merge branch 'master' into elu-strategy
authorJérôme Benoit <jerome.benoit@sap.com>
Thu, 8 Jun 2023 19:31:15 +0000 (21:31 +0200)
committerGitHub <noreply@github.com>
Thu, 8 Jun 2023 19:31:15 +0000 (21:31 +0200)
README.md
src/pools/selection-strategies/least-elu-worker-choice-strategy.ts [new file with mode: 0644]
src/pools/selection-strategies/selection-strategies-types.ts
src/pools/selection-strategies/worker-choice-strategy-context.ts
tests/pools/selection-strategies/selection-strategies.test.js

index 4c7b33a9368048b9f6cc9853affaa687e8e808c0..33c188722210fe375449ba50b39eaa8300da5322 100644 (file)
--- a/README.md
+++ b/README.md
@@ -163,6 +163,7 @@ An object with these properties:
   - `WorkerChoiceStrategies.ROUND_ROBIN`: Submit tasks to worker in a round robin fashion
   - `WorkerChoiceStrategies.LEAST_USED`: Submit tasks to the worker with the minimum number of running and ran tasks
   - `WorkerChoiceStrategies.LEAST_BUSY`: Submit tasks to the worker with the minimum tasks total execution time
+  - `WorkerChoiceStrategies.LEAST_ELU`: Submit tasks to the worker with the minimum event loop utilization (ELU) (experimental)
   - `WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN`: Submit tasks to worker by using a weighted round robin scheduling algorithm based on tasks execution time
   - `WorkerChoiceStrategies.INTERLEAVED_WEIGHTED_ROUND_ROBIN`: Submit tasks to worker by using an interleaved weighted round robin scheduling algorithm based on tasks execution time (experimental)
   - `WorkerChoiceStrategies.FAIR_SHARE`: Submit tasks to worker by using a fair share tasks scheduling algorithm based on tasks execution time
diff --git a/src/pools/selection-strategies/least-elu-worker-choice-strategy.ts b/src/pools/selection-strategies/least-elu-worker-choice-strategy.ts
new file mode 100644 (file)
index 0000000..fbbd48e
--- /dev/null
@@ -0,0 +1,76 @@
+import { DEFAULT_WORKER_CHOICE_STRATEGY_OPTIONS } from '../../utils'
+import type { IPool } from '../pool'
+import type { IWorker } from '../worker'
+import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy'
+import type {
+  IWorkerChoiceStrategy,
+  TaskStatisticsRequirements,
+  WorkerChoiceStrategyOptions
+} from './selection-strategies-types'
+
+/**
+ * Selects the worker with the least ELU.
+ *
+ * @typeParam Worker - Type of worker which manages the strategy.
+ * @typeParam Data - Type of data sent to the worker. This can only be serializable data.
+ * @typeParam Response - Type of execution response. This can only be serializable data.
+ */
+export class LeastEluWorkerChoiceStrategy<
+    Worker extends IWorker,
+    Data = unknown,
+    Response = unknown
+  >
+  extends AbstractWorkerChoiceStrategy<Worker, Data, Response>
+  implements IWorkerChoiceStrategy {
+  /** @inheritDoc */
+  public readonly taskStatisticsRequirements: TaskStatisticsRequirements = {
+    runTime: false,
+    avgRunTime: false,
+    medRunTime: false,
+    waitTime: false,
+    avgWaitTime: false,
+    medWaitTime: false,
+    elu: true
+  }
+
+  /** @inheritDoc */
+  public constructor (
+    pool: IPool<Worker, Data, Response>,
+    opts: WorkerChoiceStrategyOptions = DEFAULT_WORKER_CHOICE_STRATEGY_OPTIONS
+  ) {
+    super(pool, opts)
+    this.setTaskStatistics(this.opts)
+  }
+
+  /** @inheritDoc */
+  public reset (): boolean {
+    return true
+  }
+
+  /** @inheritDoc */
+  public update (): boolean {
+    return true
+  }
+
+  /** @inheritDoc */
+  public choose (): number {
+    let minWorkerElu = Infinity
+    let leastEluWorkerNodeKey!: number
+    for (const [workerNodeKey, workerNode] of this.pool.workerNodes.entries()) {
+      const workerUsage = workerNode.workerUsage
+      const workerElu = workerUsage.elu?.utilization ?? 0
+      if (workerElu === 0) {
+        return workerNodeKey
+      } else if (workerElu < minWorkerElu) {
+        minWorkerElu = workerElu
+        leastEluWorkerNodeKey = workerNodeKey
+      }
+    }
+    return leastEluWorkerNodeKey
+  }
+
+  /** @inheritDoc */
+  public remove (): boolean {
+    return true
+  }
+}
index 5723bd67da6af4b765bcb0e163e48643d48a72e7..6dd532279c3566bc92830a6a8b656ed8355293ee 100644 (file)
@@ -14,6 +14,12 @@ export const WorkerChoiceStrategies = Object.freeze({
    * Least busy worker selection strategy.
    */
   LEAST_BUSY: 'LEAST_BUSY',
+  /**
+   * Least ELU worker selection strategy.
+   *
+   * @experimental
+   */
+  LEAST_ELU: 'LEAST_ELU',
   /**
    * Fair share worker selection strategy.
    */
index f6e132c88514e3012dcf732cb01cde69a4b26410..a0ba25838dc524ea678c97701e20ac7aa1f7b413 100644 (file)
@@ -5,6 +5,7 @@ import { FairShareWorkerChoiceStrategy } from './fair-share-worker-choice-strate
 import { InterleavedWeightedRoundRobinWorkerChoiceStrategy } from './interleaved-weighted-round-robin-worker-choice-strategy'
 import { LeastBusyWorkerChoiceStrategy } from './least-busy-worker-choice-strategy'
 import { LeastUsedWorkerChoiceStrategy } from './least-used-worker-choice-strategy'
+import { LeastEluWorkerChoiceStrategy } from './least-elu-worker-choice-strategy'
 import { RoundRobinWorkerChoiceStrategy } from './round-robin-worker-choice-strategy'
 import type {
   IWorkerChoiceStrategy,
@@ -70,6 +71,13 @@ export class WorkerChoiceStrategyContext<
           opts
         )
       ],
+      [
+        WorkerChoiceStrategies.LEAST_ELU,
+        new (LeastEluWorkerChoiceStrategy.bind(this))<Worker, Data, Response>(
+          pool,
+          opts
+        )
+      ],
       [
         WorkerChoiceStrategies.FAIR_SHARE,
         new (FairShareWorkerChoiceStrategy.bind(this))<Worker, Data, Response>(
index 7a67f6c4b628922db22a760de4d3f5bafc117fea..a1eeaeaf8b4e977ea9e8c962a2d74b25c42da711 100644 (file)
@@ -15,6 +15,7 @@ describe('Selection strategies test suite', () => {
     expect(WorkerChoiceStrategies.ROUND_ROBIN).toBe('ROUND_ROBIN')
     expect(WorkerChoiceStrategies.LEAST_USED).toBe('LEAST_USED')
     expect(WorkerChoiceStrategies.LEAST_BUSY).toBe('LEAST_BUSY')
+    expect(WorkerChoiceStrategies.LEAST_ELU).toBe('LEAST_ELU')
     expect(WorkerChoiceStrategies.FAIR_SHARE).toBe('FAIR_SHARE')
     expect(WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN).toBe(
       'WEIGHTED_ROUND_ROBIN'
@@ -566,6 +567,46 @@ describe('Selection strategies test suite', () => {
     await pool.destroy()
   })
 
+  it('Verify LEAST_ELU strategy default tasks usage statistics requirements', async () => {
+    const workerChoiceStrategy = WorkerChoiceStrategies.LEAST_ELU
+    let pool = new FixedThreadPool(
+      max,
+      './tests/worker-files/thread/testWorker.js',
+      { workerChoiceStrategy }
+    )
+    expect(
+      pool.workerChoiceStrategyContext.getTaskStatisticsRequirements()
+    ).toStrictEqual({
+      runTime: false,
+      avgRunTime: false,
+      medRunTime: false,
+      waitTime: false,
+      avgWaitTime: false,
+      medWaitTime: false,
+      elu: true
+    })
+    await pool.destroy()
+    pool = new DynamicThreadPool(
+      min,
+      max,
+      './tests/worker-files/thread/testWorker.js',
+      { workerChoiceStrategy }
+    )
+    expect(
+      pool.workerChoiceStrategyContext.getTaskStatisticsRequirements()
+    ).toStrictEqual({
+      runTime: false,
+      avgRunTime: false,
+      medRunTime: false,
+      waitTime: false,
+      avgWaitTime: false,
+      medWaitTime: false,
+      elu: true
+    })
+    // We need to clean up the resources after our test
+    await pool.destroy()
+  })
+
   it('Verify FAIR_SHARE strategy default tasks usage statistics requirements', async () => {
     const workerChoiceStrategy = WorkerChoiceStrategies.FAIR_SHARE
     let pool = new FixedThreadPool(