fix: fix task wait time computation
[poolifier.git] / src / pools / selection-strategies / abstract-worker-choice-strategy.ts
1 import { cpus } from 'node:os'
2 import { DEFAULT_WORKER_CHOICE_STRATEGY_OPTIONS } from '../../utils'
3 import type { IPool } from '../pool'
4 import type { IWorker } from '../worker'
5 import type {
6 IWorkerChoiceStrategy,
7 TaskStatisticsRequirements,
8 WorkerChoiceStrategyOptions
9 } from './selection-strategies-types'
10
11 /**
12 * Worker choice strategy abstract base class.
13 *
14 * @typeParam Worker - Type of worker which manages the strategy.
15 * @typeParam Data - Type of data sent to the worker. This can only be serializable data.
16 * @typeParam Response - Type of execution response. This can only be serializable data.
17 */
18 export abstract class AbstractWorkerChoiceStrategy<
19 Worker extends IWorker,
20 Data = unknown,
21 Response = unknown
22 > implements IWorkerChoiceStrategy {
23 /**
24 * Toggles finding the last free worker node key.
25 */
26 private toggleFindLastFreeWorkerNodeKey: boolean = false
27 /** @inheritDoc */
28 public readonly taskStatisticsRequirements: TaskStatisticsRequirements = {
29 runTime: false,
30 avgRunTime: false,
31 medRunTime: false,
32 waitTime: false,
33 avgWaitTime: false,
34 medWaitTime: false,
35 elu: false
36 }
37
38 /**
39 * Constructs a worker choice strategy bound to the pool.
40 *
41 * @param pool - The pool instance.
42 * @param opts - The worker choice strategy options.
43 */
44 public constructor (
45 protected readonly pool: IPool<Worker, Data, Response>,
46 protected opts: WorkerChoiceStrategyOptions = DEFAULT_WORKER_CHOICE_STRATEGY_OPTIONS
47 ) {
48 this.choose = this.choose.bind(this)
49 }
50
51 protected setTaskStatistics (opts: WorkerChoiceStrategyOptions): void {
52 if (
53 this.taskStatisticsRequirements.avgRunTime &&
54 opts.medRunTime === true
55 ) {
56 this.taskStatisticsRequirements.avgRunTime = false
57 this.taskStatisticsRequirements.medRunTime = opts.medRunTime as boolean
58 }
59 if (
60 this.taskStatisticsRequirements.medRunTime &&
61 opts.medRunTime === false
62 ) {
63 this.taskStatisticsRequirements.avgRunTime = true
64 this.taskStatisticsRequirements.medRunTime = opts.medRunTime as boolean
65 }
66 if (
67 this.taskStatisticsRequirements.avgWaitTime &&
68 opts.medWaitTime === true
69 ) {
70 this.taskStatisticsRequirements.avgWaitTime = false
71 this.taskStatisticsRequirements.medWaitTime = opts.medWaitTime as boolean
72 }
73 if (
74 this.taskStatisticsRequirements.medWaitTime &&
75 opts.medWaitTime === false
76 ) {
77 this.taskStatisticsRequirements.avgWaitTime = true
78 this.taskStatisticsRequirements.medWaitTime = opts.medWaitTime as boolean
79 }
80 }
81
82 /** @inheritDoc */
83 public abstract reset (): boolean
84
85 /** @inheritDoc */
86 public abstract update (workerNodeKey: number): boolean
87
88 /** @inheritDoc */
89 public abstract choose (): number
90
91 /** @inheritDoc */
92 public abstract remove (workerNodeKey: number): boolean
93
94 /** @inheritDoc */
95 public setOptions (opts: WorkerChoiceStrategyOptions): void {
96 opts = opts ?? DEFAULT_WORKER_CHOICE_STRATEGY_OPTIONS
97 this.setTaskStatistics(opts)
98 this.opts = opts
99 }
100
101 /**
102 * Finds a free worker node key.
103 *
104 * @returns The free worker node key or `-1` if there is no free worker node.
105 */
106 protected findFreeWorkerNodeKey (): number {
107 if (this.toggleFindLastFreeWorkerNodeKey) {
108 this.toggleFindLastFreeWorkerNodeKey = false
109 return this.findLastFreeWorkerNodeKey()
110 }
111 this.toggleFindLastFreeWorkerNodeKey = true
112 return this.findFirstFreeWorkerNodeKey()
113 }
114
115 /**
116 * Gets the worker task runtime.
117 * If the task statistics require `avgRunTime`, the average runtime is returned.
118 * If the task statistics require `medRunTime`, the median runtime is returned.
119 *
120 * @param workerNodeKey - The worker node key.
121 * @returns The worker task runtime.
122 */
123 protected getWorkerTaskRunTime (workerNodeKey: number): number {
124 return this.taskStatisticsRequirements.medRunTime
125 ? this.pool.workerNodes[workerNodeKey].workerUsage.runTime.median
126 : this.pool.workerNodes[workerNodeKey].workerUsage.runTime.average
127 }
128
129 /**
130 * Gets the worker task wait time.
131 * If the task statistics require `avgWaitTime`, the average wait time is returned.
132 * If the task statistics require `medWaitTime`, the median wait time is returned.
133 *
134 * @param workerNodeKey - The worker node key.
135 * @returns The worker task wait time.
136 */
137 protected getWorkerWaitTime (workerNodeKey: number): number {
138 return this.taskStatisticsRequirements.medWaitTime
139 ? this.pool.workerNodes[workerNodeKey].workerUsage.runTime.median
140 : this.pool.workerNodes[workerNodeKey].workerUsage.runTime.average
141 }
142
143 protected computeDefaultWorkerWeight (): number {
144 let cpusCycleTimeWeight = 0
145 for (const cpu of cpus()) {
146 // CPU estimated cycle time
147 const numberOfDigits = cpu.speed.toString().length - 1
148 const cpuCycleTime = 1 / (cpu.speed / Math.pow(10, numberOfDigits))
149 cpusCycleTimeWeight += cpuCycleTime * Math.pow(10, numberOfDigits)
150 }
151 return Math.round(cpusCycleTimeWeight / cpus().length)
152 }
153
154 /**
155 * Finds the first free worker node key based on the number of tasks the worker has applied.
156 *
157 * If a worker is found with `0` executing tasks, it is detected as free and its worker node key is returned.
158 *
159 * If no free worker is found, `-1` is returned.
160 *
161 * @returns A worker node key if there is one, `-1` otherwise.
162 */
163 private findFirstFreeWorkerNodeKey (): number {
164 return this.pool.workerNodes.findIndex(workerNode => {
165 return workerNode.workerUsage.tasks.executing === 0
166 })
167 }
168
169 /**
170 * Finds the last free worker node key based on the number of tasks the worker has applied.
171 *
172 * If a worker is found with `0` executing tasks, it is detected as free and its worker node key is returned.
173 *
174 * If no free worker is found, `-1` is returned.
175 *
176 * @returns A worker node key if there is one, `-1` otherwise.
177 */
178 private findLastFreeWorkerNodeKey (): number {
179 // It requires node >= 18.0.0:
180 // return this.workerNodes.findLastIndex(workerNode => {
181 // return workerNode.workerUsage.tasks.executing === 0
182 // })
183 for (
184 let workerNodeKey = this.pool.workerNodes.length - 1;
185 workerNodeKey >= 0;
186 workerNodeKey--
187 ) {
188 if (
189 this.pool.workerNodes[workerNodeKey].workerUsage.tasks.executing === 0
190 ) {
191 return workerNodeKey
192 }
193 }
194 return -1
195 }
196 }