poolifier 2.5.4 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -17
- package/lib/index.d.ts +5 -5
- package/lib/index.js +1 -1
- package/lib/index.mjs +1 -1
- package/lib/pools/abstract-pool.d.ts +10 -17
- package/lib/pools/cluster/fixed.d.ts +1 -3
- package/lib/pools/pool.d.ts +3 -1
- package/lib/pools/selection-strategies/abstract-worker-choice-strategy.d.ts +19 -10
- package/lib/pools/selection-strategies/fair-share-worker-choice-strategy.d.ts +2 -2
- package/lib/pools/selection-strategies/least-busy-worker-choice-strategy.d.ts +2 -2
- package/lib/pools/selection-strategies/least-elu-worker-choice-strategy.d.ts +25 -0
- package/lib/pools/selection-strategies/selection-strategies-types.d.ts +66 -26
- package/lib/pools/selection-strategies/weighted-round-robin-worker-choice-strategy.d.ts +2 -2
- package/lib/pools/selection-strategies/worker-choice-strategy-context.d.ts +4 -4
- package/lib/pools/thread/fixed.d.ts +0 -2
- package/lib/pools/worker.d.ts +52 -30
- package/lib/utility-types.d.ts +37 -17
- package/lib/worker/abstract-worker.d.ts +2 -13
- package/lib/worker/worker-options.d.ts +4 -4
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -150,7 +150,7 @@ Node versions >= 16.14.x are supported.
|
|
|
150
150
|
|
|
151
151
|
## [API](https://poolifier.github.io/poolifier/)
|
|
152
152
|
|
|
153
|
-
###
|
|
153
|
+
### `PoolOptions`
|
|
154
154
|
|
|
155
155
|
An object with these properties:
|
|
156
156
|
|
|
@@ -161,11 +161,12 @@ An object with these properties:
|
|
|
161
161
|
- `workerChoiceStrategy` (optional) - The worker choice strategy to use in this pool:
|
|
162
162
|
|
|
163
163
|
- `WorkerChoiceStrategies.ROUND_ROBIN`: Submit tasks to worker in a round robin fashion
|
|
164
|
-
- `WorkerChoiceStrategies.LEAST_USED`: Submit tasks to the worker with the minimum number of
|
|
165
|
-
- `WorkerChoiceStrategies.LEAST_BUSY`: Submit tasks to the worker with the minimum tasks total execution time
|
|
166
|
-
- `WorkerChoiceStrategies.
|
|
167
|
-
- `WorkerChoiceStrategies.
|
|
168
|
-
- `WorkerChoiceStrategies.
|
|
164
|
+
- `WorkerChoiceStrategies.LEAST_USED`: Submit tasks to the worker with the minimum number of executed, executing and queued tasks
|
|
165
|
+
- `WorkerChoiceStrategies.LEAST_BUSY`: Submit tasks to the worker with the minimum tasks total execution and wait time
|
|
166
|
+
- `WorkerChoiceStrategies.LEAST_ELU`: Submit tasks to the worker with the minimum event loop utilization (ELU) (experimental)
|
|
167
|
+
- `WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN`: Submit tasks to worker by using a [weighted round robin scheduling algorithm](./src/pools/selection-strategies/README.md#weighted-round-robin) based on tasks execution time
|
|
168
|
+
- `WorkerChoiceStrategies.INTERLEAVED_WEIGHTED_ROUND_ROBIN`: Submit tasks to worker by using an [interleaved weighted round robin scheduling algorithm](./src/pools/selection-strategies/README.md#interleaved-weighted-round-robin) based on tasks execution time(experimental)
|
|
169
|
+
- `WorkerChoiceStrategies.FAIR_SHARE`: Submit tasks to worker by using a [fair share scheduling algorithm](./src/pools/selection-strategies/README.md#fair-share) based on tasks execution time (the default) or ELU active time
|
|
169
170
|
|
|
170
171
|
`WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN`, `WorkerChoiceStrategies.INTERLEAVED_WEIGHTED_ROUND_ROBIN` and `WorkerChoiceStrategies.FAIR_SHARE` strategies are targeted to heavy and long tasks.
|
|
171
172
|
Default: `WorkerChoiceStrategies.ROUND_ROBIN`
|
|
@@ -173,10 +174,13 @@ An object with these properties:
|
|
|
173
174
|
- `workerChoiceStrategyOptions` (optional) - The worker choice strategy options object to use in this pool.
|
|
174
175
|
Properties:
|
|
175
176
|
|
|
176
|
-
- `
|
|
177
|
-
- `
|
|
177
|
+
- `measurement` (optional) - The measurement to use in worker choice strategies: `runTime`, `waitTime` or `elu`.
|
|
178
|
+
- `runTime` (optional) - Use the tasks [median](./src/pools/selection-strategies/README.md#median) runtime instead of the tasks average runtime in worker choice strategies.
|
|
179
|
+
- `waitTime` (optional) - Use the tasks [median](./src/pools/selection-strategies/README.md#median) wait time instead of the tasks average wait time in worker choice strategies.
|
|
180
|
+
- `elu` (optional) - Use the tasks [median](./src/pools/selection-strategies/README.md#median) ELU instead of the tasks average ELU in worker choice strategies.
|
|
181
|
+
- `weights` (optional) - The worker weights to use in weighted round robin worker choice strategies: `{ 0: 200, 1: 300, ..., n: 100 }`.
|
|
178
182
|
|
|
179
|
-
Default: `{
|
|
183
|
+
Default: `{ runTime: { median: false }, waitTime: { median: false }, elu: { median: false } }`
|
|
180
184
|
|
|
181
185
|
- `restartWorkerOnError` (optional) - Restart worker on uncaught error in this pool.
|
|
182
186
|
Default: `true`
|
|
@@ -192,15 +196,15 @@ An object with these properties:
|
|
|
192
196
|
|
|
193
197
|
Default: `{ concurrency: 1 }`
|
|
194
198
|
|
|
195
|
-
|
|
199
|
+
#### `ThreadPoolOptions extends PoolOptions`
|
|
196
200
|
|
|
197
|
-
|
|
201
|
+
- `workerOptions` (optional) - An object with the worker options to pass to worker. See [worker_threads](https://nodejs.org/api/worker_threads.html#worker_threads_new_worker_filename_options) for more details.
|
|
198
202
|
|
|
199
|
-
|
|
203
|
+
#### `ClusterPoolOptions extends PoolOptions`
|
|
200
204
|
|
|
201
|
-
|
|
205
|
+
- `env` (optional) - An object with the environment variables to pass to worker. See [cluster](https://nodejs.org/api/cluster.html#cluster_cluster_fork_env) for more details.
|
|
202
206
|
|
|
203
|
-
|
|
207
|
+
- `settings` (optional) - An object with the cluster settings. See [cluster](https://nodejs.org/api/cluster.html#cluster_cluster_settings) for more details.
|
|
204
208
|
|
|
205
209
|
### `pool = new FixedThreadPool/FixedClusterPool(numberOfThreads/numberOfWorkers, filePath, opts)`
|
|
206
210
|
|
|
@@ -238,8 +242,8 @@ This method will call the terminate method on each worker.
|
|
|
238
242
|
Default: `60000`
|
|
239
243
|
|
|
240
244
|
- `killBehavior` (optional) - Dictates if your async unit (worker/process) will be deleted in case that a task is active on it.
|
|
241
|
-
**KillBehaviors.SOFT**: If `currentTime - lastActiveTime` is greater than `maxInactiveTime` but a task is still
|
|
242
|
-
**KillBehaviors.HARD**: If `currentTime - lastActiveTime` is greater than `maxInactiveTime` but a task is still
|
|
245
|
+
**KillBehaviors.SOFT**: If `currentTime - lastActiveTime` is greater than `maxInactiveTime` but a task is still executing, then the worker **won't** be deleted.
|
|
246
|
+
**KillBehaviors.HARD**: If `currentTime - lastActiveTime` is greater than `maxInactiveTime` but a task is still executing, then the worker will be deleted.
|
|
243
247
|
This option only apply to the newly created workers.
|
|
244
248
|
Default: `KillBehaviors.SOFT`
|
|
245
249
|
|
|
@@ -277,7 +281,7 @@ But in general, **always profile your application**.
|
|
|
277
281
|
|
|
278
282
|
To choose your pool consider that with a FixedThreadPool/FixedClusterPool or a DynamicThreadPool/DynamicClusterPool (in this case is important the min parameter passed to the constructor) your application memory footprint will increase.
|
|
279
283
|
Increasing the memory footprint, your application will be ready to accept more tasks, but during idle time your application will consume more memory.
|
|
280
|
-
One good
|
|
284
|
+
One good choice from poolifier team point of view is to profile your application using fixed or dynamic worker pool, and to see your application metrics when you increase/decrease the num of workers.
|
|
281
285
|
For example you could keep the memory footprint low choosing a DynamicThreadPool/DynamicClusterPool with 5 workers, and allow to create new workers until 50/100 when needed, this is the advantage to use the DynamicThreadPool/DynamicClusterPool.
|
|
282
286
|
But in general, **always profile your application**.
|
|
283
287
|
|
package/lib/index.d.ts
CHANGED
|
@@ -3,18 +3,18 @@ export { DynamicClusterPool } from './pools/cluster/dynamic';
|
|
|
3
3
|
export { FixedClusterPool, type ClusterPoolOptions } from './pools/cluster/fixed';
|
|
4
4
|
export { PoolEvents, PoolTypes, WorkerTypes } from './pools/pool';
|
|
5
5
|
export type { IPool, PoolEmitter, PoolEvent, PoolInfo, PoolOptions, PoolType, TasksQueueOptions, WorkerType } from './pools/pool';
|
|
6
|
-
export type { ErrorHandler, ExitHandler, IWorker, MessageHandler, OnlineHandler, Task,
|
|
7
|
-
export { WorkerChoiceStrategies } from './pools/selection-strategies/selection-strategies-types';
|
|
8
|
-
export type { IWorkerChoiceStrategy,
|
|
6
|
+
export type { ErrorHandler, EventLoopUtilizationMeasurementStatistics, ExitHandler, IWorker, MeasurementStatistics, MessageHandler, OnlineHandler, Task, TaskStatistics, WorkerNode, WorkerUsage } from './pools/worker';
|
|
7
|
+
export { Measurements, WorkerChoiceStrategies } from './pools/selection-strategies/selection-strategies-types';
|
|
8
|
+
export type { IWorkerChoiceStrategy, Measurement, MeasurementOptions, MeasurementStatisticsRequirements, TaskStatisticsRequirements, WorkerChoiceStrategy, WorkerChoiceStrategyOptions } from './pools/selection-strategies/selection-strategies-types';
|
|
9
9
|
export type { WorkerChoiceStrategyContext } from './pools/selection-strategies/worker-choice-strategy-context';
|
|
10
10
|
export { DynamicThreadPool } from './pools/thread/dynamic';
|
|
11
11
|
export { FixedThreadPool, type ThreadPoolOptions, type ThreadWorkerWithMessageChannel } from './pools/thread/fixed';
|
|
12
|
-
export type { AbstractWorker
|
|
12
|
+
export type { AbstractWorker } from './worker/abstract-worker';
|
|
13
13
|
export { ClusterWorker } from './worker/cluster-worker';
|
|
14
14
|
export { ThreadWorker } from './worker/thread-worker';
|
|
15
15
|
export { KillBehaviors } from './worker/worker-options';
|
|
16
16
|
export type { KillBehavior, WorkerOptions } from './worker/worker-options';
|
|
17
17
|
export type { TaskFunctions, WorkerAsyncFunction, WorkerFunction, WorkerSyncFunction } from './worker/worker-functions';
|
|
18
|
-
export type { Draft, MessageValue, PromiseResponseWrapper, WorkerStatistics } from './utility-types';
|
|
18
|
+
export type { Draft, MessageValue, PromiseResponseWrapper, TaskError, TaskPerformance, WorkerStatistics } from './utility-types';
|
|
19
19
|
export type { CircularArray } from './circular-array';
|
|
20
20
|
export type { Queue } from './queue';
|
package/lib/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var e=require("node:events"),t=require("node:cluster"),s=require("node:crypto"),r=require("node:perf_hooks"),i=require("node:os"),o=require("node:worker_threads"),n=require("node:async_hooks");const a=Object.freeze({fixed:"fixed",dynamic:"dynamic"}),h=Object.freeze({cluster:"cluster",thread:"thread"});class u extends e{}const k=Object.freeze({full:"full",busy:"busy",error:"error",taskError:"taskError"}),c=Object.freeze((()=>{})),d={medRunTime:!1,medWaitTime:!1},l=e=>{if(Array.isArray(e)&&0===e.length)return 0;if(Array.isArray(e)&&1===e.length)return e[0];const t=e.slice().sort(((e,t)=>e-t));return(t[t.length-1>>1]+t[t.length>>1])/2},m=e=>"object"==typeof e&&null!==e&&e?.constructor===Object&&"[object Object]"===Object.prototype.toString.call(e),p=Object.freeze({SOFT:"SOFT",HARD:"HARD"});class T extends Array{size;constructor(e=1024,...t){super(),this.checkSize(e),this.size=e,arguments.length>1&&this.push(...t)}push(...e){const t=super.push(...e);return t>this.size&&super.splice(0,t-this.size),this.length}unshift(...e){return super.unshift(...e)>this.size&&super.splice(this.size,e.length),this.length}concat(...e){const t=super.concat(e);return t.size=this.size,t.length>t.size&&t.splice(0,t.length-t.size),t}splice(e,t,...s){let r;return arguments.length>=3&&void 0!==t?(r=super.splice(e,t),this.push(...s)):r=2===arguments.length?super.splice(e,t):super.splice(e),r}resize(e){if(this.checkSize(e),0===e)this.length=0;else if(e<this.size)for(let t=e;t<this.size;t++)super.pop();this.size=e}empty(){return 0===this.length}full(){return this.length===this.size}checkSize(e){if(!Number.isSafeInteger(e))throw new TypeError(`Invalid circular array size: ${e} is not a safe integer`);if(e<0)throw new RangeError(`Invalid circular array size: ${e} < 0`)}}class g{items;head;tail;max;constructor(){this.items={},this.head=0,this.tail=0,this.max=0}get size(){return this.tail-this.head}get maxSize(){return this.max}enqueue(e){return this.items[this.tail]=e,this.tail++,this.size>this.max&&(this.max=this.size),this.size}dequeue(){if(this.size<=0)return;const e=this.items[this.head];return delete this.items[this.head],this.head++,this.head===this.tail&&(this.head=0,this.tail=0),e}peek(){if(!(this.size<=0))return this.items[this.head]}}const w=Object.freeze({ROUND_ROBIN:"ROUND_ROBIN",LEAST_USED:"LEAST_USED",LEAST_BUSY:"LEAST_BUSY",FAIR_SHARE:"FAIR_SHARE",WEIGHTED_ROUND_ROBIN:"WEIGHTED_ROUND_ROBIN",INTERLEAVED_WEIGHTED_ROUND_ROBIN:"INTERLEAVED_WEIGHTED_ROUND_ROBIN"});class W{pool;opts;toggleFindLastFreeWorkerNodeKey=!1;taskStatistics={runTime:!1,avgRunTime:!1,medRunTime:!1,waitTime:!1,avgWaitTime:!1,medWaitTime:!1,elu:!1};constructor(e,t=d){this.pool=e,this.opts=t,this.choose=this.choose.bind(this)}setTaskStatistics(e){this.taskStatistics.avgRunTime&&!0===e.medRunTime&&(this.taskStatistics.avgRunTime=!1,this.taskStatistics.medRunTime=e.medRunTime),this.taskStatistics.medRunTime&&!1===e.medRunTime&&(this.taskStatistics.avgRunTime=!0,this.taskStatistics.medRunTime=e.medRunTime),this.taskStatistics.avgWaitTime&&!0===e.medWaitTime&&(this.taskStatistics.avgWaitTime=!1,this.taskStatistics.medWaitTime=e.medWaitTime),this.taskStatistics.medWaitTime&&!1===e.medWaitTime&&(this.taskStatistics.avgWaitTime=!0,this.taskStatistics.medWaitTime=e.medWaitTime)}setOptions(e){e=e??d,this.setTaskStatistics(e),this.opts=e}findFreeWorkerNodeKey(){return this.toggleFindLastFreeWorkerNodeKey?(this.toggleFindLastFreeWorkerNodeKey=!1,this.findLastFreeWorkerNodeKey()):(this.toggleFindLastFreeWorkerNodeKey=!0,this.findFirstFreeWorkerNodeKey())}getWorkerTaskRunTime(e){return this.taskStatistics.medRunTime?this.pool.workerNodes[e].tasksUsage.medRunTime:this.pool.workerNodes[e].tasksUsage.avgRunTime}getWorkerWaitTime(e){return this.taskStatistics.medWaitTime?this.pool.workerNodes[e].tasksUsage.medWaitTime:this.pool.workerNodes[e].tasksUsage.avgWaitTime}computeDefaultWorkerWeight(){let e=0;for(const t of i.cpus()){const s=t.speed.toString().length-1;e+=1/(t.speed/Math.pow(10,s))*Math.pow(10,s)}return Math.round(e/i.cpus().length)}findFirstFreeWorkerNodeKey(){return this.pool.workerNodes.findIndex((e=>0===e.tasksUsage.running))}findLastFreeWorkerNodeKey(){for(let e=this.pool.workerNodes.length-1;e>=0;e--)if(0===this.pool.workerNodes[e].tasksUsage.running)return e;return-1}}class f extends W{taskStatistics={runTime:!0,avgRunTime:!0,medRunTime:!1,waitTime:!1,avgWaitTime:!1,medWaitTime:!1,elu:!1};workersVirtualTaskEndTimestamp=[];constructor(e,t=d){super(e,t),this.setTaskStatistics(this.opts)}reset(){return this.workersVirtualTaskEndTimestamp=[],!0}update(e){return this.computeWorkerVirtualTaskEndTimestamp(e),!0}choose(){let e,t=1/0;for(const[s]of this.pool.workerNodes.entries()){null==this.workersVirtualTaskEndTimestamp[s]&&this.computeWorkerVirtualTaskEndTimestamp(s);const r=this.workersVirtualTaskEndTimestamp[s];r<t&&(t=r,e=s)}return e}remove(e){return this.workersVirtualTaskEndTimestamp.splice(e,1),!0}computeWorkerVirtualTaskEndTimestamp(e){this.workersVirtualTaskEndTimestamp[e]=this.getWorkerVirtualTaskEndTimestamp(e,this.getWorkerVirtualTaskStartTimestamp(e))}getWorkerVirtualTaskEndTimestamp(e,t){return t+this.getWorkerTaskRunTime(e)}getWorkerVirtualTaskStartTimestamp(e){return Math.max(performance.now(),this.workersVirtualTaskEndTimestamp[e]??-1/0)}}class y extends W{currentWorkerNodeId=0;currentRoundId=0;roundWeights;defaultWorkerWeight;constructor(e,t=d){super(e,t),this.setTaskStatistics(this.opts),this.defaultWorkerWeight=this.computeDefaultWorkerWeight(),this.roundWeights=this.getRoundWeights()}reset(){return this.currentWorkerNodeId=0,this.currentRoundId=0,!0}update(){return!0}choose(){let e,t;for(let s=this.currentRoundId;s<this.roundWeights.length;s++)for(let r=this.currentWorkerNodeId;r<this.pool.workerNodes.length;r++){if((this.opts.weights?.[r]??this.defaultWorkerWeight)>=this.roundWeights[s]){e=s,t=r;break}}this.currentRoundId=e??0,this.currentWorkerNodeId=t??0;const s=this.currentWorkerNodeId;return this.currentWorkerNodeId===this.pool.workerNodes.length-1?(this.currentWorkerNodeId=0,this.currentRoundId=this.currentRoundId===this.roundWeights.length-1?0:this.currentRoundId+1):this.currentWorkerNodeId=this.currentWorkerNodeId+1,s}remove(e){return this.currentWorkerNodeId===e&&(0===this.pool.workerNodes.length?this.currentWorkerNodeId=0:this.currentWorkerNodeId>this.pool.workerNodes.length-1&&(this.currentWorkerNodeId=this.pool.workerNodes.length-1,this.currentRoundId=this.currentRoundId===this.roundWeights.length-1?0:this.currentRoundId+1)),!0}setOptions(e){super.setOptions(e),this.roundWeights=this.getRoundWeights()}getRoundWeights(){return null==this.opts.weights?[this.defaultWorkerWeight]:[...new Set(Object.values(this.opts.weights).slice().sort(((e,t)=>e-t)))]}}class S extends W{taskStatistics={runTime:!0,avgRunTime:!1,medRunTime:!1,waitTime:!1,avgWaitTime:!1,medWaitTime:!1,elu:!1};constructor(e,t=d){super(e,t),this.setTaskStatistics(this.opts)}reset(){return!0}update(){return!0}choose(){let e,t=1/0;for(const[s,r]of this.pool.workerNodes.entries()){const i=r.tasksUsage.runTime;if(0===i)return s;i<t&&(t=i,e=s)}return e}remove(){return!0}}class N extends W{constructor(e,t=d){super(e,t),this.setTaskStatistics(this.opts)}reset(){return!0}update(){return!0}choose(){const e=this.findFreeWorkerNodeKey();if(-1!==e)return e;let t,s=1/0;for(const[e,r]of this.pool.workerNodes.entries()){const i=r.tasksUsage,o=i.ran+i.running;if(0===o)return e;o<s&&(s=o,t=e)}return t}remove(){return!0}}class x extends W{nextWorkerNodeId=0;constructor(e,t=d){super(e,t),this.setTaskStatistics(this.opts)}reset(){return this.nextWorkerNodeId=0,!0}update(){return!0}choose(){const e=this.nextWorkerNodeId;return this.nextWorkerNodeId=this.nextWorkerNodeId===this.pool.workerNodes.length-1?0:this.nextWorkerNodeId+1,e}remove(e){return this.nextWorkerNodeId===e&&(0===this.pool.workerNodes.length?this.nextWorkerNodeId=0:this.nextWorkerNodeId>this.pool.workerNodes.length-1&&(this.nextWorkerNodeId=this.pool.workerNodes.length-1)),!0}}class E extends W{taskStatistics={runTime:!0,avgRunTime:!0,medRunTime:!1,waitTime:!1,avgWaitTime:!1,medWaitTime:!1,elu:!1};currentWorkerNodeId=0;defaultWorkerWeight;workerVirtualTaskRunTime=0;constructor(e,t=d){super(e,t),this.setTaskStatistics(this.opts),this.defaultWorkerWeight=this.computeDefaultWorkerWeight()}reset(){return this.currentWorkerNodeId=0,this.workerVirtualTaskRunTime=0,!0}update(){return!0}choose(){const e=this.currentWorkerNodeId,t=this.workerVirtualTaskRunTime;return t<(this.opts.weights?.[e]??this.defaultWorkerWeight)?this.workerVirtualTaskRunTime=t+this.getWorkerTaskRunTime(e):(this.currentWorkerNodeId=this.currentWorkerNodeId===this.pool.workerNodes.length-1?0:this.currentWorkerNodeId+1,this.workerVirtualTaskRunTime=0),e}remove(e){return this.currentWorkerNodeId===e&&(0===this.pool.workerNodes.length?this.currentWorkerNodeId=0:this.currentWorkerNodeId>this.pool.workerNodes.length-1&&(this.currentWorkerNodeId=this.pool.workerNodes.length-1),this.workerVirtualTaskRunTime=0),!0}}class R{workerChoiceStrategy;workerChoiceStrategies;constructor(e,t=w.ROUND_ROBIN,s=d){this.workerChoiceStrategy=t,this.execute=this.execute.bind(this),this.workerChoiceStrategies=new Map([[w.ROUND_ROBIN,new(x.bind(this))(e,s)],[w.LEAST_USED,new(N.bind(this))(e,s)],[w.LEAST_BUSY,new(S.bind(this))(e,s)],[w.FAIR_SHARE,new(f.bind(this))(e,s)],[w.WEIGHTED_ROUND_ROBIN,new(E.bind(this))(e,s)],[w.INTERLEAVED_WEIGHTED_ROUND_ROBIN,new(y.bind(this))(e,s)]])}getTaskStatistics(){return this.workerChoiceStrategies.get(this.workerChoiceStrategy).taskStatistics}setWorkerChoiceStrategy(e){this.workerChoiceStrategy!==e&&(this.workerChoiceStrategy=e),this.workerChoiceStrategies.get(this.workerChoiceStrategy)?.reset()}update(e){return this.workerChoiceStrategies.get(this.workerChoiceStrategy).update(e)}execute(){const e=this.workerChoiceStrategies.get(this.workerChoiceStrategy).choose();if(null==e)throw new Error("Worker node key chosen is null or undefined");return e}remove(e){return this.workerChoiceStrategies.get(this.workerChoiceStrategy).remove(e)}setOptions(e){for(const t of this.workerChoiceStrategies.values())t.setOptions(e)}}class v{numberOfWorkers;filePath;opts;workerNodes=[];emitter;promiseResponseMap=new Map;workerChoiceStrategyContext;constructor(e,t,s){if(this.numberOfWorkers=e,this.filePath=t,this.opts=s,!this.isMain())throw new Error("Cannot start a pool from a worker!");this.checkNumberOfWorkers(this.numberOfWorkers),this.checkFilePath(this.filePath),this.checkPoolOptions(this.opts),this.chooseWorkerNode=this.chooseWorkerNode.bind(this),this.executeTask=this.executeTask.bind(this),this.enqueueTask=this.enqueueTask.bind(this),this.checkAndEmitEvents=this.checkAndEmitEvents.bind(this),!0===this.opts.enableEvents&&(this.emitter=new u),this.workerChoiceStrategyContext=new R(this,this.opts.workerChoiceStrategy,this.opts.workerChoiceStrategyOptions),this.setupHook();for(let e=1;e<=this.numberOfWorkers;e++)this.createAndSetupWorker()}checkFilePath(e){if(null==e||"string"==typeof e&&0===e.trim().length)throw new Error("Please specify a file with a worker implementation")}checkNumberOfWorkers(e){if(null==e)throw new Error("Cannot instantiate a pool without specifying the number of workers");if(!Number.isSafeInteger(e))throw new TypeError("Cannot instantiate a pool with a non safe integer number of workers");if(e<0)throw new RangeError("Cannot instantiate a pool with a negative number of workers");if(this.type===a.fixed&&0===e)throw new Error("Cannot instantiate a fixed pool with no worker")}checkPoolOptions(e){if(!m(e))throw new TypeError("Invalid pool options: must be a plain object");this.opts.workerChoiceStrategy=e.workerChoiceStrategy??w.ROUND_ROBIN,this.checkValidWorkerChoiceStrategy(this.opts.workerChoiceStrategy),this.opts.workerChoiceStrategyOptions=e.workerChoiceStrategyOptions??d,this.checkValidWorkerChoiceStrategyOptions(this.opts.workerChoiceStrategyOptions),this.opts.restartWorkerOnError=e.restartWorkerOnError??!0,this.opts.enableEvents=e.enableEvents??!0,this.opts.enableTasksQueue=e.enableTasksQueue??!1,this.opts.enableTasksQueue&&(this.checkValidTasksQueueOptions(e.tasksQueueOptions),this.opts.tasksQueueOptions=this.buildTasksQueueOptions(e.tasksQueueOptions))}checkValidWorkerChoiceStrategy(e){if(!Object.values(w).includes(e))throw new Error(`Invalid worker choice strategy '${e}'`)}checkValidWorkerChoiceStrategyOptions(e){if(!m(e))throw new TypeError("Invalid worker choice strategy options: must be a plain object");if(null!=e.weights&&Object.keys(e.weights).length!==this.maxSize)throw new Error("Invalid worker choice strategy options: must have a weight for each worker node")}checkValidTasksQueueOptions(e){if(null!=e&&!m(e))throw new TypeError("Invalid tasks queue options: must be a plain object");if(e?.concurrency<=0)throw new Error(`Invalid worker tasks concurrency '${e.concurrency}'`)}get info(){return{type:this.type,worker:this.worker,minSize:this.minSize,maxSize:this.maxSize,workerNodes:this.workerNodes.length,idleWorkerNodes:this.workerNodes.reduce(((e,t)=>0===t.tasksUsage.running?e+1:e),0),busyWorkerNodes:this.workerNodes.reduce(((e,t)=>t.tasksUsage.running>0?e+1:e),0),runningTasks:this.workerNodes.reduce(((e,t)=>e+t.tasksUsage.running),0),queuedTasks:this.workerNodes.reduce(((e,t)=>e+t.tasksQueue.size),0),maxQueuedTasks:this.workerNodes.reduce(((e,t)=>e+t.tasksQueue.maxSize),0)}}getWorkerNodeKey(e){return this.workerNodes.findIndex((t=>t.worker===e))}setWorkerChoiceStrategy(e,t){this.checkValidWorkerChoiceStrategy(e),this.opts.workerChoiceStrategy=e,this.workerChoiceStrategyContext.setWorkerChoiceStrategy(this.opts.workerChoiceStrategy),null!=t&&this.setWorkerChoiceStrategyOptions(t);for(const e of this.workerNodes)this.setWorkerNodeTasksUsage(e,{ran:0,running:0,runTime:0,runTimeHistory:new T,avgRunTime:0,medRunTime:0,waitTime:0,waitTimeHistory:new T,avgWaitTime:0,medWaitTime:0,error:0,elu:void 0}),this.setWorkerStatistics(e.worker)}setWorkerChoiceStrategyOptions(e){this.checkValidWorkerChoiceStrategyOptions(e),this.opts.workerChoiceStrategyOptions=e,this.workerChoiceStrategyContext.setOptions(this.opts.workerChoiceStrategyOptions)}enableTasksQueue(e,t){!0!==this.opts.enableTasksQueue||e||this.flushTasksQueues(),this.opts.enableTasksQueue=e,this.setTasksQueueOptions(t)}setTasksQueueOptions(e){!0===this.opts.enableTasksQueue?(this.checkValidTasksQueueOptions(e),this.opts.tasksQueueOptions=this.buildTasksQueueOptions(e)):null!=this.opts.tasksQueueOptions&&delete this.opts.tasksQueueOptions}buildTasksQueueOptions(e){return{concurrency:e?.concurrency??1}}get full(){return this.workerNodes.length>=this.maxSize}internalBusy(){return-1===this.workerNodes.findIndex((e=>0===e.tasksUsage.running))}async execute(e,t){const i=r.performance.now(),o=this.chooseWorkerNode(),n={name:t,data:e??{},timestamp:i,id:s.randomUUID()},a=new Promise(((e,t)=>{this.promiseResponseMap.set(n.id,{resolve:e,reject:t,worker:this.workerNodes[o].worker})}));return!0===this.opts.enableTasksQueue&&(this.busy||this.workerNodes[o].tasksUsage.running>=this.opts.tasksQueueOptions.concurrency)?this.enqueueTask(o,n):this.executeTask(o,n),this.workerChoiceStrategyContext.update(o),this.checkAndEmitEvents(),a}async destroy(){await Promise.all(this.workerNodes.map((async(e,t)=>{this.flushTasksQueue(t),await this.destroyWorker(e.worker)})))}setupHook(){}beforeTaskExecutionHook(e){++this.workerNodes[e].tasksUsage.running}afterTaskExecutionHook(e,t){const s=this.workerNodes[this.getWorkerNodeKey(e)].tasksUsage;--s.running,++s.ran,null!=t.error&&++s.error,this.updateRunTimeTasksUsage(s,t),this.updateWaitTimeTasksUsage(s,t),this.updateEluTasksUsage(s,t)}updateRunTimeTasksUsage(e,t){this.workerChoiceStrategyContext.getTaskStatistics().runTime&&(e.runTime+=t.runTime??0,this.workerChoiceStrategyContext.getTaskStatistics().avgRunTime&&0!==e.ran&&(e.avgRunTime=e.runTime/e.ran),this.workerChoiceStrategyContext.getTaskStatistics().medRunTime&&null!=t.runTime&&(e.runTimeHistory.push(t.runTime),e.medRunTime=l(e.runTimeHistory)))}updateWaitTimeTasksUsage(e,t){this.workerChoiceStrategyContext.getTaskStatistics().waitTime&&(e.waitTime+=t.waitTime??0,this.workerChoiceStrategyContext.getTaskStatistics().avgWaitTime&&0!==e.ran&&(e.avgWaitTime=e.waitTime/e.ran),this.workerChoiceStrategyContext.getTaskStatistics().medWaitTime&&null!=t.waitTime&&(e.waitTimeHistory.push(t.waitTime),e.medWaitTime=l(e.waitTimeHistory)))}updateEluTasksUsage(e,t){this.workerChoiceStrategyContext.getTaskStatistics().elu&&(null!=e.elu&&null!=t.elu?e.elu={idle:e.elu.idle+t.elu.idle,active:e.elu.active+t.elu.active,utilization:(e.elu.utilization+t.elu.utilization)/2}:null!=t.elu&&(e.elu=t.elu))}chooseWorkerNode(){let e;if(this.type===a.dynamic&&!this.full&&this.internalBusy()){const t=this.createAndSetupWorker();this.registerWorkerMessageListener(t,(e=>{const s=this.getWorkerNodeKey(t);var r;r=p.HARD,(e.kill===r||null!=e.kill&&0===this.workerNodes[s].tasksUsage.running)&&(this.flushTasksQueue(s),this.destroyWorker(t))})),e=this.getWorkerNodeKey(t)}else e=this.workerChoiceStrategyContext.execute();return e}createAndSetupWorker(){const e=this.createWorker();return e.on("message",this.opts.messageHandler??c),e.on("error",this.opts.errorHandler??c),e.on("error",(e=>{null!=this.emitter&&this.emitter.emit(k.error,e)})),e.on("error",(()=>{!0===this.opts.restartWorkerOnError&&this.createAndSetupWorker()})),e.on("online",this.opts.onlineHandler??c),e.on("exit",this.opts.exitHandler??c),e.once("exit",(()=>{this.removeWorkerNode(e)})),this.pushWorkerNode(e),this.setWorkerStatistics(e),this.afterWorkerSetup(e),e}workerListener(){return e=>{if(null!=e.id){const t=this.promiseResponseMap.get(e.id);if(null!=t){null!=e.error?(t.reject(e.error),null!=this.emitter&&this.emitter.emit(k.taskError,{error:e.error,errorData:e.errorData})):t.resolve(e.data),this.afterTaskExecutionHook(t.worker,e),this.promiseResponseMap.delete(e.id);const s=this.getWorkerNodeKey(t.worker);!0===this.opts.enableTasksQueue&&this.tasksQueueSize(s)>0&&this.executeTask(s,this.dequeueTask(s))}}}}checkAndEmitEvents(){null!=this.emitter&&(this.busy&&this.emitter?.emit(k.busy,this.info),this.type===a.dynamic&&this.full&&this.emitter?.emit(k.full,this.info))}setWorkerNodeTasksUsage(e,t){e.tasksUsage=t}pushWorkerNode(e){return this.workerNodes.push({worker:e,tasksUsage:{ran:0,running:0,runTime:0,runTimeHistory:new T,avgRunTime:0,medRunTime:0,waitTime:0,waitTimeHistory:new T,avgWaitTime:0,medWaitTime:0,error:0,elu:void 0},tasksQueue:new g})}setWorkerNode(e,t,s,r){this.workerNodes[e]={worker:t,tasksUsage:s,tasksQueue:r}}removeWorkerNode(e){const t=this.getWorkerNodeKey(e);-1!==t&&(this.workerNodes.splice(t,1),this.workerChoiceStrategyContext.remove(t))}executeTask(e,t){this.beforeTaskExecutionHook(e),this.sendToWorker(this.workerNodes[e].worker,t)}enqueueTask(e,t){return this.workerNodes[e].tasksQueue.enqueue(t)}dequeueTask(e){return this.workerNodes[e].tasksQueue.dequeue()}tasksQueueSize(e){return this.workerNodes[e].tasksQueue.size}flushTasksQueue(e){if(this.tasksQueueSize(e)>0)for(let t=0;t<this.tasksQueueSize(e);t++)this.executeTask(e,this.dequeueTask(e))}flushTasksQueues(){for(const[e]of this.workerNodes.entries())this.flushTasksQueue(e)}setWorkerStatistics(e){this.sendToWorker(e,{statistics:{runTime:this.workerChoiceStrategyContext.getTaskStatistics().runTime,waitTime:this.workerChoiceStrategyContext.getTaskStatistics().waitTime,elu:this.workerChoiceStrategyContext.getTaskStatistics().elu}})}}class I extends v{opts;constructor(e,t,s={}){super(e,t,s),this.opts=s}setupHook(){t.setupPrimary({...this.opts.settings,exec:this.filePath})}isMain(){return t.isPrimary}destroyWorker(e){this.sendToWorker(e,{kill:1}),e.kill()}sendToWorker(e,t){e.send(t)}registerWorkerMessageListener(e,t){e.on("message",t)}createWorker(){return t.fork(this.opts.env)}afterWorkerSetup(e){this.registerWorkerMessageListener(e,super.workerListener())}get type(){return a.fixed}get worker(){return h.cluster}get minSize(){return this.numberOfWorkers}get maxSize(){return this.numberOfWorkers}get busy(){return this.internalBusy()}}class b extends v{opts;constructor(e,t,s={}){super(e,t,s),this.opts=s}isMain(){return o.isMainThread}async destroyWorker(e){this.sendToWorker(e,{kill:1}),await e.terminate()}sendToWorker(e,t){e.postMessage(t)}registerWorkerMessageListener(e,t){e.port2?.on("message",t)}createWorker(){return new o.Worker(this.filePath,{env:o.SHARE_ENV,...this.opts.workerOptions})}afterWorkerSetup(e){const{port1:t,port2:s}=new o.MessageChannel;e.postMessage({parent:t},[t]),e.port1=t,e.port2=s,this.registerWorkerMessageListener(e,super.workerListener())}get type(){return a.fixed}get worker(){return h.thread}get minSize(){return this.numberOfWorkers}get maxSize(){return this.numberOfWorkers}get busy(){return this.internalBusy()}}const O="default",C=6e4,z=p.SOFT;class U extends n.AsyncResource{isMain;mainWorker;opts;taskFunctions;lastTaskTimestamp;statistics;aliveInterval;constructor(e,t,s,i,o={killBehavior:z,maxInactiveTime:C}){super(e),this.isMain=t,this.mainWorker=i,this.opts=o,this.checkWorkerOptions(this.opts),this.checkTaskFunctions(s),this.isMain||(this.lastTaskTimestamp=r.performance.now(),this.aliveInterval=setInterval(this.checkAlive.bind(this),(this.opts.maxInactiveTime??C)/2),this.checkAlive.bind(this)()),this.mainWorker?.on("message",this.messageListener.bind(this))}checkWorkerOptions(e){this.opts.killBehavior=e.killBehavior??z,this.opts.maxInactiveTime=e.maxInactiveTime??C,delete this.opts.async}checkTaskFunctions(e){if(null==e)throw new Error("taskFunctions parameter is mandatory");if(this.taskFunctions=new Map,"function"==typeof e)this.taskFunctions.set(O,e.bind(this));else{if(!m(e))throw new TypeError("taskFunctions parameter is not a function or a plain object");{let t=!0;for(const[s,r]of Object.entries(e)){if("function"!=typeof r)throw new TypeError("A taskFunctions parameter object value is not a function");this.taskFunctions.set(s,r.bind(this)),t&&(this.taskFunctions.set(O,r.bind(this)),t=!1)}if(t)throw new Error("taskFunctions parameter object is empty")}}}messageListener(e){if(null!=e.id&&null!=e.data){const t=this.getTaskFunction(e.name);"AsyncFunction"===t?.constructor.name?this.runInAsyncScope(this.runAsync.bind(this),this,t,e):this.runInAsyncScope(this.runSync.bind(this),this,t,e)}else null!=e.parent?this.mainWorker=e.parent:null!=e.kill?(null!=this.aliveInterval&&clearInterval(this.aliveInterval),this.emitDestroy()):null!=e.statistics&&(this.statistics=e.statistics)}getMainWorker(){if(null==this.mainWorker)throw new Error("Main worker was not set");return this.mainWorker}checkAlive(){r.performance.now()-this.lastTaskTimestamp>(this.opts.maxInactiveTime??C)&&this.sendToMainWorker({kill:this.opts.killBehavior})}handleError(e){return e}runSync(e,t){try{const s=this.beginTaskPerformance(t),r=e(t.data),{runTime:i,waitTime:o,elu:n}=this.endTaskPerformance(s);this.sendToMainWorker({data:r,runTime:i,waitTime:o,elu:n,id:t.id})}catch(e){const s=this.handleError(e);this.sendToMainWorker({error:s,errorData:t.data,id:t.id})}finally{!this.isMain&&(this.lastTaskTimestamp=r.performance.now())}}runAsync(e,t){const s=this.beginTaskPerformance(t);e(t.data).then((e=>{const{runTime:r,waitTime:i,elu:o}=this.endTaskPerformance(s);return this.sendToMainWorker({data:e,runTime:r,waitTime:i,elu:o,id:t.id}),null})).catch((e=>{const s=this.handleError(e);this.sendToMainWorker({error:s,errorData:t.data,id:t.id})})).finally((()=>{!this.isMain&&(this.lastTaskTimestamp=r.performance.now())})).catch(c)}getTaskFunction(e){e=e??O;const t=this.taskFunctions.get(e);if(null==t)throw new Error(`Task function '${e}' not found`);return t}beginTaskPerformance(e){const t=r.performance.now();return{timestamp:t,...this.statistics.waitTime&&{waitTime:t-(e.timestamp??t)},...this.statistics.elu&&{elu:r.performance.eventLoopUtilization()}}}endTaskPerformance(e){return{...e,...this.statistics.runTime&&{runTime:r.performance.now()-e.timestamp},...this.statistics.elu&&{elu:r.performance.eventLoopUtilization(e.elu)}}}}exports.ClusterWorker=class extends U{constructor(e,s={}){super("worker-cluster-pool:poolifier",t.isPrimary,e,t.worker,s)}sendToMainWorker(e){this.getMainWorker().send(e)}handleError(e){return e instanceof Error?e.message:e}},exports.DynamicClusterPool=class extends I{max;constructor(e,t,s,r={}){super(e,s,r),this.max=t}get type(){return a.dynamic}get maxSize(){return this.max}get busy(){return this.full&&this.internalBusy()}},exports.DynamicThreadPool=class extends b{max;constructor(e,t,s,r={}){super(e,s,r),this.max=t}get type(){return a.dynamic}get maxSize(){return this.max}get busy(){return this.full&&this.internalBusy()}},exports.FixedClusterPool=I,exports.FixedThreadPool=b,exports.KillBehaviors=p,exports.PoolEvents=k,exports.PoolTypes=a,exports.ThreadWorker=class extends U{constructor(e,t={}){super("worker-thread-pool:poolifier",o.isMainThread,e,o.parentPort,t)}sendToMainWorker(e){this.getMainWorker().postMessage(e)}},exports.WorkerChoiceStrategies=w,exports.WorkerTypes=h;
|
|
1
|
+
"use strict";var e=require("node:events"),t=require("node:cluster"),r=require("node:crypto"),s=require("node:perf_hooks"),i=require("node:os"),o=require("node:worker_threads"),a=require("node:async_hooks");const n=Object.freeze({fixed:"fixed",dynamic:"dynamic"}),h=Object.freeze({cluster:"cluster",thread:"thread"});class u extends e{}const k=Object.freeze({full:"full",busy:"busy",error:"error",taskError:"taskError"}),c=Object.freeze((()=>{})),d={runTime:{median:!1},waitTime:{median:!1},elu:{median:!1}},l=e=>{if(Array.isArray(e)&&0===e.length)return 0;if(Array.isArray(e)&&1===e.length)return e[0];const t=e.slice().sort(((e,t)=>e-t));return(t[t.length-1>>1]+t[t.length>>1])/2},g=e=>"object"==typeof e&&null!==e&&e?.constructor===Object&&"[object Object]"===Object.prototype.toString.call(e),m=Object.freeze({SOFT:"SOFT",HARD:"HARD"});class p extends Array{size;constructor(e=1024,...t){super(),this.checkSize(e),this.size=e,arguments.length>1&&this.push(...t)}push(...e){const t=super.push(...e);return t>this.size&&super.splice(0,t-this.size),this.length}unshift(...e){return super.unshift(...e)>this.size&&super.splice(this.size,e.length),this.length}concat(...e){const t=super.concat(e);return t.size=this.size,t.length>t.size&&t.splice(0,t.length-t.size),t}splice(e,t,...r){let s;return arguments.length>=3&&void 0!==t?(s=super.splice(e,t),this.push(...r)):s=2===arguments.length?super.splice(e,t):super.splice(e),s}resize(e){if(this.checkSize(e),0===e)this.length=0;else if(e<this.size)for(let t=e;t<this.size;t++)super.pop();this.size=e}empty(){return 0===this.length}full(){return this.length===this.size}checkSize(e){if(!Number.isSafeInteger(e))throw new TypeError(`Invalid circular array size: ${e} is not a safe integer`);if(e<0)throw new RangeError(`Invalid circular array size: ${e} < 0`)}}class w{items;head;tail;max;constructor(){this.items={},this.head=0,this.tail=0,this.max=0}get size(){return this.tail-this.head}get maxSize(){return this.max}enqueue(e){return this.items[this.tail]=e,this.tail++,this.size>this.max&&(this.max=this.size),this.size}dequeue(){if(this.size<=0)return;const e=this.items[this.head];return delete this.items[this.head],this.head++,this.head===this.tail&&(this.head=0,this.tail=0),e}peek(){if(!(this.size<=0))return this.items[this.head]}}const T=Object.freeze({ROUND_ROBIN:"ROUND_ROBIN",LEAST_USED:"LEAST_USED",LEAST_BUSY:"LEAST_BUSY",LEAST_ELU:"LEAST_ELU",FAIR_SHARE:"FAIR_SHARE",WEIGHTED_ROUND_ROBIN:"WEIGHTED_ROUND_ROBIN",INTERLEAVED_WEIGHTED_ROUND_ROBIN:"INTERLEAVED_WEIGHTED_ROUND_ROBIN"}),f=Object.freeze({runTime:"runTime",waitTime:"waitTime",elu:"elu"});class W{pool;opts;toggleFindLastFreeWorkerNodeKey=!1;taskStatisticsRequirements={runTime:{aggregate:!1,average:!1,median:!1},waitTime:{aggregate:!1,average:!1,median:!1},elu:{aggregate:!1,average:!1,median:!1}};constructor(e,t=d){this.pool=e,this.opts=t,this.choose=this.choose.bind(this)}setTaskStatisticsRequirements(e){this.taskStatisticsRequirements.runTime.average&&!0===e.runTime?.median&&(this.taskStatisticsRequirements.runTime.average=!1,this.taskStatisticsRequirements.runTime.median=e.runTime.median),this.taskStatisticsRequirements.runTime.median&&!1===e.runTime?.median&&(this.taskStatisticsRequirements.runTime.average=!0,this.taskStatisticsRequirements.runTime.median=e.runTime.median),this.taskStatisticsRequirements.waitTime.average&&!0===e.waitTime?.median&&(this.taskStatisticsRequirements.waitTime.average=!1,this.taskStatisticsRequirements.waitTime.median=e.waitTime.median),this.taskStatisticsRequirements.waitTime.median&&!1===e.waitTime?.median&&(this.taskStatisticsRequirements.waitTime.average=!0,this.taskStatisticsRequirements.waitTime.median=e.waitTime.median),this.taskStatisticsRequirements.elu.average&&!0===e.elu?.median&&(this.taskStatisticsRequirements.elu.average=!1,this.taskStatisticsRequirements.elu.median=e.elu.median),this.taskStatisticsRequirements.elu.median&&!1===e.elu?.median&&(this.taskStatisticsRequirements.elu.average=!0,this.taskStatisticsRequirements.elu.median=e.elu.median)}setOptions(e){e=e??d,this.setTaskStatisticsRequirements(e),this.opts=e}findFreeWorkerNodeKey(){return this.toggleFindLastFreeWorkerNodeKey?(this.toggleFindLastFreeWorkerNodeKey=!1,this.findLastFreeWorkerNodeKey()):(this.toggleFindLastFreeWorkerNodeKey=!0,this.findFirstFreeWorkerNodeKey())}getWorkerTaskRunTime(e){return this.taskStatisticsRequirements.runTime.median?this.pool.workerNodes[e].workerUsage.runTime.median:this.pool.workerNodes[e].workerUsage.runTime.average}getWorkerTaskWaitTime(e){return this.taskStatisticsRequirements.waitTime.median?this.pool.workerNodes[e].workerUsage.runTime.median:this.pool.workerNodes[e].workerUsage.runTime.average}getWorkerTaskElu(e){return this.taskStatisticsRequirements.elu.median?this.pool.workerNodes[e].workerUsage.elu.active.median:this.pool.workerNodes[e].workerUsage.elu.active.average}computeDefaultWorkerWeight(){let e=0;for(const t of i.cpus()){const r=t.speed.toString().length-1;e+=1/(t.speed/Math.pow(10,r))*Math.pow(10,r)}return Math.round(e/i.cpus().length)}findFirstFreeWorkerNodeKey(){return this.pool.workerNodes.findIndex((e=>0===e.workerUsage.tasks.executing))}findLastFreeWorkerNodeKey(){for(let e=this.pool.workerNodes.length-1;e>=0;e--)if(0===this.pool.workerNodes[e].workerUsage.tasks.executing)return e;return-1}}class y extends W{taskStatisticsRequirements={runTime:{aggregate:!0,average:!0,median:!1},waitTime:{aggregate:!1,average:!1,median:!1},elu:{aggregate:!0,average:!0,median:!1}};workersVirtualTaskEndTimestamp=[];constructor(e,t=d){super(e,t),this.setTaskStatisticsRequirements(this.opts)}reset(){return this.workersVirtualTaskEndTimestamp=[],!0}update(e){return this.computeWorkerVirtualTaskEndTimestamp(e),!0}choose(){let e,t=1/0;for(const[r]of this.pool.workerNodes.entries()){null==this.workersVirtualTaskEndTimestamp[r]&&this.computeWorkerVirtualTaskEndTimestamp(r);const s=this.workersVirtualTaskEndTimestamp[r];s<t&&(t=s,e=r)}return e}remove(e){return this.workersVirtualTaskEndTimestamp.splice(e,1),!0}computeWorkerVirtualTaskEndTimestamp(e){this.workersVirtualTaskEndTimestamp[e]=this.getWorkerVirtualTaskEndTimestamp(e,this.getWorkerVirtualTaskStartTimestamp(e))}getWorkerVirtualTaskEndTimestamp(e,t){return t+(this.opts.measurement===f.elu?this.getWorkerTaskElu(e):this.getWorkerTaskRunTime(e))}getWorkerVirtualTaskStartTimestamp(e){return Math.max(performance.now(),this.workersVirtualTaskEndTimestamp[e]??-1/0)}}class S extends W{currentWorkerNodeId=0;currentRoundId=0;roundWeights;defaultWorkerWeight;constructor(e,t=d){super(e,t),this.setTaskStatisticsRequirements(this.opts),this.defaultWorkerWeight=this.computeDefaultWorkerWeight(),this.roundWeights=this.getRoundWeights()}reset(){return this.currentWorkerNodeId=0,this.currentRoundId=0,!0}update(){return!0}choose(){let e,t;for(let r=this.currentRoundId;r<this.roundWeights.length;r++)for(let s=this.currentWorkerNodeId;s<this.pool.workerNodes.length;s++){if((this.opts.weights?.[s]??this.defaultWorkerWeight)>=this.roundWeights[r]){e=r,t=s;break}}this.currentRoundId=e??0,this.currentWorkerNodeId=t??0;const r=this.currentWorkerNodeId;return this.currentWorkerNodeId===this.pool.workerNodes.length-1?(this.currentWorkerNodeId=0,this.currentRoundId=this.currentRoundId===this.roundWeights.length-1?0:this.currentRoundId+1):this.currentWorkerNodeId=this.currentWorkerNodeId+1,r}remove(e){return this.currentWorkerNodeId===e&&(0===this.pool.workerNodes.length?this.currentWorkerNodeId=0:this.currentWorkerNodeId>this.pool.workerNodes.length-1&&(this.currentWorkerNodeId=this.pool.workerNodes.length-1,this.currentRoundId=this.currentRoundId===this.roundWeights.length-1?0:this.currentRoundId+1)),!0}setOptions(e){super.setOptions(e),this.roundWeights=this.getRoundWeights()}getRoundWeights(){return null==this.opts.weights?[this.defaultWorkerWeight]:[...new Set(Object.values(this.opts.weights).slice().sort(((e,t)=>e-t)))]}}class N extends W{taskStatisticsRequirements={runTime:{aggregate:!0,average:!1,median:!1},waitTime:{aggregate:!0,average:!1,median:!1},elu:{aggregate:!1,average:!1,median:!1}};constructor(e,t=d){super(e,t),this.setTaskStatisticsRequirements(this.opts)}reset(){return!0}update(){return!0}choose(){let e,t=1/0;for(const[r,s]of this.pool.workerNodes.entries()){const i=s.workerUsage.runTime.aggregate+s.workerUsage.waitTime.aggregate;if(0===i)return r;i<t&&(t=i,e=r)}return e}remove(){return!0}}class x extends W{constructor(e,t=d){super(e,t),this.setTaskStatisticsRequirements(this.opts)}reset(){return!0}update(){return!0}choose(){const e=this.findFreeWorkerNodeKey();if(-1!==e)return e;let t,r=1/0;for(const[e,s]of this.pool.workerNodes.entries()){const i=s.workerUsage.tasks,o=i.executed+i.executing+i.queued;if(0===o)return e;o<r&&(r=o,t=e)}return t}remove(){return!0}}class v extends W{taskStatisticsRequirements={runTime:{aggregate:!1,average:!1,median:!1},waitTime:{aggregate:!1,average:!1,median:!1},elu:{aggregate:!0,average:!1,median:!1}};constructor(e,t=d){super(e,t),this.setTaskStatisticsRequirements(this.opts)}reset(){return!0}update(){return!0}choose(){let e,t=1/0;for(const[r,s]of this.pool.workerNodes.entries()){const i=s.workerUsage,o=i.elu?.active.aggregate??0;if(0===o)return r;o<t&&(t=o,e=r)}return e}remove(){return!0}}class R extends W{nextWorkerNodeId=0;constructor(e,t=d){super(e,t),this.setTaskStatisticsRequirements(this.opts)}reset(){return this.nextWorkerNodeId=0,!0}update(){return!0}choose(){const e=this.nextWorkerNodeId;return this.nextWorkerNodeId=this.nextWorkerNodeId===this.pool.workerNodes.length-1?0:this.nextWorkerNodeId+1,e}remove(e){return this.nextWorkerNodeId===e&&(0===this.pool.workerNodes.length?this.nextWorkerNodeId=0:this.nextWorkerNodeId>this.pool.workerNodes.length-1&&(this.nextWorkerNodeId=this.pool.workerNodes.length-1)),!0}}class E extends W{taskStatisticsRequirements={runTime:{aggregate:!0,average:!0,median:!1},waitTime:{aggregate:!1,average:!1,median:!1},elu:{aggregate:!1,average:!1,median:!1}};currentWorkerNodeId=0;defaultWorkerWeight;workerVirtualTaskRunTime=0;constructor(e,t=d){super(e,t),this.setTaskStatisticsRequirements(this.opts),this.defaultWorkerWeight=this.computeDefaultWorkerWeight()}reset(){return this.currentWorkerNodeId=0,this.workerVirtualTaskRunTime=0,!0}update(){return!0}choose(){const e=this.currentWorkerNodeId,t=this.workerVirtualTaskRunTime;return t<(this.opts.weights?.[e]??this.defaultWorkerWeight)?this.workerVirtualTaskRunTime=t+this.getWorkerTaskRunTime(e):(this.currentWorkerNodeId=this.currentWorkerNodeId===this.pool.workerNodes.length-1?0:this.currentWorkerNodeId+1,this.workerVirtualTaskRunTime=0),e}remove(e){return this.currentWorkerNodeId===e&&(0===this.pool.workerNodes.length?this.currentWorkerNodeId=0:this.currentWorkerNodeId>this.pool.workerNodes.length-1&&(this.currentWorkerNodeId=this.pool.workerNodes.length-1),this.workerVirtualTaskRunTime=0),!0}}class I{workerChoiceStrategy;workerChoiceStrategies;constructor(e,t=T.ROUND_ROBIN,r=d){this.workerChoiceStrategy=t,this.execute=this.execute.bind(this),this.workerChoiceStrategies=new Map([[T.ROUND_ROBIN,new(R.bind(this))(e,r)],[T.LEAST_USED,new(x.bind(this))(e,r)],[T.LEAST_BUSY,new(N.bind(this))(e,r)],[T.LEAST_ELU,new(v.bind(this))(e,r)],[T.FAIR_SHARE,new(y.bind(this))(e,r)],[T.WEIGHTED_ROUND_ROBIN,new(E.bind(this))(e,r)],[T.INTERLEAVED_WEIGHTED_ROUND_ROBIN,new(S.bind(this))(e,r)]])}getTaskStatisticsRequirements(){return this.workerChoiceStrategies.get(this.workerChoiceStrategy).taskStatisticsRequirements}setWorkerChoiceStrategy(e){this.workerChoiceStrategy!==e&&(this.workerChoiceStrategy=e),this.workerChoiceStrategies.get(this.workerChoiceStrategy)?.reset()}update(e){return this.workerChoiceStrategies.get(this.workerChoiceStrategy).update(e)}execute(){const e=this.workerChoiceStrategies.get(this.workerChoiceStrategy).choose();if(null==e)throw new Error("Worker node key chosen is null or undefined");return e}remove(e){return this.workerChoiceStrategies.get(this.workerChoiceStrategy).remove(e)}setOptions(e){for(const t of this.workerChoiceStrategies.values())t.setOptions(e)}}class b{numberOfWorkers;filePath;opts;workerNodes=[];emitter;promiseResponseMap=new Map;workerChoiceStrategyContext;constructor(e,t,r){if(this.numberOfWorkers=e,this.filePath=t,this.opts=r,!this.isMain())throw new Error("Cannot start a pool from a worker!");this.checkNumberOfWorkers(this.numberOfWorkers),this.checkFilePath(this.filePath),this.checkPoolOptions(this.opts),this.chooseWorkerNode=this.chooseWorkerNode.bind(this),this.executeTask=this.executeTask.bind(this),this.enqueueTask=this.enqueueTask.bind(this),this.checkAndEmitEvents=this.checkAndEmitEvents.bind(this),!0===this.opts.enableEvents&&(this.emitter=new u),this.workerChoiceStrategyContext=new I(this,this.opts.workerChoiceStrategy,this.opts.workerChoiceStrategyOptions),this.setupHook();for(let e=1;e<=this.numberOfWorkers;e++)this.createAndSetupWorker()}checkFilePath(e){if(null==e||"string"==typeof e&&0===e.trim().length)throw new Error("Please specify a file with a worker implementation")}checkNumberOfWorkers(e){if(null==e)throw new Error("Cannot instantiate a pool without specifying the number of workers");if(!Number.isSafeInteger(e))throw new TypeError("Cannot instantiate a pool with a non safe integer number of workers");if(e<0)throw new RangeError("Cannot instantiate a pool with a negative number of workers");if(this.type===n.fixed&&0===e)throw new Error("Cannot instantiate a fixed pool with no worker")}checkPoolOptions(e){if(!g(e))throw new TypeError("Invalid pool options: must be a plain object");this.opts.workerChoiceStrategy=e.workerChoiceStrategy??T.ROUND_ROBIN,this.checkValidWorkerChoiceStrategy(this.opts.workerChoiceStrategy),this.opts.workerChoiceStrategyOptions=e.workerChoiceStrategyOptions??d,this.checkValidWorkerChoiceStrategyOptions(this.opts.workerChoiceStrategyOptions),this.opts.restartWorkerOnError=e.restartWorkerOnError??!0,this.opts.enableEvents=e.enableEvents??!0,this.opts.enableTasksQueue=e.enableTasksQueue??!1,this.opts.enableTasksQueue&&(this.checkValidTasksQueueOptions(e.tasksQueueOptions),this.opts.tasksQueueOptions=this.buildTasksQueueOptions(e.tasksQueueOptions))}checkValidWorkerChoiceStrategy(e){if(!Object.values(T).includes(e))throw new Error(`Invalid worker choice strategy '${e}'`)}checkValidWorkerChoiceStrategyOptions(e){if(!g(e))throw new TypeError("Invalid worker choice strategy options: must be a plain object");if(null!=e.weights&&Object.keys(e.weights).length!==this.maxSize)throw new Error("Invalid worker choice strategy options: must have a weight for each worker node")}checkValidTasksQueueOptions(e){if(null!=e&&!g(e))throw new TypeError("Invalid tasks queue options: must be a plain object");if(e?.concurrency<=0)throw new Error(`Invalid worker tasks concurrency '${e.concurrency}'`)}get info(){return{type:this.type,worker:this.worker,minSize:this.minSize,maxSize:this.maxSize,workerNodes:this.workerNodes.length,idleWorkerNodes:this.workerNodes.reduce(((e,t)=>0===t.workerUsage.tasks.executing?e+1:e),0),busyWorkerNodes:this.workerNodes.reduce(((e,t)=>t.workerUsage.tasks.executing>0?e+1:e),0),executedTasks:this.workerNodes.reduce(((e,t)=>e+t.workerUsage.tasks.executed),0),executingTasks:this.workerNodes.reduce(((e,t)=>e+t.workerUsage.tasks.executing),0),queuedTasks:this.workerNodes.reduce(((e,t)=>e+t.tasksQueue.size),0),maxQueuedTasks:this.workerNodes.reduce(((e,t)=>e+t.tasksQueue.maxSize),0),failedTasks:this.workerNodes.reduce(((e,t)=>e+t.workerUsage.tasks.failed),0)}}getWorkerNodeKey(e){return this.workerNodes.findIndex((t=>t.worker===e))}setWorkerChoiceStrategy(e,t){this.checkValidWorkerChoiceStrategy(e),this.opts.workerChoiceStrategy=e,this.workerChoiceStrategyContext.setWorkerChoiceStrategy(this.opts.workerChoiceStrategy),null!=t&&this.setWorkerChoiceStrategyOptions(t);for(const e of this.workerNodes)this.setWorkerNodeTasksUsage(e,this.getWorkerUsage(e.worker)),this.setWorkerStatistics(e.worker)}setWorkerChoiceStrategyOptions(e){this.checkValidWorkerChoiceStrategyOptions(e),this.opts.workerChoiceStrategyOptions=e,this.workerChoiceStrategyContext.setOptions(this.opts.workerChoiceStrategyOptions)}enableTasksQueue(e,t){!0!==this.opts.enableTasksQueue||e||this.flushTasksQueues(),this.opts.enableTasksQueue=e,this.setTasksQueueOptions(t)}setTasksQueueOptions(e){!0===this.opts.enableTasksQueue?(this.checkValidTasksQueueOptions(e),this.opts.tasksQueueOptions=this.buildTasksQueueOptions(e)):null!=this.opts.tasksQueueOptions&&delete this.opts.tasksQueueOptions}buildTasksQueueOptions(e){return{concurrency:e?.concurrency??1}}get full(){return this.workerNodes.length>=this.maxSize}internalBusy(){return-1===this.workerNodes.findIndex((e=>0===e.workerUsage.tasks.executing))}async execute(e,t){const i=s.performance.now(),o=this.chooseWorkerNode(),a={name:t,data:e??{},timestamp:i,id:r.randomUUID()},n=new Promise(((e,t)=>{this.promiseResponseMap.set(a.id,{resolve:e,reject:t,worker:this.workerNodes[o].worker})}));return!0===this.opts.enableTasksQueue&&(this.busy||this.workerNodes[o].workerUsage.tasks.executing>=this.opts.tasksQueueOptions.concurrency)?this.enqueueTask(o,a):this.executeTask(o,a),this.workerChoiceStrategyContext.update(o),this.checkAndEmitEvents(),n}async destroy(){await Promise.all(this.workerNodes.map((async(e,t)=>{this.flushTasksQueue(t),await this.destroyWorker(e.worker)})))}setupHook(){}beforeTaskExecutionHook(e,t){const r=this.workerNodes[e].workerUsage;++r.tasks.executing,this.updateWaitTimeWorkerUsage(r,t)}afterTaskExecutionHook(e,t){const r=this.workerNodes[this.getWorkerNodeKey(e)].workerUsage;this.updateTaskStatisticsWorkerUsage(r,t),this.updateRunTimeWorkerUsage(r,t),this.updateEluWorkerUsage(r,t)}updateTaskStatisticsWorkerUsage(e,t){const r=e.tasks;--r.executing,++r.executed,null!=t.taskError&&++r.failed}updateRunTimeWorkerUsage(e,t){this.workerChoiceStrategyContext.getTaskStatisticsRequirements().runTime.aggregate&&(e.runTime.aggregate+=t.taskPerformance?.runTime??0,this.workerChoiceStrategyContext.getTaskStatisticsRequirements().runTime.average&&0!==e.tasks.executed&&(e.runTime.average=e.runTime.aggregate/(e.tasks.executed-e.tasks.failed)),this.workerChoiceStrategyContext.getTaskStatisticsRequirements().runTime.median&&null!=t.taskPerformance?.runTime&&(e.runTime.history.push(t.taskPerformance.runTime),e.runTime.median=l(e.runTime.history)))}updateWaitTimeWorkerUsage(e,t){const r=s.performance.now(),i=r-(t.timestamp??r);this.workerChoiceStrategyContext.getTaskStatisticsRequirements().waitTime.aggregate&&(e.waitTime.aggregate+=i??0,this.workerChoiceStrategyContext.getTaskStatisticsRequirements().waitTime.average&&0!==e.tasks.executed&&(e.waitTime.average=e.waitTime.aggregate/(e.tasks.executed-e.tasks.failed)),this.workerChoiceStrategyContext.getTaskStatisticsRequirements().waitTime.median&&null!=i&&(e.waitTime.history.push(i),e.waitTime.median=l(e.waitTime.history)))}updateEluWorkerUsage(e,t){if(this.workerChoiceStrategyContext.getTaskStatisticsRequirements().elu.aggregate){if(null!=e.elu&&null!=t.taskPerformance?.elu?(e.elu.idle.aggregate+=t.taskPerformance.elu.idle,e.elu.active.aggregate+=t.taskPerformance.elu.active,e.elu.utilization=(e.elu.utilization+t.taskPerformance.elu.utilization)/2):null!=t.taskPerformance?.elu&&(e.elu.idle.aggregate=t.taskPerformance.elu.idle,e.elu.active.aggregate=t.taskPerformance.elu.active,e.elu.utilization=t.taskPerformance.elu.utilization),this.workerChoiceStrategyContext.getTaskStatisticsRequirements().elu.average&&0!==e.tasks.executed){const t=e.tasks.executed-e.tasks.failed;e.elu.idle.average=e.elu.idle.aggregate/t,e.elu.active.average=e.elu.active.aggregate/t}this.workerChoiceStrategyContext.getTaskStatisticsRequirements().elu.median&&null!=t.taskPerformance?.elu&&(e.elu.idle.history.push(t.taskPerformance.elu.idle),e.elu.active.history.push(t.taskPerformance.elu.active),e.elu.idle.median=l(e.elu.idle.history),e.elu.active.median=l(e.elu.active.history))}}chooseWorkerNode(){let e;if(this.type===n.dynamic&&!this.full&&this.internalBusy()){const t=this.createAndSetupWorker();this.registerWorkerMessageListener(t,(e=>{const r=this.getWorkerNodeKey(t);var s;s=m.HARD,(e.kill===s||null!=e.kill&&0===this.workerNodes[r].workerUsage.tasks.executing)&&(this.flushTasksQueue(r),this.destroyWorker(t))})),e=this.getWorkerNodeKey(t)}else e=this.workerChoiceStrategyContext.execute();return e}createAndSetupWorker(){const e=this.createWorker();return e.on("message",this.opts.messageHandler??c),e.on("error",this.opts.errorHandler??c),e.on("error",(e=>{null!=this.emitter&&this.emitter.emit(k.error,e)})),e.on("error",(()=>{!0===this.opts.restartWorkerOnError&&this.createAndSetupWorker()})),e.on("online",this.opts.onlineHandler??c),e.on("exit",this.opts.exitHandler??c),e.once("exit",(()=>{this.removeWorkerNode(e)})),this.pushWorkerNode(e),this.setWorkerStatistics(e),this.afterWorkerSetup(e),e}workerListener(){return e=>{if(null!=e.id){const t=this.promiseResponseMap.get(e.id);if(null!=t){null!=e.taskError?(t.reject(e.taskError.message),null!=this.emitter&&this.emitter.emit(k.taskError,e.taskError)):t.resolve(e.data),this.afterTaskExecutionHook(t.worker,e),this.promiseResponseMap.delete(e.id);const r=this.getWorkerNodeKey(t.worker);!0===this.opts.enableTasksQueue&&this.tasksQueueSize(r)>0&&this.executeTask(r,this.dequeueTask(r))}}}}checkAndEmitEvents(){null!=this.emitter&&(this.busy&&this.emitter?.emit(k.busy,this.info),this.type===n.dynamic&&this.full&&this.emitter?.emit(k.full,this.info))}setWorkerNodeTasksUsage(e,t){e.workerUsage=t}pushWorkerNode(e){return this.workerNodes.push({worker:e,workerUsage:this.getWorkerUsage(e),tasksQueue:new w})}removeWorkerNode(e){const t=this.getWorkerNodeKey(e);-1!==t&&(this.workerNodes.splice(t,1),this.workerChoiceStrategyContext.remove(t))}executeTask(e,t){this.beforeTaskExecutionHook(e,t),this.sendToWorker(this.workerNodes[e].worker,t)}enqueueTask(e,t){return this.workerNodes[e].tasksQueue.enqueue(t)}dequeueTask(e){return this.workerNodes[e].tasksQueue.dequeue()}tasksQueueSize(e){return this.workerNodes[e].tasksQueue.size}flushTasksQueue(e){if(this.tasksQueueSize(e)>0)for(let t=0;t<this.tasksQueueSize(e);t++)this.executeTask(e,this.dequeueTask(e))}flushTasksQueues(){for(const[e]of this.workerNodes.entries())this.flushTasksQueue(e)}setWorkerStatistics(e){this.sendToWorker(e,{statistics:{runTime:this.workerChoiceStrategyContext.getTaskStatisticsRequirements().runTime.aggregate,elu:this.workerChoiceStrategyContext.getTaskStatisticsRequirements().elu.aggregate}})}getWorkerUsage(e){return{tasks:this.getTaskStatistics(e),runTime:{aggregate:0,average:0,median:0,history:new p},waitTime:{aggregate:0,average:0,median:0,history:new p},elu:{idle:{aggregate:0,average:0,median:0,history:new p},active:{aggregate:0,average:0,median:0,history:new p},utilization:0}}}getTaskStatistics(e){const t=this.workerNodes[this.getWorkerNodeKey(e)]?.tasksQueue?.size;return{executed:0,executing:0,get queued(){return t??0},failed:0}}}class O extends b{opts;constructor(e,t,r={}){super(e,t,r),this.opts=r}setupHook(){t.setupPrimary({...this.opts.settings,exec:this.filePath})}isMain(){return t.isPrimary}destroyWorker(e){this.sendToWorker(e,{kill:1}),e.kill()}sendToWorker(e,t){e.send(t)}registerWorkerMessageListener(e,t){e.on("message",t)}createWorker(){return t.fork(this.opts.env)}afterWorkerSetup(e){this.registerWorkerMessageListener(e,super.workerListener())}get type(){return n.fixed}get worker(){return h.cluster}get minSize(){return this.numberOfWorkers}get maxSize(){return this.numberOfWorkers}get busy(){return this.internalBusy()}}class C extends b{opts;constructor(e,t,r={}){super(e,t,r),this.opts=r}isMain(){return o.isMainThread}async destroyWorker(e){this.sendToWorker(e,{kill:1}),await e.terminate()}sendToWorker(e,t){e.postMessage(t)}registerWorkerMessageListener(e,t){e.port2?.on("message",t)}createWorker(){return new o.Worker(this.filePath,{env:o.SHARE_ENV,...this.opts.workerOptions})}afterWorkerSetup(e){const{port1:t,port2:r}=new o.MessageChannel;e.postMessage({parent:t},[t]),e.port1=t,e.port2=r,this.registerWorkerMessageListener(e,super.workerListener())}get type(){return n.fixed}get worker(){return h.thread}get minSize(){return this.numberOfWorkers}get maxSize(){return this.numberOfWorkers}get busy(){return this.internalBusy()}}const q="default",z=6e4,U=m.SOFT;class A extends a.AsyncResource{isMain;mainWorker;opts;taskFunctions;lastTaskTimestamp;statistics;aliveInterval;constructor(e,t,r,i,o={killBehavior:U,maxInactiveTime:z}){super(e),this.isMain=t,this.mainWorker=i,this.opts=o,this.checkWorkerOptions(this.opts),this.checkTaskFunctions(r),this.isMain||(this.lastTaskTimestamp=s.performance.now(),this.aliveInterval=setInterval(this.checkAlive.bind(this),(this.opts.maxInactiveTime??z)/2),this.checkAlive.bind(this)()),this.mainWorker?.on("message",this.messageListener.bind(this))}checkWorkerOptions(e){this.opts.killBehavior=e.killBehavior??U,this.opts.maxInactiveTime=e.maxInactiveTime??z,delete this.opts.async}checkTaskFunctions(e){if(null==e)throw new Error("taskFunctions parameter is mandatory");if(this.taskFunctions=new Map,"function"==typeof e)this.taskFunctions.set(q,e.bind(this));else{if(!g(e))throw new TypeError("taskFunctions parameter is not a function or a plain object");{let t=!0;for(const[r,s]of Object.entries(e)){if("function"!=typeof s)throw new TypeError("A taskFunctions parameter object value is not a function");this.taskFunctions.set(r,s.bind(this)),t&&(this.taskFunctions.set(q,s.bind(this)),t=!1)}if(t)throw new Error("taskFunctions parameter object is empty")}}}messageListener(e){if(null!=e.id&&null!=e.data){const t=this.getTaskFunction(e.name);"AsyncFunction"===t?.constructor.name?this.runInAsyncScope(this.runAsync.bind(this),this,t,e):this.runInAsyncScope(this.runSync.bind(this),this,t,e)}else null!=e.parent?this.mainWorker=e.parent:null!=e.kill?(null!=this.aliveInterval&&clearInterval(this.aliveInterval),this.emitDestroy()):null!=e.statistics&&(this.statistics=e.statistics)}getMainWorker(){if(null==this.mainWorker)throw new Error("Main worker was not set");return this.mainWorker}checkAlive(){s.performance.now()-this.lastTaskTimestamp>(this.opts.maxInactiveTime??z)&&this.sendToMainWorker({kill:this.opts.killBehavior})}handleError(e){return e}runSync(e,t){try{let r=this.beginTaskPerformance();const s=e(t.data);r=this.endTaskPerformance(r),this.sendToMainWorker({data:s,taskPerformance:r,id:t.id})}catch(e){const r=this.handleError(e);this.sendToMainWorker({taskError:{message:r,data:t.data},id:t.id})}finally{!this.isMain&&(this.lastTaskTimestamp=s.performance.now())}}runAsync(e,t){let r=this.beginTaskPerformance();e(t.data).then((e=>(r=this.endTaskPerformance(r),this.sendToMainWorker({data:e,taskPerformance:r,id:t.id}),null))).catch((e=>{const r=this.handleError(e);this.sendToMainWorker({taskError:{message:r,data:t.data},id:t.id})})).finally((()=>{!this.isMain&&(this.lastTaskTimestamp=s.performance.now())})).catch(c)}getTaskFunction(e){e=e??q;const t=this.taskFunctions.get(e);if(null==t)throw new Error(`Task function '${e}' not found`);return t}beginTaskPerformance(){return{timestamp:s.performance.now(),...this.statistics.elu&&{elu:s.performance.eventLoopUtilization()}}}endTaskPerformance(e){return{...e,...this.statistics.runTime&&{runTime:s.performance.now()-e.timestamp},...this.statistics.elu&&{elu:s.performance.eventLoopUtilization(e.elu)}}}}exports.ClusterWorker=class extends A{constructor(e,r={}){super("worker-cluster-pool:poolifier",t.isPrimary,e,t.worker,r)}sendToMainWorker(e){this.getMainWorker().send(e)}handleError(e){return e instanceof Error?e.message:e}},exports.DynamicClusterPool=class extends O{max;constructor(e,t,r,s={}){super(e,r,s),this.max=t}get type(){return n.dynamic}get maxSize(){return this.max}get busy(){return this.full&&this.internalBusy()}},exports.DynamicThreadPool=class extends C{max;constructor(e,t,r,s={}){super(e,r,s),this.max=t}get type(){return n.dynamic}get maxSize(){return this.max}get busy(){return this.full&&this.internalBusy()}},exports.FixedClusterPool=O,exports.FixedThreadPool=C,exports.KillBehaviors=m,exports.Measurements=f,exports.PoolEvents=k,exports.PoolTypes=n,exports.ThreadWorker=class extends A{constructor(e,t={}){super("worker-thread-pool:poolifier",o.isMainThread,e,o.parentPort,t)}sendToMainWorker(e){this.getMainWorker().postMessage(e)}},exports.WorkerChoiceStrategies=T,exports.WorkerTypes=h;
|
package/lib/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"node:events";import t from"node:cluster";import s from"node:crypto";import{performance as r}from"node:perf_hooks";import{cpus as i}from"node:os";import{isMainThread as o,Worker as n,SHARE_ENV as a,MessageChannel as h,parentPort as u}from"node:worker_threads";import{AsyncResource as k}from"node:async_hooks";const c=Object.freeze({fixed:"fixed",dynamic:"dynamic"}),d=Object.freeze({cluster:"cluster",thread:"thread"});class l extends e{}const m=Object.freeze({full:"full",busy:"busy",error:"error",taskError:"taskError"}),p=Object.freeze((()=>{})),g={medRunTime:!1,medWaitTime:!1},T=e=>{if(Array.isArray(e)&&0===e.length)return 0;if(Array.isArray(e)&&1===e.length)return e[0];const t=e.slice().sort(((e,t)=>e-t));return(t[t.length-1>>1]+t[t.length>>1])/2},w=e=>"object"==typeof e&&null!==e&&e?.constructor===Object&&"[object Object]"===Object.prototype.toString.call(e),W=Object.freeze({SOFT:"SOFT",HARD:"HARD"});class f extends Array{size;constructor(e=1024,...t){super(),this.checkSize(e),this.size=e,arguments.length>1&&this.push(...t)}push(...e){const t=super.push(...e);return t>this.size&&super.splice(0,t-this.size),this.length}unshift(...e){return super.unshift(...e)>this.size&&super.splice(this.size,e.length),this.length}concat(...e){const t=super.concat(e);return t.size=this.size,t.length>t.size&&t.splice(0,t.length-t.size),t}splice(e,t,...s){let r;return arguments.length>=3&&void 0!==t?(r=super.splice(e,t),this.push(...s)):r=2===arguments.length?super.splice(e,t):super.splice(e),r}resize(e){if(this.checkSize(e),0===e)this.length=0;else if(e<this.size)for(let t=e;t<this.size;t++)super.pop();this.size=e}empty(){return 0===this.length}full(){return this.length===this.size}checkSize(e){if(!Number.isSafeInteger(e))throw new TypeError(`Invalid circular array size: ${e} is not a safe integer`);if(e<0)throw new RangeError(`Invalid circular array size: ${e} < 0`)}}class y{items;head;tail;max;constructor(){this.items={},this.head=0,this.tail=0,this.max=0}get size(){return this.tail-this.head}get maxSize(){return this.max}enqueue(e){return this.items[this.tail]=e,this.tail++,this.size>this.max&&(this.max=this.size),this.size}dequeue(){if(this.size<=0)return;const e=this.items[this.head];return delete this.items[this.head],this.head++,this.head===this.tail&&(this.head=0,this.tail=0),e}peek(){if(!(this.size<=0))return this.items[this.head]}}const S=Object.freeze({ROUND_ROBIN:"ROUND_ROBIN",LEAST_USED:"LEAST_USED",LEAST_BUSY:"LEAST_BUSY",FAIR_SHARE:"FAIR_SHARE",WEIGHTED_ROUND_ROBIN:"WEIGHTED_ROUND_ROBIN",INTERLEAVED_WEIGHTED_ROUND_ROBIN:"INTERLEAVED_WEIGHTED_ROUND_ROBIN"});class N{pool;opts;toggleFindLastFreeWorkerNodeKey=!1;taskStatistics={runTime:!1,avgRunTime:!1,medRunTime:!1,waitTime:!1,avgWaitTime:!1,medWaitTime:!1,elu:!1};constructor(e,t=g){this.pool=e,this.opts=t,this.choose=this.choose.bind(this)}setTaskStatistics(e){this.taskStatistics.avgRunTime&&!0===e.medRunTime&&(this.taskStatistics.avgRunTime=!1,this.taskStatistics.medRunTime=e.medRunTime),this.taskStatistics.medRunTime&&!1===e.medRunTime&&(this.taskStatistics.avgRunTime=!0,this.taskStatistics.medRunTime=e.medRunTime),this.taskStatistics.avgWaitTime&&!0===e.medWaitTime&&(this.taskStatistics.avgWaitTime=!1,this.taskStatistics.medWaitTime=e.medWaitTime),this.taskStatistics.medWaitTime&&!1===e.medWaitTime&&(this.taskStatistics.avgWaitTime=!0,this.taskStatistics.medWaitTime=e.medWaitTime)}setOptions(e){e=e??g,this.setTaskStatistics(e),this.opts=e}findFreeWorkerNodeKey(){return this.toggleFindLastFreeWorkerNodeKey?(this.toggleFindLastFreeWorkerNodeKey=!1,this.findLastFreeWorkerNodeKey()):(this.toggleFindLastFreeWorkerNodeKey=!0,this.findFirstFreeWorkerNodeKey())}getWorkerTaskRunTime(e){return this.taskStatistics.medRunTime?this.pool.workerNodes[e].tasksUsage.medRunTime:this.pool.workerNodes[e].tasksUsage.avgRunTime}getWorkerWaitTime(e){return this.taskStatistics.medWaitTime?this.pool.workerNodes[e].tasksUsage.medWaitTime:this.pool.workerNodes[e].tasksUsage.avgWaitTime}computeDefaultWorkerWeight(){let e=0;for(const t of i()){const s=t.speed.toString().length-1;e+=1/(t.speed/Math.pow(10,s))*Math.pow(10,s)}return Math.round(e/i().length)}findFirstFreeWorkerNodeKey(){return this.pool.workerNodes.findIndex((e=>0===e.tasksUsage.running))}findLastFreeWorkerNodeKey(){for(let e=this.pool.workerNodes.length-1;e>=0;e--)if(0===this.pool.workerNodes[e].tasksUsage.running)return e;return-1}}class x extends N{taskStatistics={runTime:!0,avgRunTime:!0,medRunTime:!1,waitTime:!1,avgWaitTime:!1,medWaitTime:!1,elu:!1};workersVirtualTaskEndTimestamp=[];constructor(e,t=g){super(e,t),this.setTaskStatistics(this.opts)}reset(){return this.workersVirtualTaskEndTimestamp=[],!0}update(e){return this.computeWorkerVirtualTaskEndTimestamp(e),!0}choose(){let e,t=1/0;for(const[s]of this.pool.workerNodes.entries()){null==this.workersVirtualTaskEndTimestamp[s]&&this.computeWorkerVirtualTaskEndTimestamp(s);const r=this.workersVirtualTaskEndTimestamp[s];r<t&&(t=r,e=s)}return e}remove(e){return this.workersVirtualTaskEndTimestamp.splice(e,1),!0}computeWorkerVirtualTaskEndTimestamp(e){this.workersVirtualTaskEndTimestamp[e]=this.getWorkerVirtualTaskEndTimestamp(e,this.getWorkerVirtualTaskStartTimestamp(e))}getWorkerVirtualTaskEndTimestamp(e,t){return t+this.getWorkerTaskRunTime(e)}getWorkerVirtualTaskStartTimestamp(e){return Math.max(performance.now(),this.workersVirtualTaskEndTimestamp[e]??-1/0)}}class E extends N{currentWorkerNodeId=0;currentRoundId=0;roundWeights;defaultWorkerWeight;constructor(e,t=g){super(e,t),this.setTaskStatistics(this.opts),this.defaultWorkerWeight=this.computeDefaultWorkerWeight(),this.roundWeights=this.getRoundWeights()}reset(){return this.currentWorkerNodeId=0,this.currentRoundId=0,!0}update(){return!0}choose(){let e,t;for(let s=this.currentRoundId;s<this.roundWeights.length;s++)for(let r=this.currentWorkerNodeId;r<this.pool.workerNodes.length;r++){if((this.opts.weights?.[r]??this.defaultWorkerWeight)>=this.roundWeights[s]){e=s,t=r;break}}this.currentRoundId=e??0,this.currentWorkerNodeId=t??0;const s=this.currentWorkerNodeId;return this.currentWorkerNodeId===this.pool.workerNodes.length-1?(this.currentWorkerNodeId=0,this.currentRoundId=this.currentRoundId===this.roundWeights.length-1?0:this.currentRoundId+1):this.currentWorkerNodeId=this.currentWorkerNodeId+1,s}remove(e){return this.currentWorkerNodeId===e&&(0===this.pool.workerNodes.length?this.currentWorkerNodeId=0:this.currentWorkerNodeId>this.pool.workerNodes.length-1&&(this.currentWorkerNodeId=this.pool.workerNodes.length-1,this.currentRoundId=this.currentRoundId===this.roundWeights.length-1?0:this.currentRoundId+1)),!0}setOptions(e){super.setOptions(e),this.roundWeights=this.getRoundWeights()}getRoundWeights(){return null==this.opts.weights?[this.defaultWorkerWeight]:[...new Set(Object.values(this.opts.weights).slice().sort(((e,t)=>e-t)))]}}class R extends N{taskStatistics={runTime:!0,avgRunTime:!1,medRunTime:!1,waitTime:!1,avgWaitTime:!1,medWaitTime:!1,elu:!1};constructor(e,t=g){super(e,t),this.setTaskStatistics(this.opts)}reset(){return!0}update(){return!0}choose(){let e,t=1/0;for(const[s,r]of this.pool.workerNodes.entries()){const i=r.tasksUsage.runTime;if(0===i)return s;i<t&&(t=i,e=s)}return e}remove(){return!0}}class I extends N{constructor(e,t=g){super(e,t),this.setTaskStatistics(this.opts)}reset(){return!0}update(){return!0}choose(){const e=this.findFreeWorkerNodeKey();if(-1!==e)return e;let t,s=1/0;for(const[e,r]of this.pool.workerNodes.entries()){const i=r.tasksUsage,o=i.ran+i.running;if(0===o)return e;o<s&&(s=o,t=e)}return t}remove(){return!0}}class v extends N{nextWorkerNodeId=0;constructor(e,t=g){super(e,t),this.setTaskStatistics(this.opts)}reset(){return this.nextWorkerNodeId=0,!0}update(){return!0}choose(){const e=this.nextWorkerNodeId;return this.nextWorkerNodeId=this.nextWorkerNodeId===this.pool.workerNodes.length-1?0:this.nextWorkerNodeId+1,e}remove(e){return this.nextWorkerNodeId===e&&(0===this.pool.workerNodes.length?this.nextWorkerNodeId=0:this.nextWorkerNodeId>this.pool.workerNodes.length-1&&(this.nextWorkerNodeId=this.pool.workerNodes.length-1)),!0}}class b extends N{taskStatistics={runTime:!0,avgRunTime:!0,medRunTime:!1,waitTime:!1,avgWaitTime:!1,medWaitTime:!1,elu:!1};currentWorkerNodeId=0;defaultWorkerWeight;workerVirtualTaskRunTime=0;constructor(e,t=g){super(e,t),this.setTaskStatistics(this.opts),this.defaultWorkerWeight=this.computeDefaultWorkerWeight()}reset(){return this.currentWorkerNodeId=0,this.workerVirtualTaskRunTime=0,!0}update(){return!0}choose(){const e=this.currentWorkerNodeId,t=this.workerVirtualTaskRunTime;return t<(this.opts.weights?.[e]??this.defaultWorkerWeight)?this.workerVirtualTaskRunTime=t+this.getWorkerTaskRunTime(e):(this.currentWorkerNodeId=this.currentWorkerNodeId===this.pool.workerNodes.length-1?0:this.currentWorkerNodeId+1,this.workerVirtualTaskRunTime=0),e}remove(e){return this.currentWorkerNodeId===e&&(0===this.pool.workerNodes.length?this.currentWorkerNodeId=0:this.currentWorkerNodeId>this.pool.workerNodes.length-1&&(this.currentWorkerNodeId=this.pool.workerNodes.length-1),this.workerVirtualTaskRunTime=0),!0}}class O{workerChoiceStrategy;workerChoiceStrategies;constructor(e,t=S.ROUND_ROBIN,s=g){this.workerChoiceStrategy=t,this.execute=this.execute.bind(this),this.workerChoiceStrategies=new Map([[S.ROUND_ROBIN,new(v.bind(this))(e,s)],[S.LEAST_USED,new(I.bind(this))(e,s)],[S.LEAST_BUSY,new(R.bind(this))(e,s)],[S.FAIR_SHARE,new(x.bind(this))(e,s)],[S.WEIGHTED_ROUND_ROBIN,new(b.bind(this))(e,s)],[S.INTERLEAVED_WEIGHTED_ROUND_ROBIN,new(E.bind(this))(e,s)]])}getTaskStatistics(){return this.workerChoiceStrategies.get(this.workerChoiceStrategy).taskStatistics}setWorkerChoiceStrategy(e){this.workerChoiceStrategy!==e&&(this.workerChoiceStrategy=e),this.workerChoiceStrategies.get(this.workerChoiceStrategy)?.reset()}update(e){return this.workerChoiceStrategies.get(this.workerChoiceStrategy).update(e)}execute(){const e=this.workerChoiceStrategies.get(this.workerChoiceStrategy).choose();if(null==e)throw new Error("Worker node key chosen is null or undefined");return e}remove(e){return this.workerChoiceStrategies.get(this.workerChoiceStrategy).remove(e)}setOptions(e){for(const t of this.workerChoiceStrategies.values())t.setOptions(e)}}class C{numberOfWorkers;filePath;opts;workerNodes=[];emitter;promiseResponseMap=new Map;workerChoiceStrategyContext;constructor(e,t,s){if(this.numberOfWorkers=e,this.filePath=t,this.opts=s,!this.isMain())throw new Error("Cannot start a pool from a worker!");this.checkNumberOfWorkers(this.numberOfWorkers),this.checkFilePath(this.filePath),this.checkPoolOptions(this.opts),this.chooseWorkerNode=this.chooseWorkerNode.bind(this),this.executeTask=this.executeTask.bind(this),this.enqueueTask=this.enqueueTask.bind(this),this.checkAndEmitEvents=this.checkAndEmitEvents.bind(this),!0===this.opts.enableEvents&&(this.emitter=new l),this.workerChoiceStrategyContext=new O(this,this.opts.workerChoiceStrategy,this.opts.workerChoiceStrategyOptions),this.setupHook();for(let e=1;e<=this.numberOfWorkers;e++)this.createAndSetupWorker()}checkFilePath(e){if(null==e||"string"==typeof e&&0===e.trim().length)throw new Error("Please specify a file with a worker implementation")}checkNumberOfWorkers(e){if(null==e)throw new Error("Cannot instantiate a pool without specifying the number of workers");if(!Number.isSafeInteger(e))throw new TypeError("Cannot instantiate a pool with a non safe integer number of workers");if(e<0)throw new RangeError("Cannot instantiate a pool with a negative number of workers");if(this.type===c.fixed&&0===e)throw new Error("Cannot instantiate a fixed pool with no worker")}checkPoolOptions(e){if(!w(e))throw new TypeError("Invalid pool options: must be a plain object");this.opts.workerChoiceStrategy=e.workerChoiceStrategy??S.ROUND_ROBIN,this.checkValidWorkerChoiceStrategy(this.opts.workerChoiceStrategy),this.opts.workerChoiceStrategyOptions=e.workerChoiceStrategyOptions??g,this.checkValidWorkerChoiceStrategyOptions(this.opts.workerChoiceStrategyOptions),this.opts.restartWorkerOnError=e.restartWorkerOnError??!0,this.opts.enableEvents=e.enableEvents??!0,this.opts.enableTasksQueue=e.enableTasksQueue??!1,this.opts.enableTasksQueue&&(this.checkValidTasksQueueOptions(e.tasksQueueOptions),this.opts.tasksQueueOptions=this.buildTasksQueueOptions(e.tasksQueueOptions))}checkValidWorkerChoiceStrategy(e){if(!Object.values(S).includes(e))throw new Error(`Invalid worker choice strategy '${e}'`)}checkValidWorkerChoiceStrategyOptions(e){if(!w(e))throw new TypeError("Invalid worker choice strategy options: must be a plain object");if(null!=e.weights&&Object.keys(e.weights).length!==this.maxSize)throw new Error("Invalid worker choice strategy options: must have a weight for each worker node")}checkValidTasksQueueOptions(e){if(null!=e&&!w(e))throw new TypeError("Invalid tasks queue options: must be a plain object");if(e?.concurrency<=0)throw new Error(`Invalid worker tasks concurrency '${e.concurrency}'`)}get info(){return{type:this.type,worker:this.worker,minSize:this.minSize,maxSize:this.maxSize,workerNodes:this.workerNodes.length,idleWorkerNodes:this.workerNodes.reduce(((e,t)=>0===t.tasksUsage.running?e+1:e),0),busyWorkerNodes:this.workerNodes.reduce(((e,t)=>t.tasksUsage.running>0?e+1:e),0),runningTasks:this.workerNodes.reduce(((e,t)=>e+t.tasksUsage.running),0),queuedTasks:this.workerNodes.reduce(((e,t)=>e+t.tasksQueue.size),0),maxQueuedTasks:this.workerNodes.reduce(((e,t)=>e+t.tasksQueue.maxSize),0)}}getWorkerNodeKey(e){return this.workerNodes.findIndex((t=>t.worker===e))}setWorkerChoiceStrategy(e,t){this.checkValidWorkerChoiceStrategy(e),this.opts.workerChoiceStrategy=e,this.workerChoiceStrategyContext.setWorkerChoiceStrategy(this.opts.workerChoiceStrategy),null!=t&&this.setWorkerChoiceStrategyOptions(t);for(const e of this.workerNodes)this.setWorkerNodeTasksUsage(e,{ran:0,running:0,runTime:0,runTimeHistory:new f,avgRunTime:0,medRunTime:0,waitTime:0,waitTimeHistory:new f,avgWaitTime:0,medWaitTime:0,error:0,elu:void 0}),this.setWorkerStatistics(e.worker)}setWorkerChoiceStrategyOptions(e){this.checkValidWorkerChoiceStrategyOptions(e),this.opts.workerChoiceStrategyOptions=e,this.workerChoiceStrategyContext.setOptions(this.opts.workerChoiceStrategyOptions)}enableTasksQueue(e,t){!0!==this.opts.enableTasksQueue||e||this.flushTasksQueues(),this.opts.enableTasksQueue=e,this.setTasksQueueOptions(t)}setTasksQueueOptions(e){!0===this.opts.enableTasksQueue?(this.checkValidTasksQueueOptions(e),this.opts.tasksQueueOptions=this.buildTasksQueueOptions(e)):null!=this.opts.tasksQueueOptions&&delete this.opts.tasksQueueOptions}buildTasksQueueOptions(e){return{concurrency:e?.concurrency??1}}get full(){return this.workerNodes.length>=this.maxSize}internalBusy(){return-1===this.workerNodes.findIndex((e=>0===e.tasksUsage.running))}async execute(e,t){const i=r.now(),o=this.chooseWorkerNode(),n={name:t,data:e??{},timestamp:i,id:s.randomUUID()},a=new Promise(((e,t)=>{this.promiseResponseMap.set(n.id,{resolve:e,reject:t,worker:this.workerNodes[o].worker})}));return!0===this.opts.enableTasksQueue&&(this.busy||this.workerNodes[o].tasksUsage.running>=this.opts.tasksQueueOptions.concurrency)?this.enqueueTask(o,n):this.executeTask(o,n),this.workerChoiceStrategyContext.update(o),this.checkAndEmitEvents(),a}async destroy(){await Promise.all(this.workerNodes.map((async(e,t)=>{this.flushTasksQueue(t),await this.destroyWorker(e.worker)})))}setupHook(){}beforeTaskExecutionHook(e){++this.workerNodes[e].tasksUsage.running}afterTaskExecutionHook(e,t){const s=this.workerNodes[this.getWorkerNodeKey(e)].tasksUsage;--s.running,++s.ran,null!=t.error&&++s.error,this.updateRunTimeTasksUsage(s,t),this.updateWaitTimeTasksUsage(s,t),this.updateEluTasksUsage(s,t)}updateRunTimeTasksUsage(e,t){this.workerChoiceStrategyContext.getTaskStatistics().runTime&&(e.runTime+=t.runTime??0,this.workerChoiceStrategyContext.getTaskStatistics().avgRunTime&&0!==e.ran&&(e.avgRunTime=e.runTime/e.ran),this.workerChoiceStrategyContext.getTaskStatistics().medRunTime&&null!=t.runTime&&(e.runTimeHistory.push(t.runTime),e.medRunTime=T(e.runTimeHistory)))}updateWaitTimeTasksUsage(e,t){this.workerChoiceStrategyContext.getTaskStatistics().waitTime&&(e.waitTime+=t.waitTime??0,this.workerChoiceStrategyContext.getTaskStatistics().avgWaitTime&&0!==e.ran&&(e.avgWaitTime=e.waitTime/e.ran),this.workerChoiceStrategyContext.getTaskStatistics().medWaitTime&&null!=t.waitTime&&(e.waitTimeHistory.push(t.waitTime),e.medWaitTime=T(e.waitTimeHistory)))}updateEluTasksUsage(e,t){this.workerChoiceStrategyContext.getTaskStatistics().elu&&(null!=e.elu&&null!=t.elu?e.elu={idle:e.elu.idle+t.elu.idle,active:e.elu.active+t.elu.active,utilization:(e.elu.utilization+t.elu.utilization)/2}:null!=t.elu&&(e.elu=t.elu))}chooseWorkerNode(){let e;if(this.type===c.dynamic&&!this.full&&this.internalBusy()){const t=this.createAndSetupWorker();this.registerWorkerMessageListener(t,(e=>{const s=this.getWorkerNodeKey(t);var r;r=W.HARD,(e.kill===r||null!=e.kill&&0===this.workerNodes[s].tasksUsage.running)&&(this.flushTasksQueue(s),this.destroyWorker(t))})),e=this.getWorkerNodeKey(t)}else e=this.workerChoiceStrategyContext.execute();return e}createAndSetupWorker(){const e=this.createWorker();return e.on("message",this.opts.messageHandler??p),e.on("error",this.opts.errorHandler??p),e.on("error",(e=>{null!=this.emitter&&this.emitter.emit(m.error,e)})),e.on("error",(()=>{!0===this.opts.restartWorkerOnError&&this.createAndSetupWorker()})),e.on("online",this.opts.onlineHandler??p),e.on("exit",this.opts.exitHandler??p),e.once("exit",(()=>{this.removeWorkerNode(e)})),this.pushWorkerNode(e),this.setWorkerStatistics(e),this.afterWorkerSetup(e),e}workerListener(){return e=>{if(null!=e.id){const t=this.promiseResponseMap.get(e.id);if(null!=t){null!=e.error?(t.reject(e.error),null!=this.emitter&&this.emitter.emit(m.taskError,{error:e.error,errorData:e.errorData})):t.resolve(e.data),this.afterTaskExecutionHook(t.worker,e),this.promiseResponseMap.delete(e.id);const s=this.getWorkerNodeKey(t.worker);!0===this.opts.enableTasksQueue&&this.tasksQueueSize(s)>0&&this.executeTask(s,this.dequeueTask(s))}}}}checkAndEmitEvents(){null!=this.emitter&&(this.busy&&this.emitter?.emit(m.busy,this.info),this.type===c.dynamic&&this.full&&this.emitter?.emit(m.full,this.info))}setWorkerNodeTasksUsage(e,t){e.tasksUsage=t}pushWorkerNode(e){return this.workerNodes.push({worker:e,tasksUsage:{ran:0,running:0,runTime:0,runTimeHistory:new f,avgRunTime:0,medRunTime:0,waitTime:0,waitTimeHistory:new f,avgWaitTime:0,medWaitTime:0,error:0,elu:void 0},tasksQueue:new y})}setWorkerNode(e,t,s,r){this.workerNodes[e]={worker:t,tasksUsage:s,tasksQueue:r}}removeWorkerNode(e){const t=this.getWorkerNodeKey(e);-1!==t&&(this.workerNodes.splice(t,1),this.workerChoiceStrategyContext.remove(t))}executeTask(e,t){this.beforeTaskExecutionHook(e),this.sendToWorker(this.workerNodes[e].worker,t)}enqueueTask(e,t){return this.workerNodes[e].tasksQueue.enqueue(t)}dequeueTask(e){return this.workerNodes[e].tasksQueue.dequeue()}tasksQueueSize(e){return this.workerNodes[e].tasksQueue.size}flushTasksQueue(e){if(this.tasksQueueSize(e)>0)for(let t=0;t<this.tasksQueueSize(e);t++)this.executeTask(e,this.dequeueTask(e))}flushTasksQueues(){for(const[e]of this.workerNodes.entries())this.flushTasksQueue(e)}setWorkerStatistics(e){this.sendToWorker(e,{statistics:{runTime:this.workerChoiceStrategyContext.getTaskStatistics().runTime,waitTime:this.workerChoiceStrategyContext.getTaskStatistics().waitTime,elu:this.workerChoiceStrategyContext.getTaskStatistics().elu}})}}class z extends C{opts;constructor(e,t,s={}){super(e,t,s),this.opts=s}setupHook(){t.setupPrimary({...this.opts.settings,exec:this.filePath})}isMain(){return t.isPrimary}destroyWorker(e){this.sendToWorker(e,{kill:1}),e.kill()}sendToWorker(e,t){e.send(t)}registerWorkerMessageListener(e,t){e.on("message",t)}createWorker(){return t.fork(this.opts.env)}afterWorkerSetup(e){this.registerWorkerMessageListener(e,super.workerListener())}get type(){return c.fixed}get worker(){return d.cluster}get minSize(){return this.numberOfWorkers}get maxSize(){return this.numberOfWorkers}get busy(){return this.internalBusy()}}class U extends z{max;constructor(e,t,s,r={}){super(e,s,r),this.max=t}get type(){return c.dynamic}get maxSize(){return this.max}get busy(){return this.full&&this.internalBusy()}}class Q extends C{opts;constructor(e,t,s={}){super(e,t,s),this.opts=s}isMain(){return o}async destroyWorker(e){this.sendToWorker(e,{kill:1}),await e.terminate()}sendToWorker(e,t){e.postMessage(t)}registerWorkerMessageListener(e,t){e.port2?.on("message",t)}createWorker(){return new n(this.filePath,{env:a,...this.opts.workerOptions})}afterWorkerSetup(e){const{port1:t,port2:s}=new h;e.postMessage({parent:t},[t]),e.port1=t,e.port2=s,this.registerWorkerMessageListener(e,super.workerListener())}get type(){return c.fixed}get worker(){return d.thread}get minSize(){return this.numberOfWorkers}get maxSize(){return this.numberOfWorkers}get busy(){return this.internalBusy()}}class A extends Q{max;constructor(e,t,s,r={}){super(e,s,r),this.max=t}get type(){return c.dynamic}get maxSize(){return this.max}get busy(){return this.full&&this.internalBusy()}}const F="default",M=6e4,D=W.SOFT;class V extends k{isMain;mainWorker;opts;taskFunctions;lastTaskTimestamp;statistics;aliveInterval;constructor(e,t,s,i,o={killBehavior:D,maxInactiveTime:M}){super(e),this.isMain=t,this.mainWorker=i,this.opts=o,this.checkWorkerOptions(this.opts),this.checkTaskFunctions(s),this.isMain||(this.lastTaskTimestamp=r.now(),this.aliveInterval=setInterval(this.checkAlive.bind(this),(this.opts.maxInactiveTime??M)/2),this.checkAlive.bind(this)()),this.mainWorker?.on("message",this.messageListener.bind(this))}checkWorkerOptions(e){this.opts.killBehavior=e.killBehavior??D,this.opts.maxInactiveTime=e.maxInactiveTime??M,delete this.opts.async}checkTaskFunctions(e){if(null==e)throw new Error("taskFunctions parameter is mandatory");if(this.taskFunctions=new Map,"function"==typeof e)this.taskFunctions.set(F,e.bind(this));else{if(!w(e))throw new TypeError("taskFunctions parameter is not a function or a plain object");{let t=!0;for(const[s,r]of Object.entries(e)){if("function"!=typeof r)throw new TypeError("A taskFunctions parameter object value is not a function");this.taskFunctions.set(s,r.bind(this)),t&&(this.taskFunctions.set(F,r.bind(this)),t=!1)}if(t)throw new Error("taskFunctions parameter object is empty")}}}messageListener(e){if(null!=e.id&&null!=e.data){const t=this.getTaskFunction(e.name);"AsyncFunction"===t?.constructor.name?this.runInAsyncScope(this.runAsync.bind(this),this,t,e):this.runInAsyncScope(this.runSync.bind(this),this,t,e)}else null!=e.parent?this.mainWorker=e.parent:null!=e.kill?(null!=this.aliveInterval&&clearInterval(this.aliveInterval),this.emitDestroy()):null!=e.statistics&&(this.statistics=e.statistics)}getMainWorker(){if(null==this.mainWorker)throw new Error("Main worker was not set");return this.mainWorker}checkAlive(){r.now()-this.lastTaskTimestamp>(this.opts.maxInactiveTime??M)&&this.sendToMainWorker({kill:this.opts.killBehavior})}handleError(e){return e}runSync(e,t){try{const s=this.beginTaskPerformance(t),r=e(t.data),{runTime:i,waitTime:o,elu:n}=this.endTaskPerformance(s);this.sendToMainWorker({data:r,runTime:i,waitTime:o,elu:n,id:t.id})}catch(e){const s=this.handleError(e);this.sendToMainWorker({error:s,errorData:t.data,id:t.id})}finally{!this.isMain&&(this.lastTaskTimestamp=r.now())}}runAsync(e,t){const s=this.beginTaskPerformance(t);e(t.data).then((e=>{const{runTime:r,waitTime:i,elu:o}=this.endTaskPerformance(s);return this.sendToMainWorker({data:e,runTime:r,waitTime:i,elu:o,id:t.id}),null})).catch((e=>{const s=this.handleError(e);this.sendToMainWorker({error:s,errorData:t.data,id:t.id})})).finally((()=>{!this.isMain&&(this.lastTaskTimestamp=r.now())})).catch(p)}getTaskFunction(e){e=e??F;const t=this.taskFunctions.get(e);if(null==t)throw new Error(`Task function '${e}' not found`);return t}beginTaskPerformance(e){const t=r.now();return{timestamp:t,...this.statistics.waitTime&&{waitTime:t-(e.timestamp??t)},...this.statistics.elu&&{elu:r.eventLoopUtilization()}}}endTaskPerformance(e){return{...e,...this.statistics.runTime&&{runTime:r.now()-e.timestamp},...this.statistics.elu&&{elu:r.eventLoopUtilization(e.elu)}}}}class _ extends V{constructor(e,s={}){super("worker-cluster-pool:poolifier",t.isPrimary,e,t.worker,s)}sendToMainWorker(e){this.getMainWorker().send(e)}handleError(e){return e instanceof Error?e.message:e}}class H extends V{constructor(e,t={}){super("worker-thread-pool:poolifier",o,e,u,t)}sendToMainWorker(e){this.getMainWorker().postMessage(e)}}export{_ as ClusterWorker,U as DynamicClusterPool,A as DynamicThreadPool,z as FixedClusterPool,Q as FixedThreadPool,W as KillBehaviors,m as PoolEvents,c as PoolTypes,H as ThreadWorker,S as WorkerChoiceStrategies,d as WorkerTypes};
|
|
1
|
+
import e from"node:events";import t from"node:cluster";import r from"node:crypto";import{performance as s}from"node:perf_hooks";import{cpus as i}from"node:os";import{isMainThread as o,Worker as a,SHARE_ENV as n,MessageChannel as h,parentPort as u}from"node:worker_threads";import{AsyncResource as k}from"node:async_hooks";const c=Object.freeze({fixed:"fixed",dynamic:"dynamic"}),d=Object.freeze({cluster:"cluster",thread:"thread"});class l extends e{}const g=Object.freeze({full:"full",busy:"busy",error:"error",taskError:"taskError"}),m=Object.freeze((()=>{})),p={runTime:{median:!1},waitTime:{median:!1},elu:{median:!1}},w=e=>{if(Array.isArray(e)&&0===e.length)return 0;if(Array.isArray(e)&&1===e.length)return e[0];const t=e.slice().sort(((e,t)=>e-t));return(t[t.length-1>>1]+t[t.length>>1])/2},T=e=>"object"==typeof e&&null!==e&&e?.constructor===Object&&"[object Object]"===Object.prototype.toString.call(e),f=Object.freeze({SOFT:"SOFT",HARD:"HARD"});class W extends Array{size;constructor(e=1024,...t){super(),this.checkSize(e),this.size=e,arguments.length>1&&this.push(...t)}push(...e){const t=super.push(...e);return t>this.size&&super.splice(0,t-this.size),this.length}unshift(...e){return super.unshift(...e)>this.size&&super.splice(this.size,e.length),this.length}concat(...e){const t=super.concat(e);return t.size=this.size,t.length>t.size&&t.splice(0,t.length-t.size),t}splice(e,t,...r){let s;return arguments.length>=3&&void 0!==t?(s=super.splice(e,t),this.push(...r)):s=2===arguments.length?super.splice(e,t):super.splice(e),s}resize(e){if(this.checkSize(e),0===e)this.length=0;else if(e<this.size)for(let t=e;t<this.size;t++)super.pop();this.size=e}empty(){return 0===this.length}full(){return this.length===this.size}checkSize(e){if(!Number.isSafeInteger(e))throw new TypeError(`Invalid circular array size: ${e} is not a safe integer`);if(e<0)throw new RangeError(`Invalid circular array size: ${e} < 0`)}}class S{items;head;tail;max;constructor(){this.items={},this.head=0,this.tail=0,this.max=0}get size(){return this.tail-this.head}get maxSize(){return this.max}enqueue(e){return this.items[this.tail]=e,this.tail++,this.size>this.max&&(this.max=this.size),this.size}dequeue(){if(this.size<=0)return;const e=this.items[this.head];return delete this.items[this.head],this.head++,this.head===this.tail&&(this.head=0,this.tail=0),e}peek(){if(!(this.size<=0))return this.items[this.head]}}const y=Object.freeze({ROUND_ROBIN:"ROUND_ROBIN",LEAST_USED:"LEAST_USED",LEAST_BUSY:"LEAST_BUSY",LEAST_ELU:"LEAST_ELU",FAIR_SHARE:"FAIR_SHARE",WEIGHTED_ROUND_ROBIN:"WEIGHTED_ROUND_ROBIN",INTERLEAVED_WEIGHTED_ROUND_ROBIN:"INTERLEAVED_WEIGHTED_ROUND_ROBIN"}),N=Object.freeze({runTime:"runTime",waitTime:"waitTime",elu:"elu"});class x{pool;opts;toggleFindLastFreeWorkerNodeKey=!1;taskStatisticsRequirements={runTime:{aggregate:!1,average:!1,median:!1},waitTime:{aggregate:!1,average:!1,median:!1},elu:{aggregate:!1,average:!1,median:!1}};constructor(e,t=p){this.pool=e,this.opts=t,this.choose=this.choose.bind(this)}setTaskStatisticsRequirements(e){this.taskStatisticsRequirements.runTime.average&&!0===e.runTime?.median&&(this.taskStatisticsRequirements.runTime.average=!1,this.taskStatisticsRequirements.runTime.median=e.runTime.median),this.taskStatisticsRequirements.runTime.median&&!1===e.runTime?.median&&(this.taskStatisticsRequirements.runTime.average=!0,this.taskStatisticsRequirements.runTime.median=e.runTime.median),this.taskStatisticsRequirements.waitTime.average&&!0===e.waitTime?.median&&(this.taskStatisticsRequirements.waitTime.average=!1,this.taskStatisticsRequirements.waitTime.median=e.waitTime.median),this.taskStatisticsRequirements.waitTime.median&&!1===e.waitTime?.median&&(this.taskStatisticsRequirements.waitTime.average=!0,this.taskStatisticsRequirements.waitTime.median=e.waitTime.median),this.taskStatisticsRequirements.elu.average&&!0===e.elu?.median&&(this.taskStatisticsRequirements.elu.average=!1,this.taskStatisticsRequirements.elu.median=e.elu.median),this.taskStatisticsRequirements.elu.median&&!1===e.elu?.median&&(this.taskStatisticsRequirements.elu.average=!0,this.taskStatisticsRequirements.elu.median=e.elu.median)}setOptions(e){e=e??p,this.setTaskStatisticsRequirements(e),this.opts=e}findFreeWorkerNodeKey(){return this.toggleFindLastFreeWorkerNodeKey?(this.toggleFindLastFreeWorkerNodeKey=!1,this.findLastFreeWorkerNodeKey()):(this.toggleFindLastFreeWorkerNodeKey=!0,this.findFirstFreeWorkerNodeKey())}getWorkerTaskRunTime(e){return this.taskStatisticsRequirements.runTime.median?this.pool.workerNodes[e].workerUsage.runTime.median:this.pool.workerNodes[e].workerUsage.runTime.average}getWorkerTaskWaitTime(e){return this.taskStatisticsRequirements.waitTime.median?this.pool.workerNodes[e].workerUsage.runTime.median:this.pool.workerNodes[e].workerUsage.runTime.average}getWorkerTaskElu(e){return this.taskStatisticsRequirements.elu.median?this.pool.workerNodes[e].workerUsage.elu.active.median:this.pool.workerNodes[e].workerUsage.elu.active.average}computeDefaultWorkerWeight(){let e=0;for(const t of i()){const r=t.speed.toString().length-1;e+=1/(t.speed/Math.pow(10,r))*Math.pow(10,r)}return Math.round(e/i().length)}findFirstFreeWorkerNodeKey(){return this.pool.workerNodes.findIndex((e=>0===e.workerUsage.tasks.executing))}findLastFreeWorkerNodeKey(){for(let e=this.pool.workerNodes.length-1;e>=0;e--)if(0===this.pool.workerNodes[e].workerUsage.tasks.executing)return e;return-1}}class v extends x{taskStatisticsRequirements={runTime:{aggregate:!0,average:!0,median:!1},waitTime:{aggregate:!1,average:!1,median:!1},elu:{aggregate:!0,average:!0,median:!1}};workersVirtualTaskEndTimestamp=[];constructor(e,t=p){super(e,t),this.setTaskStatisticsRequirements(this.opts)}reset(){return this.workersVirtualTaskEndTimestamp=[],!0}update(e){return this.computeWorkerVirtualTaskEndTimestamp(e),!0}choose(){let e,t=1/0;for(const[r]of this.pool.workerNodes.entries()){null==this.workersVirtualTaskEndTimestamp[r]&&this.computeWorkerVirtualTaskEndTimestamp(r);const s=this.workersVirtualTaskEndTimestamp[r];s<t&&(t=s,e=r)}return e}remove(e){return this.workersVirtualTaskEndTimestamp.splice(e,1),!0}computeWorkerVirtualTaskEndTimestamp(e){this.workersVirtualTaskEndTimestamp[e]=this.getWorkerVirtualTaskEndTimestamp(e,this.getWorkerVirtualTaskStartTimestamp(e))}getWorkerVirtualTaskEndTimestamp(e,t){return t+(this.opts.measurement===N.elu?this.getWorkerTaskElu(e):this.getWorkerTaskRunTime(e))}getWorkerVirtualTaskStartTimestamp(e){return Math.max(performance.now(),this.workersVirtualTaskEndTimestamp[e]??-1/0)}}class R extends x{currentWorkerNodeId=0;currentRoundId=0;roundWeights;defaultWorkerWeight;constructor(e,t=p){super(e,t),this.setTaskStatisticsRequirements(this.opts),this.defaultWorkerWeight=this.computeDefaultWorkerWeight(),this.roundWeights=this.getRoundWeights()}reset(){return this.currentWorkerNodeId=0,this.currentRoundId=0,!0}update(){return!0}choose(){let e,t;for(let r=this.currentRoundId;r<this.roundWeights.length;r++)for(let s=this.currentWorkerNodeId;s<this.pool.workerNodes.length;s++){if((this.opts.weights?.[s]??this.defaultWorkerWeight)>=this.roundWeights[r]){e=r,t=s;break}}this.currentRoundId=e??0,this.currentWorkerNodeId=t??0;const r=this.currentWorkerNodeId;return this.currentWorkerNodeId===this.pool.workerNodes.length-1?(this.currentWorkerNodeId=0,this.currentRoundId=this.currentRoundId===this.roundWeights.length-1?0:this.currentRoundId+1):this.currentWorkerNodeId=this.currentWorkerNodeId+1,r}remove(e){return this.currentWorkerNodeId===e&&(0===this.pool.workerNodes.length?this.currentWorkerNodeId=0:this.currentWorkerNodeId>this.pool.workerNodes.length-1&&(this.currentWorkerNodeId=this.pool.workerNodes.length-1,this.currentRoundId=this.currentRoundId===this.roundWeights.length-1?0:this.currentRoundId+1)),!0}setOptions(e){super.setOptions(e),this.roundWeights=this.getRoundWeights()}getRoundWeights(){return null==this.opts.weights?[this.defaultWorkerWeight]:[...new Set(Object.values(this.opts.weights).slice().sort(((e,t)=>e-t)))]}}class E extends x{taskStatisticsRequirements={runTime:{aggregate:!0,average:!1,median:!1},waitTime:{aggregate:!0,average:!1,median:!1},elu:{aggregate:!1,average:!1,median:!1}};constructor(e,t=p){super(e,t),this.setTaskStatisticsRequirements(this.opts)}reset(){return!0}update(){return!0}choose(){let e,t=1/0;for(const[r,s]of this.pool.workerNodes.entries()){const i=s.workerUsage.runTime.aggregate+s.workerUsage.waitTime.aggregate;if(0===i)return r;i<t&&(t=i,e=r)}return e}remove(){return!0}}class I extends x{constructor(e,t=p){super(e,t),this.setTaskStatisticsRequirements(this.opts)}reset(){return!0}update(){return!0}choose(){const e=this.findFreeWorkerNodeKey();if(-1!==e)return e;let t,r=1/0;for(const[e,s]of this.pool.workerNodes.entries()){const i=s.workerUsage.tasks,o=i.executed+i.executing+i.queued;if(0===o)return e;o<r&&(r=o,t=e)}return t}remove(){return!0}}class b extends x{taskStatisticsRequirements={runTime:{aggregate:!1,average:!1,median:!1},waitTime:{aggregate:!1,average:!1,median:!1},elu:{aggregate:!0,average:!1,median:!1}};constructor(e,t=p){super(e,t),this.setTaskStatisticsRequirements(this.opts)}reset(){return!0}update(){return!0}choose(){let e,t=1/0;for(const[r,s]of this.pool.workerNodes.entries()){const i=s.workerUsage,o=i.elu?.active.aggregate??0;if(0===o)return r;o<t&&(t=o,e=r)}return e}remove(){return!0}}class O extends x{nextWorkerNodeId=0;constructor(e,t=p){super(e,t),this.setTaskStatisticsRequirements(this.opts)}reset(){return this.nextWorkerNodeId=0,!0}update(){return!0}choose(){const e=this.nextWorkerNodeId;return this.nextWorkerNodeId=this.nextWorkerNodeId===this.pool.workerNodes.length-1?0:this.nextWorkerNodeId+1,e}remove(e){return this.nextWorkerNodeId===e&&(0===this.pool.workerNodes.length?this.nextWorkerNodeId=0:this.nextWorkerNodeId>this.pool.workerNodes.length-1&&(this.nextWorkerNodeId=this.pool.workerNodes.length-1)),!0}}class C extends x{taskStatisticsRequirements={runTime:{aggregate:!0,average:!0,median:!1},waitTime:{aggregate:!1,average:!1,median:!1},elu:{aggregate:!1,average:!1,median:!1}};currentWorkerNodeId=0;defaultWorkerWeight;workerVirtualTaskRunTime=0;constructor(e,t=p){super(e,t),this.setTaskStatisticsRequirements(this.opts),this.defaultWorkerWeight=this.computeDefaultWorkerWeight()}reset(){return this.currentWorkerNodeId=0,this.workerVirtualTaskRunTime=0,!0}update(){return!0}choose(){const e=this.currentWorkerNodeId,t=this.workerVirtualTaskRunTime;return t<(this.opts.weights?.[e]??this.defaultWorkerWeight)?this.workerVirtualTaskRunTime=t+this.getWorkerTaskRunTime(e):(this.currentWorkerNodeId=this.currentWorkerNodeId===this.pool.workerNodes.length-1?0:this.currentWorkerNodeId+1,this.workerVirtualTaskRunTime=0),e}remove(e){return this.currentWorkerNodeId===e&&(0===this.pool.workerNodes.length?this.currentWorkerNodeId=0:this.currentWorkerNodeId>this.pool.workerNodes.length-1&&(this.currentWorkerNodeId=this.pool.workerNodes.length-1),this.workerVirtualTaskRunTime=0),!0}}class q{workerChoiceStrategy;workerChoiceStrategies;constructor(e,t=y.ROUND_ROBIN,r=p){this.workerChoiceStrategy=t,this.execute=this.execute.bind(this),this.workerChoiceStrategies=new Map([[y.ROUND_ROBIN,new(O.bind(this))(e,r)],[y.LEAST_USED,new(I.bind(this))(e,r)],[y.LEAST_BUSY,new(E.bind(this))(e,r)],[y.LEAST_ELU,new(b.bind(this))(e,r)],[y.FAIR_SHARE,new(v.bind(this))(e,r)],[y.WEIGHTED_ROUND_ROBIN,new(C.bind(this))(e,r)],[y.INTERLEAVED_WEIGHTED_ROUND_ROBIN,new(R.bind(this))(e,r)]])}getTaskStatisticsRequirements(){return this.workerChoiceStrategies.get(this.workerChoiceStrategy).taskStatisticsRequirements}setWorkerChoiceStrategy(e){this.workerChoiceStrategy!==e&&(this.workerChoiceStrategy=e),this.workerChoiceStrategies.get(this.workerChoiceStrategy)?.reset()}update(e){return this.workerChoiceStrategies.get(this.workerChoiceStrategy).update(e)}execute(){const e=this.workerChoiceStrategies.get(this.workerChoiceStrategy).choose();if(null==e)throw new Error("Worker node key chosen is null or undefined");return e}remove(e){return this.workerChoiceStrategies.get(this.workerChoiceStrategy).remove(e)}setOptions(e){for(const t of this.workerChoiceStrategies.values())t.setOptions(e)}}class z{numberOfWorkers;filePath;opts;workerNodes=[];emitter;promiseResponseMap=new Map;workerChoiceStrategyContext;constructor(e,t,r){if(this.numberOfWorkers=e,this.filePath=t,this.opts=r,!this.isMain())throw new Error("Cannot start a pool from a worker!");this.checkNumberOfWorkers(this.numberOfWorkers),this.checkFilePath(this.filePath),this.checkPoolOptions(this.opts),this.chooseWorkerNode=this.chooseWorkerNode.bind(this),this.executeTask=this.executeTask.bind(this),this.enqueueTask=this.enqueueTask.bind(this),this.checkAndEmitEvents=this.checkAndEmitEvents.bind(this),!0===this.opts.enableEvents&&(this.emitter=new l),this.workerChoiceStrategyContext=new q(this,this.opts.workerChoiceStrategy,this.opts.workerChoiceStrategyOptions),this.setupHook();for(let e=1;e<=this.numberOfWorkers;e++)this.createAndSetupWorker()}checkFilePath(e){if(null==e||"string"==typeof e&&0===e.trim().length)throw new Error("Please specify a file with a worker implementation")}checkNumberOfWorkers(e){if(null==e)throw new Error("Cannot instantiate a pool without specifying the number of workers");if(!Number.isSafeInteger(e))throw new TypeError("Cannot instantiate a pool with a non safe integer number of workers");if(e<0)throw new RangeError("Cannot instantiate a pool with a negative number of workers");if(this.type===c.fixed&&0===e)throw new Error("Cannot instantiate a fixed pool with no worker")}checkPoolOptions(e){if(!T(e))throw new TypeError("Invalid pool options: must be a plain object");this.opts.workerChoiceStrategy=e.workerChoiceStrategy??y.ROUND_ROBIN,this.checkValidWorkerChoiceStrategy(this.opts.workerChoiceStrategy),this.opts.workerChoiceStrategyOptions=e.workerChoiceStrategyOptions??p,this.checkValidWorkerChoiceStrategyOptions(this.opts.workerChoiceStrategyOptions),this.opts.restartWorkerOnError=e.restartWorkerOnError??!0,this.opts.enableEvents=e.enableEvents??!0,this.opts.enableTasksQueue=e.enableTasksQueue??!1,this.opts.enableTasksQueue&&(this.checkValidTasksQueueOptions(e.tasksQueueOptions),this.opts.tasksQueueOptions=this.buildTasksQueueOptions(e.tasksQueueOptions))}checkValidWorkerChoiceStrategy(e){if(!Object.values(y).includes(e))throw new Error(`Invalid worker choice strategy '${e}'`)}checkValidWorkerChoiceStrategyOptions(e){if(!T(e))throw new TypeError("Invalid worker choice strategy options: must be a plain object");if(null!=e.weights&&Object.keys(e.weights).length!==this.maxSize)throw new Error("Invalid worker choice strategy options: must have a weight for each worker node")}checkValidTasksQueueOptions(e){if(null!=e&&!T(e))throw new TypeError("Invalid tasks queue options: must be a plain object");if(e?.concurrency<=0)throw new Error(`Invalid worker tasks concurrency '${e.concurrency}'`)}get info(){return{type:this.type,worker:this.worker,minSize:this.minSize,maxSize:this.maxSize,workerNodes:this.workerNodes.length,idleWorkerNodes:this.workerNodes.reduce(((e,t)=>0===t.workerUsage.tasks.executing?e+1:e),0),busyWorkerNodes:this.workerNodes.reduce(((e,t)=>t.workerUsage.tasks.executing>0?e+1:e),0),executedTasks:this.workerNodes.reduce(((e,t)=>e+t.workerUsage.tasks.executed),0),executingTasks:this.workerNodes.reduce(((e,t)=>e+t.workerUsage.tasks.executing),0),queuedTasks:this.workerNodes.reduce(((e,t)=>e+t.tasksQueue.size),0),maxQueuedTasks:this.workerNodes.reduce(((e,t)=>e+t.tasksQueue.maxSize),0),failedTasks:this.workerNodes.reduce(((e,t)=>e+t.workerUsage.tasks.failed),0)}}getWorkerNodeKey(e){return this.workerNodes.findIndex((t=>t.worker===e))}setWorkerChoiceStrategy(e,t){this.checkValidWorkerChoiceStrategy(e),this.opts.workerChoiceStrategy=e,this.workerChoiceStrategyContext.setWorkerChoiceStrategy(this.opts.workerChoiceStrategy),null!=t&&this.setWorkerChoiceStrategyOptions(t);for(const e of this.workerNodes)this.setWorkerNodeTasksUsage(e,this.getWorkerUsage(e.worker)),this.setWorkerStatistics(e.worker)}setWorkerChoiceStrategyOptions(e){this.checkValidWorkerChoiceStrategyOptions(e),this.opts.workerChoiceStrategyOptions=e,this.workerChoiceStrategyContext.setOptions(this.opts.workerChoiceStrategyOptions)}enableTasksQueue(e,t){!0!==this.opts.enableTasksQueue||e||this.flushTasksQueues(),this.opts.enableTasksQueue=e,this.setTasksQueueOptions(t)}setTasksQueueOptions(e){!0===this.opts.enableTasksQueue?(this.checkValidTasksQueueOptions(e),this.opts.tasksQueueOptions=this.buildTasksQueueOptions(e)):null!=this.opts.tasksQueueOptions&&delete this.opts.tasksQueueOptions}buildTasksQueueOptions(e){return{concurrency:e?.concurrency??1}}get full(){return this.workerNodes.length>=this.maxSize}internalBusy(){return-1===this.workerNodes.findIndex((e=>0===e.workerUsage.tasks.executing))}async execute(e,t){const i=s.now(),o=this.chooseWorkerNode(),a={name:t,data:e??{},timestamp:i,id:r.randomUUID()},n=new Promise(((e,t)=>{this.promiseResponseMap.set(a.id,{resolve:e,reject:t,worker:this.workerNodes[o].worker})}));return!0===this.opts.enableTasksQueue&&(this.busy||this.workerNodes[o].workerUsage.tasks.executing>=this.opts.tasksQueueOptions.concurrency)?this.enqueueTask(o,a):this.executeTask(o,a),this.workerChoiceStrategyContext.update(o),this.checkAndEmitEvents(),n}async destroy(){await Promise.all(this.workerNodes.map((async(e,t)=>{this.flushTasksQueue(t),await this.destroyWorker(e.worker)})))}setupHook(){}beforeTaskExecutionHook(e,t){const r=this.workerNodes[e].workerUsage;++r.tasks.executing,this.updateWaitTimeWorkerUsage(r,t)}afterTaskExecutionHook(e,t){const r=this.workerNodes[this.getWorkerNodeKey(e)].workerUsage;this.updateTaskStatisticsWorkerUsage(r,t),this.updateRunTimeWorkerUsage(r,t),this.updateEluWorkerUsage(r,t)}updateTaskStatisticsWorkerUsage(e,t){const r=e.tasks;--r.executing,++r.executed,null!=t.taskError&&++r.failed}updateRunTimeWorkerUsage(e,t){this.workerChoiceStrategyContext.getTaskStatisticsRequirements().runTime.aggregate&&(e.runTime.aggregate+=t.taskPerformance?.runTime??0,this.workerChoiceStrategyContext.getTaskStatisticsRequirements().runTime.average&&0!==e.tasks.executed&&(e.runTime.average=e.runTime.aggregate/(e.tasks.executed-e.tasks.failed)),this.workerChoiceStrategyContext.getTaskStatisticsRequirements().runTime.median&&null!=t.taskPerformance?.runTime&&(e.runTime.history.push(t.taskPerformance.runTime),e.runTime.median=w(e.runTime.history)))}updateWaitTimeWorkerUsage(e,t){const r=s.now(),i=r-(t.timestamp??r);this.workerChoiceStrategyContext.getTaskStatisticsRequirements().waitTime.aggregate&&(e.waitTime.aggregate+=i??0,this.workerChoiceStrategyContext.getTaskStatisticsRequirements().waitTime.average&&0!==e.tasks.executed&&(e.waitTime.average=e.waitTime.aggregate/(e.tasks.executed-e.tasks.failed)),this.workerChoiceStrategyContext.getTaskStatisticsRequirements().waitTime.median&&null!=i&&(e.waitTime.history.push(i),e.waitTime.median=w(e.waitTime.history)))}updateEluWorkerUsage(e,t){if(this.workerChoiceStrategyContext.getTaskStatisticsRequirements().elu.aggregate){if(null!=e.elu&&null!=t.taskPerformance?.elu?(e.elu.idle.aggregate+=t.taskPerformance.elu.idle,e.elu.active.aggregate+=t.taskPerformance.elu.active,e.elu.utilization=(e.elu.utilization+t.taskPerformance.elu.utilization)/2):null!=t.taskPerformance?.elu&&(e.elu.idle.aggregate=t.taskPerformance.elu.idle,e.elu.active.aggregate=t.taskPerformance.elu.active,e.elu.utilization=t.taskPerformance.elu.utilization),this.workerChoiceStrategyContext.getTaskStatisticsRequirements().elu.average&&0!==e.tasks.executed){const t=e.tasks.executed-e.tasks.failed;e.elu.idle.average=e.elu.idle.aggregate/t,e.elu.active.average=e.elu.active.aggregate/t}this.workerChoiceStrategyContext.getTaskStatisticsRequirements().elu.median&&null!=t.taskPerformance?.elu&&(e.elu.idle.history.push(t.taskPerformance.elu.idle),e.elu.active.history.push(t.taskPerformance.elu.active),e.elu.idle.median=w(e.elu.idle.history),e.elu.active.median=w(e.elu.active.history))}}chooseWorkerNode(){let e;if(this.type===c.dynamic&&!this.full&&this.internalBusy()){const t=this.createAndSetupWorker();this.registerWorkerMessageListener(t,(e=>{const r=this.getWorkerNodeKey(t);var s;s=f.HARD,(e.kill===s||null!=e.kill&&0===this.workerNodes[r].workerUsage.tasks.executing)&&(this.flushTasksQueue(r),this.destroyWorker(t))})),e=this.getWorkerNodeKey(t)}else e=this.workerChoiceStrategyContext.execute();return e}createAndSetupWorker(){const e=this.createWorker();return e.on("message",this.opts.messageHandler??m),e.on("error",this.opts.errorHandler??m),e.on("error",(e=>{null!=this.emitter&&this.emitter.emit(g.error,e)})),e.on("error",(()=>{!0===this.opts.restartWorkerOnError&&this.createAndSetupWorker()})),e.on("online",this.opts.onlineHandler??m),e.on("exit",this.opts.exitHandler??m),e.once("exit",(()=>{this.removeWorkerNode(e)})),this.pushWorkerNode(e),this.setWorkerStatistics(e),this.afterWorkerSetup(e),e}workerListener(){return e=>{if(null!=e.id){const t=this.promiseResponseMap.get(e.id);if(null!=t){null!=e.taskError?(t.reject(e.taskError.message),null!=this.emitter&&this.emitter.emit(g.taskError,e.taskError)):t.resolve(e.data),this.afterTaskExecutionHook(t.worker,e),this.promiseResponseMap.delete(e.id);const r=this.getWorkerNodeKey(t.worker);!0===this.opts.enableTasksQueue&&this.tasksQueueSize(r)>0&&this.executeTask(r,this.dequeueTask(r))}}}}checkAndEmitEvents(){null!=this.emitter&&(this.busy&&this.emitter?.emit(g.busy,this.info),this.type===c.dynamic&&this.full&&this.emitter?.emit(g.full,this.info))}setWorkerNodeTasksUsage(e,t){e.workerUsage=t}pushWorkerNode(e){return this.workerNodes.push({worker:e,workerUsage:this.getWorkerUsage(e),tasksQueue:new S})}removeWorkerNode(e){const t=this.getWorkerNodeKey(e);-1!==t&&(this.workerNodes.splice(t,1),this.workerChoiceStrategyContext.remove(t))}executeTask(e,t){this.beforeTaskExecutionHook(e,t),this.sendToWorker(this.workerNodes[e].worker,t)}enqueueTask(e,t){return this.workerNodes[e].tasksQueue.enqueue(t)}dequeueTask(e){return this.workerNodes[e].tasksQueue.dequeue()}tasksQueueSize(e){return this.workerNodes[e].tasksQueue.size}flushTasksQueue(e){if(this.tasksQueueSize(e)>0)for(let t=0;t<this.tasksQueueSize(e);t++)this.executeTask(e,this.dequeueTask(e))}flushTasksQueues(){for(const[e]of this.workerNodes.entries())this.flushTasksQueue(e)}setWorkerStatistics(e){this.sendToWorker(e,{statistics:{runTime:this.workerChoiceStrategyContext.getTaskStatisticsRequirements().runTime.aggregate,elu:this.workerChoiceStrategyContext.getTaskStatisticsRequirements().elu.aggregate}})}getWorkerUsage(e){return{tasks:this.getTaskStatistics(e),runTime:{aggregate:0,average:0,median:0,history:new W},waitTime:{aggregate:0,average:0,median:0,history:new W},elu:{idle:{aggregate:0,average:0,median:0,history:new W},active:{aggregate:0,average:0,median:0,history:new W},utilization:0}}}getTaskStatistics(e){const t=this.workerNodes[this.getWorkerNodeKey(e)]?.tasksQueue?.size;return{executed:0,executing:0,get queued(){return t??0},failed:0}}}class U extends z{opts;constructor(e,t,r={}){super(e,t,r),this.opts=r}setupHook(){t.setupPrimary({...this.opts.settings,exec:this.filePath})}isMain(){return t.isPrimary}destroyWorker(e){this.sendToWorker(e,{kill:1}),e.kill()}sendToWorker(e,t){e.send(t)}registerWorkerMessageListener(e,t){e.on("message",t)}createWorker(){return t.fork(this.opts.env)}afterWorkerSetup(e){this.registerWorkerMessageListener(e,super.workerListener())}get type(){return c.fixed}get worker(){return d.cluster}get minSize(){return this.numberOfWorkers}get maxSize(){return this.numberOfWorkers}get busy(){return this.internalBusy()}}class A extends U{max;constructor(e,t,r,s={}){super(e,r,s),this.max=t}get type(){return c.dynamic}get maxSize(){return this.max}get busy(){return this.full&&this.internalBusy()}}class Q extends z{opts;constructor(e,t,r={}){super(e,t,r),this.opts=r}isMain(){return o}async destroyWorker(e){this.sendToWorker(e,{kill:1}),await e.terminate()}sendToWorker(e,t){e.postMessage(t)}registerWorkerMessageListener(e,t){e.port2?.on("message",t)}createWorker(){return new a(this.filePath,{env:n,...this.opts.workerOptions})}afterWorkerSetup(e){const{port1:t,port2:r}=new h;e.postMessage({parent:t},[t]),e.port1=t,e.port2=r,this.registerWorkerMessageListener(e,super.workerListener())}get type(){return c.fixed}get worker(){return d.thread}get minSize(){return this.numberOfWorkers}get maxSize(){return this.numberOfWorkers}get busy(){return this.internalBusy()}}class F extends Q{max;constructor(e,t,r,s={}){super(e,r,s),this.max=t}get type(){return c.dynamic}get maxSize(){return this.max}get busy(){return this.full&&this.internalBusy()}}const M="default",P=6e4,_=f.SOFT;class L extends k{isMain;mainWorker;opts;taskFunctions;lastTaskTimestamp;statistics;aliveInterval;constructor(e,t,r,i,o={killBehavior:_,maxInactiveTime:P}){super(e),this.isMain=t,this.mainWorker=i,this.opts=o,this.checkWorkerOptions(this.opts),this.checkTaskFunctions(r),this.isMain||(this.lastTaskTimestamp=s.now(),this.aliveInterval=setInterval(this.checkAlive.bind(this),(this.opts.maxInactiveTime??P)/2),this.checkAlive.bind(this)()),this.mainWorker?.on("message",this.messageListener.bind(this))}checkWorkerOptions(e){this.opts.killBehavior=e.killBehavior??_,this.opts.maxInactiveTime=e.maxInactiveTime??P,delete this.opts.async}checkTaskFunctions(e){if(null==e)throw new Error("taskFunctions parameter is mandatory");if(this.taskFunctions=new Map,"function"==typeof e)this.taskFunctions.set(M,e.bind(this));else{if(!T(e))throw new TypeError("taskFunctions parameter is not a function or a plain object");{let t=!0;for(const[r,s]of Object.entries(e)){if("function"!=typeof s)throw new TypeError("A taskFunctions parameter object value is not a function");this.taskFunctions.set(r,s.bind(this)),t&&(this.taskFunctions.set(M,s.bind(this)),t=!1)}if(t)throw new Error("taskFunctions parameter object is empty")}}}messageListener(e){if(null!=e.id&&null!=e.data){const t=this.getTaskFunction(e.name);"AsyncFunction"===t?.constructor.name?this.runInAsyncScope(this.runAsync.bind(this),this,t,e):this.runInAsyncScope(this.runSync.bind(this),this,t,e)}else null!=e.parent?this.mainWorker=e.parent:null!=e.kill?(null!=this.aliveInterval&&clearInterval(this.aliveInterval),this.emitDestroy()):null!=e.statistics&&(this.statistics=e.statistics)}getMainWorker(){if(null==this.mainWorker)throw new Error("Main worker was not set");return this.mainWorker}checkAlive(){s.now()-this.lastTaskTimestamp>(this.opts.maxInactiveTime??P)&&this.sendToMainWorker({kill:this.opts.killBehavior})}handleError(e){return e}runSync(e,t){try{let r=this.beginTaskPerformance();const s=e(t.data);r=this.endTaskPerformance(r),this.sendToMainWorker({data:s,taskPerformance:r,id:t.id})}catch(e){const r=this.handleError(e);this.sendToMainWorker({taskError:{message:r,data:t.data},id:t.id})}finally{!this.isMain&&(this.lastTaskTimestamp=s.now())}}runAsync(e,t){let r=this.beginTaskPerformance();e(t.data).then((e=>(r=this.endTaskPerformance(r),this.sendToMainWorker({data:e,taskPerformance:r,id:t.id}),null))).catch((e=>{const r=this.handleError(e);this.sendToMainWorker({taskError:{message:r,data:t.data},id:t.id})})).finally((()=>{!this.isMain&&(this.lastTaskTimestamp=s.now())})).catch(m)}getTaskFunction(e){e=e??M;const t=this.taskFunctions.get(e);if(null==t)throw new Error(`Task function '${e}' not found`);return t}beginTaskPerformance(){return{timestamp:s.now(),...this.statistics.elu&&{elu:s.eventLoopUtilization()}}}endTaskPerformance(e){return{...e,...this.statistics.runTime&&{runTime:s.now()-e.timestamp},...this.statistics.elu&&{elu:s.eventLoopUtilization(e.elu)}}}}class V extends L{constructor(e,r={}){super("worker-cluster-pool:poolifier",t.isPrimary,e,t.worker,r)}sendToMainWorker(e){this.getMainWorker().send(e)}handleError(e){return e instanceof Error?e.message:e}}class D extends L{constructor(e,t={}){super("worker-thread-pool:poolifier",o,e,u,t)}sendToMainWorker(e){this.getMainWorker().postMessage(e)}}export{V as ClusterWorker,A as DynamicClusterPool,F as DynamicThreadPool,U as FixedClusterPool,Q as FixedThreadPool,f as KillBehaviors,N as Measurements,g as PoolEvents,c as PoolTypes,D as ThreadWorker,y as WorkerChoiceStrategies,d as WorkerTypes};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MessageValue, PromiseResponseWrapper } from '../utility-types';
|
|
2
2
|
import { type IPool, PoolEmitter, type PoolInfo, type PoolOptions, type PoolType, type TasksQueueOptions, type WorkerType } from './pool';
|
|
3
|
-
import type { IWorker, WorkerNode } from './worker';
|
|
3
|
+
import type { IWorker, Task, WorkerNode } from './worker';
|
|
4
4
|
import { type WorkerChoiceStrategy, type WorkerChoiceStrategyOptions } from './selection-strategies/selection-strategies-types';
|
|
5
5
|
import { WorkerChoiceStrategyContext } from './selection-strategies/worker-choice-strategy-context';
|
|
6
6
|
/**
|
|
@@ -29,8 +29,6 @@ export declare abstract class AbstractPool<Worker extends IWorker, Data = unknow
|
|
|
29
29
|
protected promiseResponseMap: Map<string, PromiseResponseWrapper<Worker, Response>>;
|
|
30
30
|
/**
|
|
31
31
|
* Worker choice strategy context referencing a worker choice algorithm implementation.
|
|
32
|
-
*
|
|
33
|
-
* Default to a round robin algorithm.
|
|
34
32
|
*/
|
|
35
33
|
protected workerChoiceStrategyContext: WorkerChoiceStrategyContext<Worker, Data, Response>;
|
|
36
34
|
/**
|
|
@@ -122,8 +120,9 @@ export declare abstract class AbstractPool<Worker extends IWorker, Data = unknow
|
|
|
122
120
|
* Can be overridden.
|
|
123
121
|
*
|
|
124
122
|
* @param workerNodeKey - The worker node key.
|
|
123
|
+
* @param task - The task to execute.
|
|
125
124
|
*/
|
|
126
|
-
protected beforeTaskExecutionHook(workerNodeKey: number): void;
|
|
125
|
+
protected beforeTaskExecutionHook(workerNodeKey: number, task: Task<Data>): void;
|
|
127
126
|
/**
|
|
128
127
|
* Hook executed after the worker task execution.
|
|
129
128
|
* Can be overridden.
|
|
@@ -132,9 +131,10 @@ export declare abstract class AbstractPool<Worker extends IWorker, Data = unknow
|
|
|
132
131
|
* @param message - The received message.
|
|
133
132
|
*/
|
|
134
133
|
protected afterTaskExecutionHook(worker: Worker, message: MessageValue<Response>): void;
|
|
135
|
-
private
|
|
136
|
-
private
|
|
137
|
-
private
|
|
134
|
+
private updateTaskStatisticsWorkerUsage;
|
|
135
|
+
private updateRunTimeWorkerUsage;
|
|
136
|
+
private updateWaitTimeWorkerUsage;
|
|
137
|
+
private updateEluWorkerUsage;
|
|
138
138
|
/**
|
|
139
139
|
* Chooses a worker node for the next task.
|
|
140
140
|
*
|
|
@@ -186,7 +186,7 @@ export declare abstract class AbstractPool<Worker extends IWorker, Data = unknow
|
|
|
186
186
|
* Sets the given worker node its tasks usage in the pool.
|
|
187
187
|
*
|
|
188
188
|
* @param workerNode - The worker node.
|
|
189
|
-
* @param
|
|
189
|
+
* @param workerUsage - The worker usage.
|
|
190
190
|
*/
|
|
191
191
|
private setWorkerNodeTasksUsage;
|
|
192
192
|
/**
|
|
@@ -196,15 +196,6 @@ export declare abstract class AbstractPool<Worker extends IWorker, Data = unknow
|
|
|
196
196
|
* @returns The worker nodes length.
|
|
197
197
|
*/
|
|
198
198
|
private pushWorkerNode;
|
|
199
|
-
/**
|
|
200
|
-
* Sets the given worker in the pool worker nodes.
|
|
201
|
-
*
|
|
202
|
-
* @param workerNodeKey - The worker node key.
|
|
203
|
-
* @param worker - The worker.
|
|
204
|
-
* @param tasksUsage - The worker tasks usage.
|
|
205
|
-
* @param tasksQueue - The worker task queue.
|
|
206
|
-
*/
|
|
207
|
-
private setWorkerNode;
|
|
208
199
|
/**
|
|
209
200
|
* Removes the given worker from the pool worker nodes.
|
|
210
201
|
*
|
|
@@ -218,4 +209,6 @@ export declare abstract class AbstractPool<Worker extends IWorker, Data = unknow
|
|
|
218
209
|
private flushTasksQueue;
|
|
219
210
|
private flushTasksQueues;
|
|
220
211
|
private setWorkerStatistics;
|
|
212
|
+
private getWorkerUsage;
|
|
213
|
+
private getTaskStatistics;
|
|
221
214
|
}
|
|
@@ -12,7 +12,7 @@ export interface ClusterPoolOptions extends PoolOptions<Worker> {
|
|
|
12
12
|
*
|
|
13
13
|
* @see https://nodejs.org/api/cluster.html#cluster_cluster_fork_env
|
|
14
14
|
*/
|
|
15
|
-
env?:
|
|
15
|
+
env?: Record<string, unknown>;
|
|
16
16
|
/**
|
|
17
17
|
* Cluster settings.
|
|
18
18
|
*
|
|
@@ -25,8 +25,6 @@ export interface ClusterPoolOptions extends PoolOptions<Worker> {
|
|
|
25
25
|
*
|
|
26
26
|
* It is possible to perform tasks in sync or asynchronous mode as you prefer.
|
|
27
27
|
*
|
|
28
|
-
* This pool selects the workers in a round robin fashion.
|
|
29
|
-
*
|
|
30
28
|
* @typeParam Data - Type of data sent to the worker. This can only be serializable data.
|
|
31
29
|
* @typeParam Response - Type of execution response. This can only be serializable data.
|
|
32
30
|
* @author [Christopher Quadflieg](https://github.com/Shinigami92)
|
package/lib/pools/pool.d.ts
CHANGED
|
@@ -59,9 +59,11 @@ export interface PoolInfo {
|
|
|
59
59
|
workerNodes: number;
|
|
60
60
|
idleWorkerNodes: number;
|
|
61
61
|
busyWorkerNodes: number;
|
|
62
|
-
|
|
62
|
+
executedTasks: number;
|
|
63
|
+
executingTasks: number;
|
|
63
64
|
queuedTasks: number;
|
|
64
65
|
maxQueuedTasks: number;
|
|
66
|
+
failedTasks: number;
|
|
65
67
|
}
|
|
66
68
|
/**
|
|
67
69
|
* Worker tasks queue options.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { IPool } from '../pool';
|
|
2
2
|
import type { IWorker } from '../worker';
|
|
3
|
-
import type { IWorkerChoiceStrategy,
|
|
3
|
+
import type { IWorkerChoiceStrategy, TaskStatisticsRequirements, WorkerChoiceStrategyOptions } from './selection-strategies-types';
|
|
4
4
|
/**
|
|
5
5
|
* Worker choice strategy abstract base class.
|
|
6
6
|
*
|
|
@@ -16,7 +16,7 @@ export declare abstract class AbstractWorkerChoiceStrategy<Worker extends IWorke
|
|
|
16
16
|
*/
|
|
17
17
|
private toggleFindLastFreeWorkerNodeKey;
|
|
18
18
|
/** @inheritDoc */
|
|
19
|
-
readonly
|
|
19
|
+
readonly taskStatisticsRequirements: TaskStatisticsRequirements;
|
|
20
20
|
/**
|
|
21
21
|
* Constructs a worker choice strategy bound to the pool.
|
|
22
22
|
*
|
|
@@ -24,7 +24,7 @@ export declare abstract class AbstractWorkerChoiceStrategy<Worker extends IWorke
|
|
|
24
24
|
* @param opts - The worker choice strategy options.
|
|
25
25
|
*/
|
|
26
26
|
constructor(pool: IPool<Worker, Data, Response>, opts?: WorkerChoiceStrategyOptions);
|
|
27
|
-
protected
|
|
27
|
+
protected setTaskStatisticsRequirements(opts: WorkerChoiceStrategyOptions): void;
|
|
28
28
|
/** @inheritDoc */
|
|
29
29
|
abstract reset(): boolean;
|
|
30
30
|
/** @inheritDoc */
|
|
@@ -43,8 +43,8 @@ export declare abstract class AbstractWorkerChoiceStrategy<Worker extends IWorke
|
|
|
43
43
|
protected findFreeWorkerNodeKey(): number;
|
|
44
44
|
/**
|
|
45
45
|
* Gets the worker task runtime.
|
|
46
|
-
* If the
|
|
47
|
-
* If the
|
|
46
|
+
* If the task statistics require the average runtime, the average runtime is returned.
|
|
47
|
+
* If the task statistics require the median runtime , the median runtime is returned.
|
|
48
48
|
*
|
|
49
49
|
* @param workerNodeKey - The worker node key.
|
|
50
50
|
* @returns The worker task runtime.
|
|
@@ -52,18 +52,27 @@ export declare abstract class AbstractWorkerChoiceStrategy<Worker extends IWorke
|
|
|
52
52
|
protected getWorkerTaskRunTime(workerNodeKey: number): number;
|
|
53
53
|
/**
|
|
54
54
|
* Gets the worker task wait time.
|
|
55
|
-
* If the
|
|
56
|
-
* If the
|
|
55
|
+
* If the task statistics require the average wait time, the average wait time is returned.
|
|
56
|
+
* If the task statistics require the median wait time, the median wait time is returned.
|
|
57
57
|
*
|
|
58
58
|
* @param workerNodeKey - The worker node key.
|
|
59
59
|
* @returns The worker task wait time.
|
|
60
60
|
*/
|
|
61
|
-
protected
|
|
61
|
+
protected getWorkerTaskWaitTime(workerNodeKey: number): number;
|
|
62
|
+
/**
|
|
63
|
+
* Gets the worker task ELU.
|
|
64
|
+
* If the task statistics require the average ELU, the average ELU is returned.
|
|
65
|
+
* If the task statistics require the median ELU, the median ELU is returned.
|
|
66
|
+
*
|
|
67
|
+
* @param workerNodeKey - The worker node key.
|
|
68
|
+
* @returns The worker task ELU.
|
|
69
|
+
*/
|
|
70
|
+
protected getWorkerTaskElu(workerNodeKey: number): number;
|
|
62
71
|
protected computeDefaultWorkerWeight(): number;
|
|
63
72
|
/**
|
|
64
73
|
* Finds the first free worker node key based on the number of tasks the worker has applied.
|
|
65
74
|
*
|
|
66
|
-
* If a worker is found with `0`
|
|
75
|
+
* If a worker is found with `0` executing tasks, it is detected as free and its worker node key is returned.
|
|
67
76
|
*
|
|
68
77
|
* If no free worker is found, `-1` is returned.
|
|
69
78
|
*
|
|
@@ -73,7 +82,7 @@ export declare abstract class AbstractWorkerChoiceStrategy<Worker extends IWorke
|
|
|
73
82
|
/**
|
|
74
83
|
* Finds the last free worker node key based on the number of tasks the worker has applied.
|
|
75
84
|
*
|
|
76
|
-
* If a worker is found with `0`
|
|
85
|
+
* If a worker is found with `0` executing tasks, it is detected as free and its worker node key is returned.
|
|
77
86
|
*
|
|
78
87
|
* If no free worker is found, `-1` is returned.
|
|
79
88
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { IPool } from '../pool';
|
|
2
2
|
import type { IWorker } from '../worker';
|
|
3
3
|
import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy';
|
|
4
|
-
import type
|
|
4
|
+
import { type IWorkerChoiceStrategy, type TaskStatisticsRequirements, type WorkerChoiceStrategyOptions } from './selection-strategies-types';
|
|
5
5
|
/**
|
|
6
6
|
* Selects the next worker with a fair share scheduling algorithm.
|
|
7
7
|
* Loosely modeled after the fair queueing algorithm: https://en.wikipedia.org/wiki/Fair_queuing.
|
|
@@ -12,7 +12,7 @@ import type { IWorkerChoiceStrategy, TaskStatistics, WorkerChoiceStrategyOptions
|
|
|
12
12
|
*/
|
|
13
13
|
export declare class FairShareWorkerChoiceStrategy<Worker extends IWorker, Data = unknown, Response = unknown> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy {
|
|
14
14
|
/** @inheritDoc */
|
|
15
|
-
readonly
|
|
15
|
+
readonly taskStatisticsRequirements: TaskStatisticsRequirements;
|
|
16
16
|
/**
|
|
17
17
|
* Workers' virtual task end execution timestamp.
|
|
18
18
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { IPool } from '../pool';
|
|
2
2
|
import type { IWorker } from '../worker';
|
|
3
3
|
import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy';
|
|
4
|
-
import type { IWorkerChoiceStrategy,
|
|
4
|
+
import type { IWorkerChoiceStrategy, TaskStatisticsRequirements, WorkerChoiceStrategyOptions } from './selection-strategies-types';
|
|
5
5
|
/**
|
|
6
6
|
* Selects the least busy worker.
|
|
7
7
|
*
|
|
@@ -11,7 +11,7 @@ import type { IWorkerChoiceStrategy, TaskStatistics, WorkerChoiceStrategyOptions
|
|
|
11
11
|
*/
|
|
12
12
|
export declare class LeastBusyWorkerChoiceStrategy<Worker extends IWorker, Data = unknown, Response = unknown> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy {
|
|
13
13
|
/** @inheritDoc */
|
|
14
|
-
readonly
|
|
14
|
+
readonly taskStatisticsRequirements: TaskStatisticsRequirements;
|
|
15
15
|
/** @inheritDoc */
|
|
16
16
|
constructor(pool: IPool<Worker, Data, Response>, opts?: WorkerChoiceStrategyOptions);
|
|
17
17
|
/** @inheritDoc */
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { IPool } from '../pool';
|
|
2
|
+
import type { IWorker } from '../worker';
|
|
3
|
+
import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy';
|
|
4
|
+
import type { IWorkerChoiceStrategy, TaskStatisticsRequirements, WorkerChoiceStrategyOptions } from './selection-strategies-types';
|
|
5
|
+
/**
|
|
6
|
+
* Selects the worker with the least ELU.
|
|
7
|
+
*
|
|
8
|
+
* @typeParam Worker - Type of worker which manages the strategy.
|
|
9
|
+
* @typeParam Data - Type of data sent to the worker. This can only be serializable data.
|
|
10
|
+
* @typeParam Response - Type of execution response. This can only be serializable data.
|
|
11
|
+
*/
|
|
12
|
+
export declare class LeastEluWorkerChoiceStrategy<Worker extends IWorker, Data = unknown, Response = unknown> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy {
|
|
13
|
+
/** @inheritDoc */
|
|
14
|
+
readonly taskStatisticsRequirements: TaskStatisticsRequirements;
|
|
15
|
+
/** @inheritDoc */
|
|
16
|
+
constructor(pool: IPool<Worker, Data, Response>, opts?: WorkerChoiceStrategyOptions);
|
|
17
|
+
/** @inheritDoc */
|
|
18
|
+
reset(): boolean;
|
|
19
|
+
/** @inheritDoc */
|
|
20
|
+
update(): boolean;
|
|
21
|
+
/** @inheritDoc */
|
|
22
|
+
choose(): number;
|
|
23
|
+
/** @inheritDoc */
|
|
24
|
+
remove(): boolean;
|
|
25
|
+
}
|
|
@@ -14,6 +14,12 @@ export declare const WorkerChoiceStrategies: Readonly<{
|
|
|
14
14
|
* Least busy worker selection strategy.
|
|
15
15
|
*/
|
|
16
16
|
readonly LEAST_BUSY: "LEAST_BUSY";
|
|
17
|
+
/**
|
|
18
|
+
* Least ELU worker selection strategy.
|
|
19
|
+
*
|
|
20
|
+
* @experimental
|
|
21
|
+
*/
|
|
22
|
+
readonly LEAST_ELU: "LEAST_ELU";
|
|
17
23
|
/**
|
|
18
24
|
* Fair share worker selection strategy.
|
|
19
25
|
*/
|
|
@@ -33,22 +39,53 @@ export declare const WorkerChoiceStrategies: Readonly<{
|
|
|
33
39
|
* Worker choice strategy.
|
|
34
40
|
*/
|
|
35
41
|
export type WorkerChoiceStrategy = keyof typeof WorkerChoiceStrategies;
|
|
42
|
+
/**
|
|
43
|
+
* Enumeration of measurements.
|
|
44
|
+
*/
|
|
45
|
+
export declare const Measurements: Readonly<{
|
|
46
|
+
readonly runTime: "runTime";
|
|
47
|
+
readonly waitTime: "waitTime";
|
|
48
|
+
readonly elu: "elu";
|
|
49
|
+
}>;
|
|
50
|
+
/**
|
|
51
|
+
* Measurement.
|
|
52
|
+
*/
|
|
53
|
+
export type Measurement = keyof typeof Measurements;
|
|
54
|
+
/**
|
|
55
|
+
* Measurement options.
|
|
56
|
+
*/
|
|
57
|
+
export interface MeasurementOptions {
|
|
58
|
+
/**
|
|
59
|
+
* Set measurement median.
|
|
60
|
+
*/
|
|
61
|
+
median: boolean;
|
|
62
|
+
}
|
|
36
63
|
/**
|
|
37
64
|
* Worker choice strategy options.
|
|
38
65
|
*/
|
|
39
66
|
export interface WorkerChoiceStrategyOptions {
|
|
40
67
|
/**
|
|
41
|
-
*
|
|
68
|
+
* Measurement to use for worker choice strategy.
|
|
69
|
+
*/
|
|
70
|
+
measurement?: Measurement;
|
|
71
|
+
/**
|
|
72
|
+
* Runtime options.
|
|
42
73
|
*
|
|
43
|
-
* @defaultValue false
|
|
74
|
+
* @defaultValue \{ median: false \}
|
|
44
75
|
*/
|
|
45
|
-
|
|
76
|
+
runTime?: MeasurementOptions;
|
|
46
77
|
/**
|
|
47
|
-
*
|
|
78
|
+
* Wait time options.
|
|
48
79
|
*
|
|
49
|
-
* @defaultValue false
|
|
80
|
+
* @defaultValue \{ median: false \}
|
|
50
81
|
*/
|
|
51
|
-
|
|
82
|
+
waitTime?: MeasurementOptions;
|
|
83
|
+
/**
|
|
84
|
+
* Event loop utilization options.
|
|
85
|
+
*
|
|
86
|
+
* @defaultValue \{ median: false \}
|
|
87
|
+
*/
|
|
88
|
+
elu?: MeasurementOptions;
|
|
52
89
|
/**
|
|
53
90
|
* Worker weights to use for weighted round robin worker selection strategy.
|
|
54
91
|
* Weight is the tasks maximum average or median runtime in milliseconds.
|
|
@@ -58,48 +95,51 @@ export interface WorkerChoiceStrategyOptions {
|
|
|
58
95
|
weights?: Record<number, number>;
|
|
59
96
|
}
|
|
60
97
|
/**
|
|
61
|
-
*
|
|
98
|
+
* Measurement statistics requirements.
|
|
62
99
|
*
|
|
63
100
|
* @internal
|
|
64
101
|
*/
|
|
65
|
-
export interface
|
|
102
|
+
export interface MeasurementStatisticsRequirements {
|
|
66
103
|
/**
|
|
67
|
-
* Require
|
|
104
|
+
* Require measurement aggregate.
|
|
68
105
|
*/
|
|
69
|
-
|
|
106
|
+
aggregate: boolean;
|
|
70
107
|
/**
|
|
71
|
-
* Require
|
|
108
|
+
* Require measurement average.
|
|
72
109
|
*/
|
|
73
|
-
|
|
110
|
+
average: boolean;
|
|
74
111
|
/**
|
|
75
|
-
* Require
|
|
112
|
+
* Require measurement median.
|
|
76
113
|
*/
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
114
|
+
median: boolean;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Pool worker node worker usage statistics requirements.
|
|
118
|
+
*
|
|
119
|
+
* @internal
|
|
120
|
+
*/
|
|
121
|
+
export interface TaskStatisticsRequirements {
|
|
82
122
|
/**
|
|
83
|
-
*
|
|
123
|
+
* Tasks runtime requirements.
|
|
84
124
|
*/
|
|
85
|
-
|
|
125
|
+
runTime: MeasurementStatisticsRequirements;
|
|
86
126
|
/**
|
|
87
|
-
*
|
|
127
|
+
* Tasks wait time requirements.
|
|
88
128
|
*/
|
|
89
|
-
|
|
129
|
+
waitTime: MeasurementStatisticsRequirements;
|
|
90
130
|
/**
|
|
91
|
-
*
|
|
131
|
+
* Tasks event loop utilization requirements.
|
|
92
132
|
*/
|
|
93
|
-
elu:
|
|
133
|
+
elu: MeasurementStatisticsRequirements;
|
|
94
134
|
}
|
|
95
135
|
/**
|
|
96
136
|
* Worker choice strategy interface.
|
|
97
137
|
*/
|
|
98
138
|
export interface IWorkerChoiceStrategy {
|
|
99
139
|
/**
|
|
100
|
-
*
|
|
140
|
+
* Tasks statistics requirements.
|
|
101
141
|
*/
|
|
102
|
-
readonly
|
|
142
|
+
readonly taskStatisticsRequirements: TaskStatisticsRequirements;
|
|
103
143
|
/**
|
|
104
144
|
* Resets strategy internals.
|
|
105
145
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { IWorker } from '../worker';
|
|
2
2
|
import type { IPool } from '../pool';
|
|
3
3
|
import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy';
|
|
4
|
-
import type { IWorkerChoiceStrategy,
|
|
4
|
+
import type { IWorkerChoiceStrategy, TaskStatisticsRequirements, WorkerChoiceStrategyOptions } from './selection-strategies-types';
|
|
5
5
|
/**
|
|
6
6
|
* Selects the next worker with a weighted round robin scheduling algorithm.
|
|
7
7
|
* Loosely modeled after the weighted round robin queueing algorithm: https://en.wikipedia.org/wiki/Weighted_round_robin.
|
|
@@ -12,7 +12,7 @@ import type { IWorkerChoiceStrategy, TaskStatistics, WorkerChoiceStrategyOptions
|
|
|
12
12
|
*/
|
|
13
13
|
export declare class WeightedRoundRobinWorkerChoiceStrategy<Worker extends IWorker, Data = unknown, Response = unknown> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy {
|
|
14
14
|
/** @inheritDoc */
|
|
15
|
-
readonly
|
|
15
|
+
readonly taskStatisticsRequirements: TaskStatisticsRequirements;
|
|
16
16
|
/**
|
|
17
17
|
* Worker node id where the current task will be submitted.
|
|
18
18
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { IPool } from '../pool';
|
|
2
2
|
import type { IWorker } from '../worker';
|
|
3
|
-
import type {
|
|
3
|
+
import type { TaskStatisticsRequirements, WorkerChoiceStrategy, WorkerChoiceStrategyOptions } from './selection-strategies-types';
|
|
4
4
|
/**
|
|
5
5
|
* The worker choice strategy context.
|
|
6
6
|
*
|
|
@@ -20,11 +20,11 @@ export declare class WorkerChoiceStrategyContext<Worker extends IWorker, Data =
|
|
|
20
20
|
*/
|
|
21
21
|
constructor(pool: IPool<Worker, Data, Response>, workerChoiceStrategy?: WorkerChoiceStrategy, opts?: WorkerChoiceStrategyOptions);
|
|
22
22
|
/**
|
|
23
|
-
* Gets the worker choice strategy task statistics in the context.
|
|
23
|
+
* Gets the worker choice strategy task statistics requirements in the context.
|
|
24
24
|
*
|
|
25
|
-
* @returns The task statistics.
|
|
25
|
+
* @returns The task statistics requirements.
|
|
26
26
|
*/
|
|
27
|
-
|
|
27
|
+
getTaskStatisticsRequirements(): TaskStatisticsRequirements;
|
|
28
28
|
/**
|
|
29
29
|
* Sets the worker choice strategy to use in the context.
|
|
30
30
|
*
|
|
@@ -23,8 +23,6 @@ export type ThreadWorkerWithMessageChannel = Worker & Draft<MessageChannel>;
|
|
|
23
23
|
*
|
|
24
24
|
* It is possible to perform tasks in sync or asynchronous mode as you prefer.
|
|
25
25
|
*
|
|
26
|
-
* This pool selects the threads in a round robin fashion.
|
|
27
|
-
*
|
|
28
26
|
* @typeParam Data - Type of data sent to the worker. This can only be serializable data.
|
|
29
27
|
* @typeParam Response - Type of execution response. This can only be serializable data.
|
|
30
28
|
* @author [Alessandro Pio Ardizio](https://github.com/pioardi)
|
package/lib/pools/worker.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
import type { EventLoopUtilization } from 'node:perf_hooks';
|
|
3
1
|
import type { CircularArray } from '../circular-array';
|
|
4
2
|
import type { Queue } from '../queue';
|
|
5
3
|
/**
|
|
@@ -43,59 +41,83 @@ export interface Task<Data = unknown> {
|
|
|
43
41
|
readonly id?: string;
|
|
44
42
|
}
|
|
45
43
|
/**
|
|
46
|
-
*
|
|
44
|
+
* Measurement statistics.
|
|
47
45
|
*
|
|
48
46
|
* @internal
|
|
49
47
|
*/
|
|
50
|
-
export interface
|
|
48
|
+
export interface MeasurementStatistics {
|
|
51
49
|
/**
|
|
52
|
-
*
|
|
50
|
+
* Measurement aggregate.
|
|
53
51
|
*/
|
|
54
|
-
|
|
52
|
+
aggregate: number;
|
|
55
53
|
/**
|
|
56
|
-
*
|
|
54
|
+
* Measurement average.
|
|
57
55
|
*/
|
|
58
|
-
|
|
56
|
+
average: number;
|
|
59
57
|
/**
|
|
60
|
-
*
|
|
58
|
+
* Measurement median.
|
|
61
59
|
*/
|
|
62
|
-
|
|
60
|
+
median: number;
|
|
63
61
|
/**
|
|
64
|
-
*
|
|
62
|
+
* Measurement history.
|
|
65
63
|
*/
|
|
66
|
-
|
|
64
|
+
history: CircularArray<number>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Event loop utilization measurement statistics.
|
|
68
|
+
*
|
|
69
|
+
* @internal
|
|
70
|
+
*/
|
|
71
|
+
export interface EventLoopUtilizationMeasurementStatistics {
|
|
72
|
+
idle: MeasurementStatistics;
|
|
73
|
+
active: MeasurementStatistics;
|
|
74
|
+
utilization: number;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Task statistics.
|
|
78
|
+
*
|
|
79
|
+
* @internal
|
|
80
|
+
*/
|
|
81
|
+
export interface TaskStatistics {
|
|
67
82
|
/**
|
|
68
|
-
*
|
|
83
|
+
* Number of executed tasks.
|
|
69
84
|
*/
|
|
70
|
-
|
|
85
|
+
executed: number;
|
|
71
86
|
/**
|
|
72
|
-
*
|
|
87
|
+
* Number of executing tasks.
|
|
73
88
|
*/
|
|
74
|
-
|
|
89
|
+
executing: number;
|
|
75
90
|
/**
|
|
76
|
-
*
|
|
91
|
+
* Number of queued tasks.
|
|
77
92
|
*/
|
|
78
|
-
|
|
93
|
+
readonly queued: number;
|
|
79
94
|
/**
|
|
80
|
-
*
|
|
95
|
+
* Number of failed tasks.
|
|
81
96
|
*/
|
|
82
|
-
|
|
97
|
+
failed: number;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Worker usage statistics.
|
|
101
|
+
*
|
|
102
|
+
* @internal
|
|
103
|
+
*/
|
|
104
|
+
export interface WorkerUsage {
|
|
83
105
|
/**
|
|
84
|
-
*
|
|
106
|
+
* Tasks statistics.
|
|
85
107
|
*/
|
|
86
|
-
|
|
108
|
+
tasks: TaskStatistics;
|
|
87
109
|
/**
|
|
88
|
-
*
|
|
110
|
+
* Tasks runtime statistics.
|
|
89
111
|
*/
|
|
90
|
-
|
|
112
|
+
runTime: MeasurementStatistics;
|
|
91
113
|
/**
|
|
92
|
-
*
|
|
114
|
+
* Tasks wait time statistics.
|
|
93
115
|
*/
|
|
94
|
-
|
|
116
|
+
waitTime: MeasurementStatistics;
|
|
95
117
|
/**
|
|
96
|
-
*
|
|
118
|
+
* Tasks event loop utilization statistics.
|
|
97
119
|
*/
|
|
98
|
-
elu:
|
|
120
|
+
elu: EventLoopUtilizationMeasurementStatistics;
|
|
99
121
|
}
|
|
100
122
|
/**
|
|
101
123
|
* Worker interface.
|
|
@@ -129,9 +151,9 @@ export interface WorkerNode<Worker extends IWorker, Data = unknown> {
|
|
|
129
151
|
*/
|
|
130
152
|
readonly worker: Worker;
|
|
131
153
|
/**
|
|
132
|
-
* Worker node
|
|
154
|
+
* Worker node worker usage statistics.
|
|
133
155
|
*/
|
|
134
|
-
|
|
156
|
+
workerUsage: WorkerUsage;
|
|
135
157
|
/**
|
|
136
158
|
* Worker node tasks queue.
|
|
137
159
|
*/
|
package/lib/utility-types.d.ts
CHANGED
|
@@ -14,22 +14,54 @@ import type { IWorker, Task } from './pools/worker';
|
|
|
14
14
|
export type Draft<T> = {
|
|
15
15
|
-readonly [P in keyof T]?: T[P];
|
|
16
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* Task error.
|
|
19
|
+
*
|
|
20
|
+
* @typeParam Data - Type of data sent to the worker. This can only be serializable data.
|
|
21
|
+
*/
|
|
22
|
+
export interface TaskError<Data = unknown> {
|
|
23
|
+
/**
|
|
24
|
+
* Error message.
|
|
25
|
+
*/
|
|
26
|
+
message: string;
|
|
27
|
+
/**
|
|
28
|
+
* Data passed to the worker triggering the error.
|
|
29
|
+
*/
|
|
30
|
+
data?: Data;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Task performance.
|
|
34
|
+
*/
|
|
35
|
+
export interface TaskPerformance {
|
|
36
|
+
/**
|
|
37
|
+
* Task performance timestamp.
|
|
38
|
+
*/
|
|
39
|
+
timestamp: number;
|
|
40
|
+
/**
|
|
41
|
+
* Task runtime.
|
|
42
|
+
*/
|
|
43
|
+
runTime?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Task event loop utilization.
|
|
46
|
+
*/
|
|
47
|
+
elu?: EventLoopUtilization;
|
|
48
|
+
}
|
|
17
49
|
/**
|
|
18
50
|
* Performance statistics computation.
|
|
19
51
|
*/
|
|
20
52
|
export interface WorkerStatistics {
|
|
21
53
|
runTime: boolean;
|
|
22
|
-
waitTime: boolean;
|
|
23
54
|
elu: boolean;
|
|
24
55
|
}
|
|
25
56
|
/**
|
|
26
57
|
* Message object that is passed between main worker and worker.
|
|
27
58
|
*
|
|
59
|
+
* @typeParam MessageData - Type of data sent to and/or from the worker. This can only be serializable data.
|
|
28
60
|
* @typeParam Data - Type of data sent to the worker. This can only be serializable data.
|
|
29
61
|
* @typeParam MainWorker - Type of main worker.
|
|
30
62
|
* @internal
|
|
31
63
|
*/
|
|
32
|
-
export interface MessageValue<Data = unknown, MainWorker extends ClusterWorker | MessagePort = ClusterWorker | MessagePort> extends Task<
|
|
64
|
+
export interface MessageValue<MessageData = unknown, Data = unknown, MainWorker extends ClusterWorker | MessagePort = ClusterWorker | MessagePort> extends Task<MessageData> {
|
|
33
65
|
/**
|
|
34
66
|
* Kill code.
|
|
35
67
|
*/
|
|
@@ -37,23 +69,11 @@ export interface MessageValue<Data = unknown, MainWorker extends ClusterWorker |
|
|
|
37
69
|
/**
|
|
38
70
|
* Task error.
|
|
39
71
|
*/
|
|
40
|
-
readonly
|
|
41
|
-
/**
|
|
42
|
-
* Task data triggering task error.
|
|
43
|
-
*/
|
|
44
|
-
readonly errorData?: unknown;
|
|
45
|
-
/**
|
|
46
|
-
* Runtime.
|
|
47
|
-
*/
|
|
48
|
-
readonly runTime?: number;
|
|
49
|
-
/**
|
|
50
|
-
* Wait time.
|
|
51
|
-
*/
|
|
52
|
-
readonly waitTime?: number;
|
|
72
|
+
readonly taskError?: TaskError<Data>;
|
|
53
73
|
/**
|
|
54
|
-
*
|
|
74
|
+
* Task performance.
|
|
55
75
|
*/
|
|
56
|
-
readonly
|
|
76
|
+
readonly taskPerformance?: TaskPerformance;
|
|
57
77
|
/**
|
|
58
78
|
* Reference to main worker.
|
|
59
79
|
*/
|
|
@@ -2,23 +2,12 @@
|
|
|
2
2
|
/// <reference types="node" />
|
|
3
3
|
/// <reference types="node" />
|
|
4
4
|
/// <reference types="node" />
|
|
5
|
-
/// <reference types="node" />
|
|
6
5
|
import { AsyncResource } from 'node:async_hooks';
|
|
7
6
|
import type { Worker } from 'node:cluster';
|
|
8
7
|
import type { MessagePort } from 'node:worker_threads';
|
|
9
|
-
import { type EventLoopUtilization } from 'node:perf_hooks';
|
|
10
8
|
import type { MessageValue, WorkerStatistics } from '../utility-types';
|
|
11
9
|
import { type WorkerOptions } from './worker-options';
|
|
12
10
|
import type { TaskFunctions, WorkerAsyncFunction, WorkerFunction, WorkerSyncFunction } from './worker-functions';
|
|
13
|
-
/**
|
|
14
|
-
* Task performance.
|
|
15
|
-
*/
|
|
16
|
-
export interface TaskPerformance {
|
|
17
|
-
timestamp: number;
|
|
18
|
-
waitTime?: number;
|
|
19
|
-
runTime?: number;
|
|
20
|
-
elu?: EventLoopUtilization;
|
|
21
|
-
}
|
|
22
11
|
/**
|
|
23
12
|
* Base class that implements some shared logic for all poolifier workers.
|
|
24
13
|
*
|
|
@@ -68,7 +57,7 @@ export declare abstract class AbstractWorker<MainWorker extends Worker | Message
|
|
|
68
57
|
*
|
|
69
58
|
* @param message - Message received.
|
|
70
59
|
*/
|
|
71
|
-
protected messageListener(message: MessageValue<Data, MainWorker>): void;
|
|
60
|
+
protected messageListener(message: MessageValue<Data, Data, MainWorker>): void;
|
|
72
61
|
/**
|
|
73
62
|
* Returns the main worker.
|
|
74
63
|
*
|
|
@@ -80,7 +69,7 @@ export declare abstract class AbstractWorker<MainWorker extends Worker | Message
|
|
|
80
69
|
*
|
|
81
70
|
* @param message - The response message.
|
|
82
71
|
*/
|
|
83
|
-
protected abstract sendToMainWorker(message: MessageValue<Response>): void;
|
|
72
|
+
protected abstract sendToMainWorker(message: MessageValue<Response, Data>): void;
|
|
84
73
|
/**
|
|
85
74
|
* Checks if the worker should be terminated, because its living too long.
|
|
86
75
|
*/
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export declare const KillBehaviors: Readonly<{
|
|
5
5
|
/**
|
|
6
|
-
* If `currentTime - lastActiveTime` is greater than `maxInactiveTime` but a task is still
|
|
6
|
+
* If `currentTime - lastActiveTime` is greater than `maxInactiveTime` but a task is still executing, then the worker **wont** be deleted.
|
|
7
7
|
*/
|
|
8
8
|
readonly SOFT: "SOFT";
|
|
9
9
|
/**
|
|
10
|
-
* If `currentTime - lastActiveTime` is greater than `maxInactiveTime` but a task is still
|
|
10
|
+
* If `currentTime - lastActiveTime` is greater than `maxInactiveTime` but a task is still executing, then the worker will be deleted.
|
|
11
11
|
*/
|
|
12
12
|
readonly HARD: "HARD";
|
|
13
13
|
}>;
|
|
@@ -51,8 +51,8 @@ export interface WorkerOptions {
|
|
|
51
51
|
/**
|
|
52
52
|
* `killBehavior` dictates if your async unit (worker/process) will be deleted in case that a task is active on it.
|
|
53
53
|
*
|
|
54
|
-
* - SOFT: If `currentTime - lastActiveTime` is greater than `maxInactiveTime` but a task is still
|
|
55
|
-
* - HARD: If `currentTime - lastActiveTime` is greater than `maxInactiveTime` but a task is still
|
|
54
|
+
* - SOFT: If `currentTime - lastActiveTime` is greater than `maxInactiveTime` but a task is still executing, then the worker **won't** be deleted.
|
|
55
|
+
* - HARD: If `currentTime - lastActiveTime` is greater than `maxInactiveTime` but a task is still executing, then the worker will be deleted.
|
|
56
56
|
*
|
|
57
57
|
* This option only apply to the newly created workers.
|
|
58
58
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "poolifier",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.1",
|
|
4
4
|
"description": "A fast, easy to use Node.js Worker Thread Pool and Cluster Pool implementation",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"pnpm": ">=8.6.0"
|
|
26
26
|
},
|
|
27
27
|
"volta": {
|
|
28
|
-
"node": "20.
|
|
28
|
+
"node": "20.3.0",
|
|
29
29
|
"pnpm": "8.6.1"
|
|
30
30
|
},
|
|
31
31
|
"repository": {
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"@release-it/keep-a-changelog": "^3.1.0",
|
|
85
85
|
"@rollup/plugin-terser": "^0.4.3",
|
|
86
86
|
"@rollup/plugin-typescript": "^11.1.1",
|
|
87
|
-
"@types/node": "^20.2.
|
|
87
|
+
"@types/node": "^20.2.6",
|
|
88
88
|
"@typescript-eslint/eslint-plugin": "^5.59.9",
|
|
89
89
|
"@typescript-eslint/parser": "^5.59.9",
|
|
90
90
|
"benny": "^3.7.1",
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
"mochawesome": "^7.1.3",
|
|
109
109
|
"prettier": "^2.8.8",
|
|
110
110
|
"release-it": "^15.11.0",
|
|
111
|
-
"rollup": "^3.24.
|
|
111
|
+
"rollup": "^3.24.1",
|
|
112
112
|
"rollup-plugin-analyzer": "^4.0.0",
|
|
113
113
|
"rollup-plugin-command": "^1.1.3",
|
|
114
114
|
"rollup-plugin-delete": "^2.0.0",
|