poolifier 2.5.3 → 2.5.4
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 +35 -20
- package/lib/index.d.ts +7 -8
- package/lib/index.js +1 -1
- package/lib/index.mjs +1 -1
- package/lib/pools/abstract-pool.d.ts +2 -0
- package/lib/pools/selection-strategies/abstract-worker-choice-strategy.d.ts +3 -3
- 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/selection-strategies-types.d.ts +7 -3
- 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/dynamic.d.ts +3 -3
- package/lib/pools/thread/fixed.d.ts +14 -2
- package/lib/pools/worker.d.ts +9 -3
- package/lib/utility-types.d.ts +18 -33
- package/lib/worker/abstract-worker.d.ts +19 -1
- package/lib/worker/cluster-worker.d.ts +2 -1
- package/lib/worker/thread-worker.d.ts +2 -1
- package/lib/worker/worker-functions.d.ts +33 -0
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -150,11 +150,9 @@ Node versions >= 16.14.x are supported.
|
|
|
150
150
|
|
|
151
151
|
## [API](https://poolifier.github.io/poolifier/)
|
|
152
152
|
|
|
153
|
-
###
|
|
153
|
+
### Pool options
|
|
154
154
|
|
|
155
|
-
|
|
156
|
-
`filePath` (mandatory) Path to a file with a worker implementation
|
|
157
|
-
`opts` (optional) An object with these properties:
|
|
155
|
+
An object with these properties:
|
|
158
156
|
|
|
159
157
|
- `messageHandler` (optional) - A function that will listen for message event on each worker
|
|
160
158
|
- `errorHandler` (optional) - A function that will listen for error event on each worker
|
|
@@ -162,14 +160,14 @@ Node versions >= 16.14.x are supported.
|
|
|
162
160
|
- `exitHandler` (optional) - A function that will listen for exit event on each worker
|
|
163
161
|
- `workerChoiceStrategy` (optional) - The worker choice strategy to use in this pool:
|
|
164
162
|
|
|
165
|
-
- `WorkerChoiceStrategies.ROUND_ROBIN`: Submit tasks to worker in a round
|
|
166
|
-
- `WorkerChoiceStrategies.LEAST_USED`: Submit tasks to the
|
|
167
|
-
- `WorkerChoiceStrategies.LEAST_BUSY`: Submit tasks to the
|
|
168
|
-
- `WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN`: Submit tasks to worker using a weighted round robin scheduling algorithm based on tasks execution time
|
|
169
|
-
- `WorkerChoiceStrategies.INTERLEAVED_WEIGHTED_ROUND_ROBIN`: Submit tasks to worker using an interleaved weighted round robin scheduling algorithm based on tasks execution time (experimental)
|
|
170
|
-
- `WorkerChoiceStrategies.FAIR_SHARE`: Submit tasks to worker using a fair share tasks scheduling algorithm based on tasks execution time
|
|
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 running and ran tasks
|
|
165
|
+
- `WorkerChoiceStrategies.LEAST_BUSY`: Submit tasks to the worker with the minimum tasks total execution time
|
|
166
|
+
- `WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN`: Submit tasks to worker by using a weighted round robin scheduling algorithm based on tasks execution time
|
|
167
|
+
- `WorkerChoiceStrategies.INTERLEAVED_WEIGHTED_ROUND_ROBIN`: Submit tasks to worker by using an interleaved weighted round robin scheduling algorithm based on tasks execution time (experimental)
|
|
168
|
+
- `WorkerChoiceStrategies.FAIR_SHARE`: Submit tasks to worker by using a fair share tasks scheduling algorithm based on tasks execution time
|
|
171
169
|
|
|
172
|
-
`WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN` and `WorkerChoiceStrategies.FAIR_SHARE` strategies are targeted to heavy and long tasks.
|
|
170
|
+
`WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN`, `WorkerChoiceStrategies.INTERLEAVED_WEIGHTED_ROUND_ROBIN` and `WorkerChoiceStrategies.FAIR_SHARE` strategies are targeted to heavy and long tasks.
|
|
173
171
|
Default: `WorkerChoiceStrategies.ROUND_ROBIN`
|
|
174
172
|
|
|
175
173
|
- `workerChoiceStrategyOptions` (optional) - The worker choice strategy options object to use in this pool.
|
|
@@ -181,11 +179,11 @@ Node versions >= 16.14.x are supported.
|
|
|
181
179
|
Default: `{ medRunTime: false }`
|
|
182
180
|
|
|
183
181
|
- `restartWorkerOnError` (optional) - Restart worker on uncaught error in this pool.
|
|
184
|
-
Default: true
|
|
182
|
+
Default: `true`
|
|
185
183
|
- `enableEvents` (optional) - Events emission enablement in this pool.
|
|
186
|
-
Default: true
|
|
184
|
+
Default: `true`
|
|
187
185
|
- `enableTasksQueue` (optional) - Tasks queue per worker enablement in this pool.
|
|
188
|
-
Default: false
|
|
186
|
+
Default: `false`
|
|
189
187
|
|
|
190
188
|
- `tasksQueueOptions` (optional) - The worker tasks queue options object to use in this pool.
|
|
191
189
|
Properties:
|
|
@@ -194,16 +192,33 @@ Node versions >= 16.14.x are supported.
|
|
|
194
192
|
|
|
195
193
|
Default: `{ concurrency: 1 }`
|
|
196
194
|
|
|
195
|
+
#### Thread pool specific options
|
|
196
|
+
|
|
197
|
+
- `workerOptions` (optional) - An object with the worker options. See [worker_threads](https://nodejs.org/api/worker_threads.html#worker_threads_new_worker_filename_options) for more details.
|
|
198
|
+
|
|
199
|
+
#### Cluster pool specific options
|
|
200
|
+
|
|
201
|
+
- `env` (optional) - An object with the environment variables to pass to the worker. See [cluster](https://nodejs.org/api/cluster.html#cluster_cluster_fork_env) for more details.
|
|
202
|
+
|
|
203
|
+
- `settings` (optional) - An object with the cluster settings. See [cluster](https://nodejs.org/api/cluster.html#cluster_cluster_settings) for more details.
|
|
204
|
+
|
|
205
|
+
### `pool = new FixedThreadPool/FixedClusterPool(numberOfThreads/numberOfWorkers, filePath, opts)`
|
|
206
|
+
|
|
207
|
+
`numberOfThreads/numberOfWorkers` (mandatory) Number of workers for this pool
|
|
208
|
+
`filePath` (mandatory) Path to a file with a worker implementation
|
|
209
|
+
`opts` (optional) An object with the pool options properties described above
|
|
210
|
+
|
|
197
211
|
### `pool = new DynamicThreadPool/DynamicClusterPool(min, max, filePath, opts)`
|
|
198
212
|
|
|
199
213
|
`min` (mandatory) Same as FixedThreadPool/FixedClusterPool numberOfThreads/numberOfWorkers, this number of workers will be always active
|
|
200
214
|
`max` (mandatory) Max number of workers that this pool can contain, the new created workers will die after a threshold (default is 1 minute, you can override it in your worker implementation).
|
|
201
|
-
`filePath` (mandatory)
|
|
202
|
-
`opts` (optional)
|
|
215
|
+
`filePath` (mandatory) Path to a file with a worker implementation
|
|
216
|
+
`opts` (optional) An object with the pool options properties described above
|
|
203
217
|
|
|
204
|
-
### `pool.execute(data)`
|
|
218
|
+
### `pool.execute(data, name)`
|
|
205
219
|
|
|
206
220
|
`data` (optional) An object that you want to pass to your worker implementation
|
|
221
|
+
`name` (optional) A string with the task function name that you want to execute on the worker. Default: `'default'`
|
|
207
222
|
This method is available on both pool implementations and returns a promise.
|
|
208
223
|
|
|
209
224
|
### `pool.destroy()`
|
|
@@ -213,14 +228,14 @@ This method will call the terminate method on each worker.
|
|
|
213
228
|
|
|
214
229
|
### `class YourWorker extends ThreadWorker/ClusterWorker`
|
|
215
230
|
|
|
216
|
-
`taskFunctions` (mandatory) The task function
|
|
231
|
+
`taskFunctions` (mandatory) The task function or task functions object that you want to execute on the worker
|
|
217
232
|
`opts` (optional) An object with these properties:
|
|
218
233
|
|
|
219
234
|
- `maxInactiveTime` (optional) - Max time to wait tasks to work on in milliseconds, after this period the new worker will die.
|
|
220
235
|
The last active time of your worker unit will be updated when a task is submitted to a worker or when a worker terminate a task.
|
|
221
236
|
If `killBehavior` is set to `KillBehaviors.HARD` this value represents also the timeout for the tasks that you submit to the pool, when this timeout expires your tasks is interrupted and the worker is killed if is not part of the minimum size of the pool.
|
|
222
237
|
If `killBehavior` is set to `KillBehaviors.SOFT` your tasks have no timeout and your workers will not be terminated until your task is completed.
|
|
223
|
-
Default: 60000
|
|
238
|
+
Default: `60000`
|
|
224
239
|
|
|
225
240
|
- `killBehavior` (optional) - Dictates if your async unit (worker/process) will be deleted in case that a task is active on it.
|
|
226
241
|
**KillBehaviors.SOFT**: If `currentTime - lastActiveTime` is greater than `maxInactiveTime` but a task is still running, then the worker **won't** be deleted.
|
|
@@ -268,7 +283,7 @@ But in general, **always profile your application**.
|
|
|
268
283
|
|
|
269
284
|
## Contribute
|
|
270
285
|
|
|
271
|
-
Choose your task here [2.
|
|
286
|
+
Choose your task here [2.5.x](https://github.com/orgs/poolifier/projects/1), propose an idea, a fix, an improvement.
|
|
272
287
|
|
|
273
288
|
See [CONTRIBUTING](CONTRIBUTING.md) guidelines.
|
|
274
289
|
|
package/lib/index.d.ts
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
|
-
export { DynamicClusterPool } from './pools/cluster/dynamic';
|
|
2
|
-
export { FixedClusterPool } from './pools/cluster/fixed';
|
|
3
|
-
export type { ClusterPoolOptions } from './pools/cluster/fixed';
|
|
4
1
|
export type { AbstractPool } from './pools/abstract-pool';
|
|
2
|
+
export { DynamicClusterPool } from './pools/cluster/dynamic';
|
|
3
|
+
export { FixedClusterPool, type ClusterPoolOptions } from './pools/cluster/fixed';
|
|
5
4
|
export { PoolEvents, PoolTypes, WorkerTypes } from './pools/pool';
|
|
6
5
|
export type { IPool, PoolEmitter, PoolEvent, PoolInfo, PoolOptions, PoolType, TasksQueueOptions, WorkerType } from './pools/pool';
|
|
7
6
|
export type { ErrorHandler, ExitHandler, IWorker, MessageHandler, OnlineHandler, Task, TasksUsage, WorkerNode } from './pools/worker';
|
|
8
7
|
export { WorkerChoiceStrategies } from './pools/selection-strategies/selection-strategies-types';
|
|
9
|
-
export type { IWorkerChoiceStrategy,
|
|
8
|
+
export type { IWorkerChoiceStrategy, TaskStatistics, WorkerChoiceStrategy, WorkerChoiceStrategyOptions } from './pools/selection-strategies/selection-strategies-types';
|
|
10
9
|
export type { WorkerChoiceStrategyContext } from './pools/selection-strategies/worker-choice-strategy-context';
|
|
11
10
|
export { DynamicThreadPool } from './pools/thread/dynamic';
|
|
12
|
-
export { FixedThreadPool } from './pools/thread/fixed';
|
|
13
|
-
export type {
|
|
14
|
-
export type { AbstractWorker } from './worker/abstract-worker';
|
|
11
|
+
export { FixedThreadPool, type ThreadPoolOptions, type ThreadWorkerWithMessageChannel } from './pools/thread/fixed';
|
|
12
|
+
export type { AbstractWorker, TaskPerformance } from './worker/abstract-worker';
|
|
15
13
|
export { ClusterWorker } from './worker/cluster-worker';
|
|
16
14
|
export { ThreadWorker } from './worker/thread-worker';
|
|
17
15
|
export { KillBehaviors } from './worker/worker-options';
|
|
18
16
|
export type { KillBehavior, WorkerOptions } from './worker/worker-options';
|
|
19
|
-
export type {
|
|
17
|
+
export type { TaskFunctions, WorkerAsyncFunction, WorkerFunction, WorkerSyncFunction } from './worker/worker-functions';
|
|
18
|
+
export type { Draft, MessageValue, PromiseResponseWrapper, WorkerStatistics } from './utility-types';
|
|
20
19
|
export type { CircularArray } from './circular-array';
|
|
21
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"),r=require("node:crypto"),s=require("node:os"),i=require("node:worker_threads"),o=require("node:async_hooks");const n=Object.freeze({fixed:"fixed",dynamic:"dynamic"}),a=Object.freeze({cluster:"cluster",thread:"thread"});class h extends e{}const u=Object.freeze({full:"full",busy:"busy",error:"error",taskError:"taskError"}),k=Object.freeze((()=>{})),c={medRunTime:!1,medWaitTime:!1},d=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},l=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 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 T=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;requiredStatistics={runTime:!1,avgRunTime:!1,medRunTime:!1,waitTime:!1,avgWaitTime:!1,medWaitTime:!1};constructor(e,t=c){this.pool=e,this.opts=t,this.choose=this.choose.bind(this)}setRequiredStatistics(e){this.requiredStatistics.avgRunTime&&!0===e.medRunTime&&(this.requiredStatistics.avgRunTime=!1,this.requiredStatistics.medRunTime=e.medRunTime),this.requiredStatistics.medRunTime&&!1===e.medRunTime&&(this.requiredStatistics.avgRunTime=!0,this.requiredStatistics.medRunTime=e.medRunTime),this.requiredStatistics.avgWaitTime&&!0===e.medWaitTime&&(this.requiredStatistics.avgWaitTime=!1,this.requiredStatistics.medWaitTime=e.medWaitTime),this.requiredStatistics.medWaitTime&&!1===e.medWaitTime&&(this.requiredStatistics.avgWaitTime=!0,this.requiredStatistics.medWaitTime=e.medWaitTime)}setOptions(e){e=e??c,this.setRequiredStatistics(e),this.opts=e}findFreeWorkerNodeKey(){return this.toggleFindLastFreeWorkerNodeKey?(this.toggleFindLastFreeWorkerNodeKey=!1,this.findLastFreeWorkerNodeKey()):(this.toggleFindLastFreeWorkerNodeKey=!0,this.findFirstFreeWorkerNodeKey())}getWorkerTaskRunTime(e){return this.requiredStatistics.medRunTime?this.pool.workerNodes[e].tasksUsage.medRunTime:this.pool.workerNodes[e].tasksUsage.avgRunTime}getWorkerWaitTime(e){return this.requiredStatistics.medWaitTime?this.pool.workerNodes[e].tasksUsage.medWaitTime:this.pool.workerNodes[e].tasksUsage.avgWaitTime}computeDefaultWorkerWeight(){let e=0;for(const t of s.cpus()){const r=t.speed.toString().length-1;e+=1/(t.speed/Math.pow(10,r))*Math.pow(10,r)}return Math.round(e/s.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 W extends w{requiredStatistics={runTime:!0,avgRunTime:!0,medRunTime:!1,waitTime:!1,avgWaitTime:!1,medWaitTime:!1};workersVirtualTaskEndTimestamp=[];constructor(e,t=c){super(e,t),this.setRequiredStatistics(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.getWorkerTaskRunTime(e)}getWorkerVirtualTaskStartTimestamp(e){return Math.max(performance.now(),this.workersVirtualTaskEndTimestamp[e]??-1/0)}}class f extends w{currentWorkerNodeId=0;currentRoundId=0;roundWeights;defaultWorkerWeight;constructor(e,t=c){super(e,t),this.setRequiredStatistics(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 y extends w{requiredStatistics={runTime:!0,avgRunTime:!1,medRunTime:!1,waitTime:!1,avgWaitTime:!1,medWaitTime:!1};constructor(e,t=c){super(e,t),this.setRequiredStatistics(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.tasksUsage.runTime;if(0===i)return e;i<r&&(r=i,t=e)}return t}remove(){return!0}}class S extends w{constructor(e,t=c){super(e,t),this.setRequiredStatistics(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.tasksUsage,o=i.run+i.running;if(0===o)return e;o<r&&(r=o,t=e)}return t}remove(){return!0}}class N extends w{nextWorkerNodeId=0;constructor(e,t=c){super(e,t),this.setRequiredStatistics(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 R extends w{requiredStatistics={runTime:!0,avgRunTime:!0,medRunTime:!1,waitTime:!1,avgWaitTime:!1,medWaitTime:!1};currentWorkerNodeId=0;defaultWorkerWeight;workerVirtualTaskRunTime=0;constructor(e,t=c){super(e,t),this.setRequiredStatistics(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 x{workerChoiceStrategy;workerChoiceStrategies;constructor(e,t=T.ROUND_ROBIN,r=c){this.workerChoiceStrategy=t,this.execute=this.execute.bind(this),this.workerChoiceStrategies=new Map([[T.ROUND_ROBIN,new(N.bind(this))(e,r)],[T.LEAST_USED,new(S.bind(this))(e,r)],[T.LEAST_BUSY,new(y.bind(this))(e,r)],[T.FAIR_SHARE,new(W.bind(this))(e,r)],[T.WEIGHTED_ROUND_ROBIN,new(R.bind(this))(e,r)],[T.INTERLEAVED_WEIGHTED_ROUND_ROBIN,new(f.bind(this))(e,r)]])}getRequiredStatistics(){return this.workerChoiceStrategies.get(this.workerChoiceStrategy).requiredStatistics}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 E{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),this.setupHook();for(let e=1;e<=this.numberOfWorkers;e++)this.createAndSetupWorker();!0===this.opts.enableEvents&&(this.emitter=new h),this.workerChoiceStrategyContext=new x(this,this.opts.workerChoiceStrategy,this.opts.workerChoiceStrategyOptions)}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(!l(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??c,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(!l(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&&!l(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;for(const e of this.workerNodes)this.setWorkerNodeTasksUsage(e,{run:0,running:0,runTime:0,runTimeHistory:new p,avgRunTime:0,medRunTime:0,waitTime:0,waitTimeHistory:new p,avgWaitTime:0,medWaitTime:0,error:0});this.workerChoiceStrategyContext.setWorkerChoiceStrategy(this.opts.workerChoiceStrategy),null!=t&&this.setWorkerChoiceStrategyOptions(t)}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)):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 s=performance.now(),i=this.chooseWorkerNode(),o={name:t,data:e??{},submissionTimestamp:s,id:r.randomUUID()},n=new Promise(((e,t)=>{this.promiseResponseMap.set(o.id,{resolve:e,reject:t,worker:this.workerNodes[i].worker})}));return!0===this.opts.enableTasksQueue&&(this.busy||this.workerNodes[i].tasksUsage.running>=this.opts.tasksQueueOptions.concurrency)?this.enqueueTask(i,o):this.executeTask(i,o),this.workerChoiceStrategyContext.update(i),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){++this.workerNodes[e].tasksUsage.running}afterTaskExecutionHook(e,t){const r=this.workerNodes[this.getWorkerNodeKey(e)].tasksUsage;--r.running,++r.run,null!=t.error&&++r.error,this.updateRunTimeTasksUsage(r,t),this.updateWaitTimeTasksUsage(r,t)}updateRunTimeTasksUsage(e,t){this.workerChoiceStrategyContext.getRequiredStatistics().runTime&&(e.runTime+=t.runTime??0,this.workerChoiceStrategyContext.getRequiredStatistics().avgRunTime&&0!==e.run&&(e.avgRunTime=e.runTime/e.run),this.workerChoiceStrategyContext.getRequiredStatistics().medRunTime&&null!=t.runTime&&(e.runTimeHistory.push(t.runTime),e.medRunTime=d(e.runTimeHistory)))}updateWaitTimeTasksUsage(e,t){this.workerChoiceStrategyContext.getRequiredStatistics().waitTime&&(e.waitTime+=t.waitTime??0,this.workerChoiceStrategyContext.getRequiredStatistics().avgWaitTime&&0!==e.run&&(e.avgWaitTime=e.waitTime/e.run),this.workerChoiceStrategyContext.getRequiredStatistics().medWaitTime&&null!=t.waitTime&&(e.waitTimeHistory.push(t.waitTime),e.medWaitTime=d(e.waitTimeHistory)))}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].tasksUsage.running)&&(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??k),e.on("error",this.opts.errorHandler??k),e.on("error",(e=>{null!=this.emitter&&this.emitter.emit(u.error,e)})),!0===this.opts.restartWorkerOnError&&e.on("error",(()=>{this.createAndSetupWorker()})),e.on("online",this.opts.onlineHandler??k),e.on("exit",this.opts.exitHandler??k),e.once("exit",(()=>{this.removeWorkerNode(e)})),this.pushWorkerNode(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(u.taskError,{error:e.error,errorData:e.errorData})):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(u.busy,this.info),this.type===n.dynamic&&this.full&&this.emitter?.emit(u.full,this.info))}setWorkerNodeTasksUsage(e,t){e.tasksUsage=t}pushWorkerNode(e){return this.workerNodes.push({worker:e,tasksUsage:{run:0,running:0,runTime:0,runTimeHistory:new p,avgRunTime:0,medRunTime:0,waitTime:0,waitTimeHistory:new p,avgWaitTime:0,medWaitTime:0,error:0},tasksQueue:new g})}setWorkerNode(e,t,r,s){this.workerNodes[e]={worker:t,tasksUsage:r,tasksQueue: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),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)}}class I extends E{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 a.cluster}get minSize(){return this.numberOfWorkers}get maxSize(){return this.numberOfWorkers}get busy(){return this.internalBusy()}}class b extends E{constructor(e,t,r={}){super(e,t,r)}isMain(){return i.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 i.Worker(this.filePath,{env:i.SHARE_ENV})}afterWorkerSetup(e){const{port1:t,port2:r}=new i.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 a.thread}get minSize(){return this.numberOfWorkers}get maxSize(){return this.numberOfWorkers}get busy(){return this.internalBusy()}}const v="default",O=6e4,C=m.SOFT;class q extends o.AsyncResource{isMain;mainWorker;opts;taskFunctions;lastTaskTimestamp;aliveInterval;constructor(e,t,r,s,i={killBehavior:C,maxInactiveTime:O}){super(e),this.isMain=t,this.mainWorker=s,this.opts=i,this.checkWorkerOptions(this.opts),this.checkTaskFunctions(r),this.isMain||(this.lastTaskTimestamp=performance.now(),this.aliveInterval=setInterval(this.checkAlive.bind(this),(this.opts.maxInactiveTime??O)/2),this.checkAlive.bind(this)()),this.mainWorker?.on("message",this.messageListener.bind(this))}checkWorkerOptions(e){this.opts.killBehavior=e.killBehavior??C,this.opts.maxInactiveTime=e.maxInactiveTime??O,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(v,e.bind(this));else{if(!l(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(v,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())}getMainWorker(){if(null==this.mainWorker)throw new Error("Main worker was not set");return this.mainWorker}checkAlive(){performance.now()-this.lastTaskTimestamp>(this.opts.maxInactiveTime??O)&&this.sendToMainWorker({kill:this.opts.killBehavior})}handleError(e){return e}runSync(e,t){try{const r=performance.now(),s=r-(t.submissionTimestamp??0),i=e(t.data),o=performance.now()-r;this.sendToMainWorker({data:i,runTime:o,waitTime:s,id:t.id})}catch(e){const r=this.handleError(e);this.sendToMainWorker({error:r,errorData:t.data,id:t.id})}finally{!this.isMain&&(this.lastTaskTimestamp=performance.now())}}runAsync(e,t){const r=performance.now(),s=r-(t.submissionTimestamp??0);e(t.data).then((e=>{const i=performance.now()-r;return this.sendToMainWorker({data:e,runTime:i,waitTime:s,id:t.id}),null})).catch((e=>{const r=this.handleError(e);this.sendToMainWorker({error:r,errorData:t.data,id:t.id})})).finally((()=>{!this.isMain&&(this.lastTaskTimestamp=performance.now())})).catch(k)}getTaskFunction(e){e=e??v;const t=this.taskFunctions.get(e);if(null==t)throw new Error(`Task function '${e}' not found`);return t}}exports.ClusterWorker=class extends q{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 I{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 b{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=I,exports.FixedThreadPool=b,exports.KillBehaviors=m,exports.PoolEvents=u,exports.PoolTypes=n,exports.ThreadWorker=class extends q{constructor(e,t={}){super("worker-thread-pool:poolifier",i.isMainThread,e,i.parentPort,t)}sendToMainWorker(e){this.getMainWorker().postMessage(e)}},exports.WorkerChoiceStrategies=T,exports.WorkerTypes=a;
|
|
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;
|
package/lib/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"node:events";import t from"node:cluster";import r from"node:crypto";import{cpus as s}from"node:os";import{isMainThread as i,Worker as o,SHARE_ENV as n,MessageChannel as a,parentPort as h}from"node:worker_threads";import{AsyncResource as u}from"node:async_hooks";const k=Object.freeze({fixed:"fixed",dynamic:"dynamic"}),c=Object.freeze({cluster:"cluster",thread:"thread"});class d extends e{}const l=Object.freeze({full:"full",busy:"busy",error:"error",taskError:"taskError"}),m=Object.freeze((()=>{})),p={medRunTime:!1,medWaitTime:!1},g=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),w=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 f{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",FAIR_SHARE:"FAIR_SHARE",WEIGHTED_ROUND_ROBIN:"WEIGHTED_ROUND_ROBIN",INTERLEAVED_WEIGHTED_ROUND_ROBIN:"INTERLEAVED_WEIGHTED_ROUND_ROBIN"});class S{pool;opts;toggleFindLastFreeWorkerNodeKey=!1;requiredStatistics={runTime:!1,avgRunTime:!1,medRunTime:!1,waitTime:!1,avgWaitTime:!1,medWaitTime:!1};constructor(e,t=p){this.pool=e,this.opts=t,this.choose=this.choose.bind(this)}setRequiredStatistics(e){this.requiredStatistics.avgRunTime&&!0===e.medRunTime&&(this.requiredStatistics.avgRunTime=!1,this.requiredStatistics.medRunTime=e.medRunTime),this.requiredStatistics.medRunTime&&!1===e.medRunTime&&(this.requiredStatistics.avgRunTime=!0,this.requiredStatistics.medRunTime=e.medRunTime),this.requiredStatistics.avgWaitTime&&!0===e.medWaitTime&&(this.requiredStatistics.avgWaitTime=!1,this.requiredStatistics.medWaitTime=e.medWaitTime),this.requiredStatistics.medWaitTime&&!1===e.medWaitTime&&(this.requiredStatistics.avgWaitTime=!0,this.requiredStatistics.medWaitTime=e.medWaitTime)}setOptions(e){e=e??p,this.setRequiredStatistics(e),this.opts=e}findFreeWorkerNodeKey(){return this.toggleFindLastFreeWorkerNodeKey?(this.toggleFindLastFreeWorkerNodeKey=!1,this.findLastFreeWorkerNodeKey()):(this.toggleFindLastFreeWorkerNodeKey=!0,this.findFirstFreeWorkerNodeKey())}getWorkerTaskRunTime(e){return this.requiredStatistics.medRunTime?this.pool.workerNodes[e].tasksUsage.medRunTime:this.pool.workerNodes[e].tasksUsage.avgRunTime}getWorkerWaitTime(e){return this.requiredStatistics.medWaitTime?this.pool.workerNodes[e].tasksUsage.medWaitTime:this.pool.workerNodes[e].tasksUsage.avgWaitTime}computeDefaultWorkerWeight(){let e=0;for(const t of s()){const r=t.speed.toString().length-1;e+=1/(t.speed/Math.pow(10,r))*Math.pow(10,r)}return Math.round(e/s().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 N extends S{requiredStatistics={runTime:!0,avgRunTime:!0,medRunTime:!1,waitTime:!1,avgWaitTime:!1,medWaitTime:!1};workersVirtualTaskEndTimestamp=[];constructor(e,t=p){super(e,t),this.setRequiredStatistics(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.getWorkerTaskRunTime(e)}getWorkerVirtualTaskStartTimestamp(e){return Math.max(performance.now(),this.workersVirtualTaskEndTimestamp[e]??-1/0)}}class R extends S{currentWorkerNodeId=0;currentRoundId=0;roundWeights;defaultWorkerWeight;constructor(e,t=p){super(e,t),this.setRequiredStatistics(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 x extends S{requiredStatistics={runTime:!0,avgRunTime:!1,medRunTime:!1,waitTime:!1,avgWaitTime:!1,medWaitTime:!1};constructor(e,t=p){super(e,t),this.setRequiredStatistics(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.tasksUsage.runTime;if(0===i)return e;i<r&&(r=i,t=e)}return t}remove(){return!0}}class I extends S{constructor(e,t=p){super(e,t),this.setRequiredStatistics(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.tasksUsage,o=i.run+i.running;if(0===o)return e;o<r&&(r=o,t=e)}return t}remove(){return!0}}class E extends S{nextWorkerNodeId=0;constructor(e,t=p){super(e,t),this.setRequiredStatistics(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 S{requiredStatistics={runTime:!0,avgRunTime:!0,medRunTime:!1,waitTime:!1,avgWaitTime:!1,medWaitTime:!1};currentWorkerNodeId=0;defaultWorkerWeight;workerVirtualTaskRunTime=0;constructor(e,t=p){super(e,t),this.setRequiredStatistics(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=y.ROUND_ROBIN,r=p){this.workerChoiceStrategy=t,this.execute=this.execute.bind(this),this.workerChoiceStrategies=new Map([[y.ROUND_ROBIN,new(E.bind(this))(e,r)],[y.LEAST_USED,new(I.bind(this))(e,r)],[y.LEAST_BUSY,new(x.bind(this))(e,r)],[y.FAIR_SHARE,new(N.bind(this))(e,r)],[y.WEIGHTED_ROUND_ROBIN,new(b.bind(this))(e,r)],[y.INTERLEAVED_WEIGHTED_ROUND_ROBIN,new(R.bind(this))(e,r)]])}getRequiredStatistics(){return this.workerChoiceStrategies.get(this.workerChoiceStrategy).requiredStatistics}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,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),this.setupHook();for(let e=1;e<=this.numberOfWorkers;e++)this.createAndSetupWorker();!0===this.opts.enableEvents&&(this.emitter=new d),this.workerChoiceStrategyContext=new O(this,this.opts.workerChoiceStrategy,this.opts.workerChoiceStrategyOptions)}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===k.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.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;for(const e of this.workerNodes)this.setWorkerNodeTasksUsage(e,{run:0,running:0,runTime:0,runTimeHistory:new W,avgRunTime:0,medRunTime:0,waitTime:0,waitTimeHistory:new W,avgWaitTime:0,medWaitTime:0,error:0});this.workerChoiceStrategyContext.setWorkerChoiceStrategy(this.opts.workerChoiceStrategy),null!=t&&this.setWorkerChoiceStrategyOptions(t)}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)):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 s=performance.now(),i=this.chooseWorkerNode(),o={name:t,data:e??{},submissionTimestamp:s,id:r.randomUUID()},n=new Promise(((e,t)=>{this.promiseResponseMap.set(o.id,{resolve:e,reject:t,worker:this.workerNodes[i].worker})}));return!0===this.opts.enableTasksQueue&&(this.busy||this.workerNodes[i].tasksUsage.running>=this.opts.tasksQueueOptions.concurrency)?this.enqueueTask(i,o):this.executeTask(i,o),this.workerChoiceStrategyContext.update(i),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){++this.workerNodes[e].tasksUsage.running}afterTaskExecutionHook(e,t){const r=this.workerNodes[this.getWorkerNodeKey(e)].tasksUsage;--r.running,++r.run,null!=t.error&&++r.error,this.updateRunTimeTasksUsage(r,t),this.updateWaitTimeTasksUsage(r,t)}updateRunTimeTasksUsage(e,t){this.workerChoiceStrategyContext.getRequiredStatistics().runTime&&(e.runTime+=t.runTime??0,this.workerChoiceStrategyContext.getRequiredStatistics().avgRunTime&&0!==e.run&&(e.avgRunTime=e.runTime/e.run),this.workerChoiceStrategyContext.getRequiredStatistics().medRunTime&&null!=t.runTime&&(e.runTimeHistory.push(t.runTime),e.medRunTime=g(e.runTimeHistory)))}updateWaitTimeTasksUsage(e,t){this.workerChoiceStrategyContext.getRequiredStatistics().waitTime&&(e.waitTime+=t.waitTime??0,this.workerChoiceStrategyContext.getRequiredStatistics().avgWaitTime&&0!==e.run&&(e.avgWaitTime=e.waitTime/e.run),this.workerChoiceStrategyContext.getRequiredStatistics().medWaitTime&&null!=t.waitTime&&(e.waitTimeHistory.push(t.waitTime),e.medWaitTime=g(e.waitTimeHistory)))}chooseWorkerNode(){let e;if(this.type===k.dynamic&&!this.full&&this.internalBusy()){const t=this.createAndSetupWorker();this.registerWorkerMessageListener(t,(e=>{const r=this.getWorkerNodeKey(t);var s;s=w.HARD,(e.kill===s||null!=e.kill&&0===this.workerNodes[r].tasksUsage.running)&&(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(l.error,e)})),!0===this.opts.restartWorkerOnError&&e.on("error",(()=>{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.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(l.taskError,{error:e.error,errorData:e.errorData})):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(l.busy,this.info),this.type===k.dynamic&&this.full&&this.emitter?.emit(l.full,this.info))}setWorkerNodeTasksUsage(e,t){e.tasksUsage=t}pushWorkerNode(e){return this.workerNodes.push({worker:e,tasksUsage:{run:0,running:0,runTime:0,runTimeHistory:new W,avgRunTime:0,medRunTime:0,waitTime:0,waitTimeHistory:new W,avgWaitTime:0,medWaitTime:0,error:0},tasksQueue:new f})}setWorkerNode(e,t,r,s){this.workerNodes[e]={worker:t,tasksUsage:r,tasksQueue: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),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)}}class C extends v{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 k.fixed}get worker(){return c.cluster}get minSize(){return this.numberOfWorkers}get maxSize(){return this.numberOfWorkers}get busy(){return this.internalBusy()}}class z extends C{max;constructor(e,t,r,s={}){super(e,r,s),this.max=t}get type(){return k.dynamic}get maxSize(){return this.max}get busy(){return this.full&&this.internalBusy()}}class q extends v{constructor(e,t,r={}){super(e,t,r)}isMain(){return i}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(this.filePath,{env:n})}afterWorkerSetup(e){const{port1:t,port2:r}=new a;e.postMessage({parent:t},[t]),e.port1=t,e.port2=r,this.registerWorkerMessageListener(e,super.workerListener())}get type(){return k.fixed}get worker(){return c.thread}get minSize(){return this.numberOfWorkers}get maxSize(){return this.numberOfWorkers}get busy(){return this.internalBusy()}}class U extends q{max;constructor(e,t,r,s={}){super(e,r,s),this.max=t}get type(){return k.dynamic}get maxSize(){return this.max}get busy(){return this.full&&this.internalBusy()}}const Q="default",A=6e4,F=w.SOFT;class M extends u{isMain;mainWorker;opts;taskFunctions;lastTaskTimestamp;aliveInterval;constructor(e,t,r,s,i={killBehavior:F,maxInactiveTime:A}){super(e),this.isMain=t,this.mainWorker=s,this.opts=i,this.checkWorkerOptions(this.opts),this.checkTaskFunctions(r),this.isMain||(this.lastTaskTimestamp=performance.now(),this.aliveInterval=setInterval(this.checkAlive.bind(this),(this.opts.maxInactiveTime??A)/2),this.checkAlive.bind(this)()),this.mainWorker?.on("message",this.messageListener.bind(this))}checkWorkerOptions(e){this.opts.killBehavior=e.killBehavior??F,this.opts.maxInactiveTime=e.maxInactiveTime??A,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(!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(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())}getMainWorker(){if(null==this.mainWorker)throw new Error("Main worker was not set");return this.mainWorker}checkAlive(){performance.now()-this.lastTaskTimestamp>(this.opts.maxInactiveTime??A)&&this.sendToMainWorker({kill:this.opts.killBehavior})}handleError(e){return e}runSync(e,t){try{const r=performance.now(),s=r-(t.submissionTimestamp??0),i=e(t.data),o=performance.now()-r;this.sendToMainWorker({data:i,runTime:o,waitTime:s,id:t.id})}catch(e){const r=this.handleError(e);this.sendToMainWorker({error:r,errorData:t.data,id:t.id})}finally{!this.isMain&&(this.lastTaskTimestamp=performance.now())}}runAsync(e,t){const r=performance.now(),s=r-(t.submissionTimestamp??0);e(t.data).then((e=>{const i=performance.now()-r;return this.sendToMainWorker({data:e,runTime:i,waitTime:s,id:t.id}),null})).catch((e=>{const r=this.handleError(e);this.sendToMainWorker({error:r,errorData:t.data,id:t.id})})).finally((()=>{!this.isMain&&(this.lastTaskTimestamp=performance.now())})).catch(m)}getTaskFunction(e){e=e??Q;const t=this.taskFunctions.get(e);if(null==t)throw new Error(`Task function '${e}' not found`);return t}}class D extends M{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 V extends M{constructor(e,t={}){super("worker-thread-pool:poolifier",i,e,h,t)}sendToMainWorker(e){this.getMainWorker().postMessage(e)}}export{D as ClusterWorker,z as DynamicClusterPool,U as DynamicThreadPool,C as FixedClusterPool,q as FixedThreadPool,w as KillBehaviors,l as PoolEvents,k as PoolTypes,V as ThreadWorker,y as WorkerChoiceStrategies,c as WorkerTypes};
|
|
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};
|
|
@@ -134,6 +134,7 @@ export declare abstract class AbstractPool<Worker extends IWorker, Data = unknow
|
|
|
134
134
|
protected afterTaskExecutionHook(worker: Worker, message: MessageValue<Response>): void;
|
|
135
135
|
private updateRunTimeTasksUsage;
|
|
136
136
|
private updateWaitTimeTasksUsage;
|
|
137
|
+
private updateEluTasksUsage;
|
|
137
138
|
/**
|
|
138
139
|
* Chooses a worker node for the next task.
|
|
139
140
|
*
|
|
@@ -216,4 +217,5 @@ export declare abstract class AbstractPool<Worker extends IWorker, Data = unknow
|
|
|
216
217
|
private tasksQueueSize;
|
|
217
218
|
private flushTasksQueue;
|
|
218
219
|
private flushTasksQueues;
|
|
220
|
+
private setWorkerStatistics;
|
|
219
221
|
}
|
|
@@ -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, TaskStatistics, 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 taskStatistics: TaskStatistics;
|
|
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 setTaskStatistics(opts: WorkerChoiceStrategyOptions): void;
|
|
28
28
|
/** @inheritDoc */
|
|
29
29
|
abstract reset(): boolean;
|
|
30
30
|
/** @inheritDoc */
|
|
@@ -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, TaskStatistics, 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, RequiredStatistics, WorkerChoiceStrategyOpt
|
|
|
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 taskStatistics: TaskStatistics;
|
|
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, TaskStatistics, WorkerChoiceStrategyOptions } from './selection-strategies-types';
|
|
5
5
|
/**
|
|
6
6
|
* Selects the least busy worker.
|
|
7
7
|
*
|
|
@@ -11,7 +11,7 @@ import type { IWorkerChoiceStrategy, RequiredStatistics, WorkerChoiceStrategyOpt
|
|
|
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 taskStatistics: TaskStatistics;
|
|
15
15
|
/** @inheritDoc */
|
|
16
16
|
constructor(pool: IPool<Worker, Data, Response>, opts?: WorkerChoiceStrategyOptions);
|
|
17
17
|
/** @inheritDoc */
|
|
@@ -62,7 +62,7 @@ export interface WorkerChoiceStrategyOptions {
|
|
|
62
62
|
*
|
|
63
63
|
* @internal
|
|
64
64
|
*/
|
|
65
|
-
export interface
|
|
65
|
+
export interface TaskStatistics {
|
|
66
66
|
/**
|
|
67
67
|
* Require tasks runtime.
|
|
68
68
|
*/
|
|
@@ -87,15 +87,19 @@ export interface RequiredStatistics {
|
|
|
87
87
|
* Require tasks median wait time.
|
|
88
88
|
*/
|
|
89
89
|
medWaitTime: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Event loop utilization.
|
|
92
|
+
*/
|
|
93
|
+
elu: boolean;
|
|
90
94
|
}
|
|
91
95
|
/**
|
|
92
96
|
* Worker choice strategy interface.
|
|
93
97
|
*/
|
|
94
98
|
export interface IWorkerChoiceStrategy {
|
|
95
99
|
/**
|
|
96
|
-
* Required tasks
|
|
100
|
+
* Required tasks statistics.
|
|
97
101
|
*/
|
|
98
|
-
readonly
|
|
102
|
+
readonly taskStatistics: TaskStatistics;
|
|
99
103
|
/**
|
|
100
104
|
* Resets strategy internals.
|
|
101
105
|
*
|
|
@@ -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, TaskStatistics, 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, RequiredStatistics, WorkerChoiceStrategyOpt
|
|
|
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 taskStatistics: TaskStatistics;
|
|
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 { TaskStatistics, 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 in the context
|
|
23
|
+
* Gets the worker choice strategy task statistics in the context.
|
|
24
24
|
*
|
|
25
|
-
* @returns The
|
|
25
|
+
* @returns The task statistics.
|
|
26
26
|
*/
|
|
27
|
-
|
|
27
|
+
getTaskStatistics(): TaskStatistics;
|
|
28
28
|
/**
|
|
29
29
|
* Sets the worker choice strategy to use in the context.
|
|
30
30
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import { FixedThreadPool, type
|
|
1
|
+
import { type PoolType } from '../pool';
|
|
2
|
+
import { FixedThreadPool, type ThreadPoolOptions } from './fixed';
|
|
3
3
|
/**
|
|
4
4
|
* A thread pool with a dynamic number of threads, but a guaranteed minimum number of threads.
|
|
5
5
|
*
|
|
@@ -21,7 +21,7 @@ export declare class DynamicThreadPool<Data = unknown, Response = unknown> exten
|
|
|
21
21
|
* @param filePath - Path to an implementation of a `ThreadWorker` file, which can be relative or absolute.
|
|
22
22
|
* @param opts - Options for this dynamic thread pool.
|
|
23
23
|
*/
|
|
24
|
-
constructor(min: number, max: number, filePath: string, opts?:
|
|
24
|
+
constructor(min: number, max: number, filePath: string, opts?: ThreadPoolOptions);
|
|
25
25
|
/** @inheritDoc */
|
|
26
26
|
protected get type(): PoolType;
|
|
27
27
|
/** @inheritDoc */
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
-
import { MessageChannel, Worker } from 'node:worker_threads';
|
|
2
|
+
import { MessageChannel, Worker, type WorkerOptions } from 'node:worker_threads';
|
|
3
3
|
import type { Draft, MessageValue } from '../../utility-types';
|
|
4
4
|
import { AbstractPool } from '../abstract-pool';
|
|
5
5
|
import { type PoolOptions, type PoolType, type WorkerType } from '../pool';
|
|
6
|
+
/**
|
|
7
|
+
* Options for a poolifier thread pool.
|
|
8
|
+
*/
|
|
9
|
+
export interface ThreadPoolOptions extends PoolOptions<Worker> {
|
|
10
|
+
/**
|
|
11
|
+
* Worker options.
|
|
12
|
+
*
|
|
13
|
+
* @see https://nodejs.org/api/worker_threads.html#new-workerfilename-options
|
|
14
|
+
*/
|
|
15
|
+
workerOptions?: WorkerOptions;
|
|
16
|
+
}
|
|
6
17
|
/**
|
|
7
18
|
* A thread worker with message channels for communication between main thread and thread worker.
|
|
8
19
|
*/
|
|
@@ -20,6 +31,7 @@ export type ThreadWorkerWithMessageChannel = Worker & Draft<MessageChannel>;
|
|
|
20
31
|
* @since 0.0.1
|
|
21
32
|
*/
|
|
22
33
|
export declare class FixedThreadPool<Data = unknown, Response = unknown> extends AbstractPool<ThreadWorkerWithMessageChannel, Data, Response> {
|
|
34
|
+
protected readonly opts: ThreadPoolOptions;
|
|
23
35
|
/**
|
|
24
36
|
* Constructs a new poolifier fixed thread pool.
|
|
25
37
|
*
|
|
@@ -27,7 +39,7 @@ export declare class FixedThreadPool<Data = unknown, Response = unknown> extends
|
|
|
27
39
|
* @param filePath - Path to an implementation of a `ThreadWorker` file, which can be relative or absolute.
|
|
28
40
|
* @param opts - Options for this fixed thread pool.
|
|
29
41
|
*/
|
|
30
|
-
constructor(numberOfThreads: number, filePath: string, opts?:
|
|
42
|
+
constructor(numberOfThreads: number, filePath: string, opts?: ThreadPoolOptions);
|
|
31
43
|
/** @inheritDoc */
|
|
32
44
|
protected isMain(): boolean;
|
|
33
45
|
/** @inheritDoc */
|
package/lib/pools/worker.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import type { EventLoopUtilization } from 'node:perf_hooks';
|
|
1
3
|
import type { CircularArray } from '../circular-array';
|
|
2
4
|
import type { Queue } from '../queue';
|
|
3
5
|
/**
|
|
@@ -32,9 +34,9 @@ export interface Task<Data = unknown> {
|
|
|
32
34
|
*/
|
|
33
35
|
readonly data?: Data;
|
|
34
36
|
/**
|
|
35
|
-
*
|
|
37
|
+
* Timestamp.
|
|
36
38
|
*/
|
|
37
|
-
readonly
|
|
39
|
+
readonly timestamp?: number;
|
|
38
40
|
/**
|
|
39
41
|
* Message UUID.
|
|
40
42
|
*/
|
|
@@ -49,7 +51,7 @@ export interface TasksUsage {
|
|
|
49
51
|
/**
|
|
50
52
|
* Number of tasks executed.
|
|
51
53
|
*/
|
|
52
|
-
|
|
54
|
+
ran: number;
|
|
53
55
|
/**
|
|
54
56
|
* Number of tasks running.
|
|
55
57
|
*/
|
|
@@ -90,6 +92,10 @@ export interface TasksUsage {
|
|
|
90
92
|
* Number of tasks errored.
|
|
91
93
|
*/
|
|
92
94
|
error: number;
|
|
95
|
+
/**
|
|
96
|
+
* Event loop utilization.
|
|
97
|
+
*/
|
|
98
|
+
elu: EventLoopUtilization | undefined;
|
|
93
99
|
}
|
|
94
100
|
/**
|
|
95
101
|
* Worker interface.
|
package/lib/utility-types.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
/// <reference types="node" />
|
|
3
|
+
/// <reference types="node" />
|
|
3
4
|
import type { Worker as ClusterWorker } from 'node:cluster';
|
|
4
5
|
import type { MessagePort } from 'node:worker_threads';
|
|
6
|
+
import type { EventLoopUtilization } from 'node:perf_hooks';
|
|
5
7
|
import type { KillBehavior } from './worker/worker-options';
|
|
6
8
|
import type { IWorker, Task } from './pools/worker';
|
|
7
9
|
/**
|
|
@@ -12,6 +14,14 @@ import type { IWorker, Task } from './pools/worker';
|
|
|
12
14
|
export type Draft<T> = {
|
|
13
15
|
-readonly [P in keyof T]?: T[P];
|
|
14
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* Performance statistics computation.
|
|
19
|
+
*/
|
|
20
|
+
export interface WorkerStatistics {
|
|
21
|
+
runTime: boolean;
|
|
22
|
+
waitTime: boolean;
|
|
23
|
+
elu: boolean;
|
|
24
|
+
}
|
|
15
25
|
/**
|
|
16
26
|
* Message object that is passed between main worker and worker.
|
|
17
27
|
*
|
|
@@ -40,44 +50,19 @@ export interface MessageValue<Data = unknown, MainWorker extends ClusterWorker |
|
|
|
40
50
|
* Wait time.
|
|
41
51
|
*/
|
|
42
52
|
readonly waitTime?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Event loop utilization.
|
|
55
|
+
*/
|
|
56
|
+
readonly elu?: EventLoopUtilization;
|
|
43
57
|
/**
|
|
44
58
|
* Reference to main worker.
|
|
45
59
|
*/
|
|
46
60
|
readonly parent?: MainWorker;
|
|
61
|
+
/**
|
|
62
|
+
* Whether to compute the given statistics or not.
|
|
63
|
+
*/
|
|
64
|
+
readonly statistics?: WorkerStatistics;
|
|
47
65
|
}
|
|
48
|
-
/**
|
|
49
|
-
* Worker synchronous function that can be executed.
|
|
50
|
-
*
|
|
51
|
-
* @typeParam Data - Type of data sent to the worker. This can only be serializable data.
|
|
52
|
-
* @typeParam Response - Type of execution response. This can only be serializable data.
|
|
53
|
-
*/
|
|
54
|
-
export type WorkerSyncFunction<Data = unknown, Response = unknown> = (data?: Data) => Response;
|
|
55
|
-
/**
|
|
56
|
-
* Worker asynchronous function that can be executed.
|
|
57
|
-
* This function must return a promise.
|
|
58
|
-
*
|
|
59
|
-
* @typeParam Data - Type of data sent to the worker. This can only be serializable data.
|
|
60
|
-
* @typeParam Response - Type of execution response. This can only be serializable data.
|
|
61
|
-
*/
|
|
62
|
-
export type WorkerAsyncFunction<Data = unknown, Response = unknown> = (data?: Data) => Promise<Response>;
|
|
63
|
-
/**
|
|
64
|
-
* Worker function that can be executed.
|
|
65
|
-
* This function can be synchronous or asynchronous.
|
|
66
|
-
*
|
|
67
|
-
* @typeParam Data - Type of data sent to the worker. This can only be serializable data.
|
|
68
|
-
* @typeParam Response - Type of execution response. This can only be serializable data.
|
|
69
|
-
*/
|
|
70
|
-
export type WorkerFunction<Data = unknown, Response = unknown> = WorkerSyncFunction<Data, Response> | WorkerAsyncFunction<Data, Response>;
|
|
71
|
-
/**
|
|
72
|
-
* Worker functions that can be executed.
|
|
73
|
-
* This object can contain synchronous or asynchronous functions.
|
|
74
|
-
* The key is the name of the function.
|
|
75
|
-
* The value is the function itself.
|
|
76
|
-
*
|
|
77
|
-
* @typeParam Data - Type of data sent to the worker. This can only be serializable data.
|
|
78
|
-
* @typeParam Response - Type of execution response. This can only be serializable data.
|
|
79
|
-
*/
|
|
80
|
-
export type TaskFunctions<Data = unknown, Response = unknown> = Record<string, WorkerFunction<Data, Response>>;
|
|
81
66
|
/**
|
|
82
67
|
* An object holding the execution response promise resolve/reject callbacks.
|
|
83
68
|
*
|
|
@@ -2,11 +2,23 @@
|
|
|
2
2
|
/// <reference types="node" />
|
|
3
3
|
/// <reference types="node" />
|
|
4
4
|
/// <reference types="node" />
|
|
5
|
+
/// <reference types="node" />
|
|
5
6
|
import { AsyncResource } from 'node:async_hooks';
|
|
6
7
|
import type { Worker } from 'node:cluster';
|
|
7
8
|
import type { MessagePort } from 'node:worker_threads';
|
|
8
|
-
import
|
|
9
|
+
import { type EventLoopUtilization } from 'node:perf_hooks';
|
|
10
|
+
import type { MessageValue, WorkerStatistics } from '../utility-types';
|
|
9
11
|
import { type WorkerOptions } from './worker-options';
|
|
12
|
+
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
|
+
}
|
|
10
22
|
/**
|
|
11
23
|
* Base class that implements some shared logic for all poolifier workers.
|
|
12
24
|
*
|
|
@@ -26,6 +38,10 @@ export declare abstract class AbstractWorker<MainWorker extends Worker | Message
|
|
|
26
38
|
* Timestamp of the last task processed by this worker.
|
|
27
39
|
*/
|
|
28
40
|
protected lastTaskTimestamp: number;
|
|
41
|
+
/**
|
|
42
|
+
* Performance statistics computation.
|
|
43
|
+
*/
|
|
44
|
+
protected statistics: WorkerStatistics;
|
|
29
45
|
/**
|
|
30
46
|
* Handler id of the `aliveInterval` worker alive check.
|
|
31
47
|
*/
|
|
@@ -96,4 +112,6 @@ export declare abstract class AbstractWorker<MainWorker extends Worker | Message
|
|
|
96
112
|
* @param name - Name of the function that will be returned.
|
|
97
113
|
*/
|
|
98
114
|
private getTaskFunction;
|
|
115
|
+
private beginTaskPerformance;
|
|
116
|
+
private endTaskPerformance;
|
|
99
117
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import { type Worker } from 'node:cluster';
|
|
3
|
-
import type { MessageValue
|
|
3
|
+
import type { MessageValue } from '../utility-types';
|
|
4
4
|
import { AbstractWorker } from './abstract-worker';
|
|
5
5
|
import type { WorkerOptions } from './worker-options';
|
|
6
|
+
import type { TaskFunctions, WorkerFunction } from './worker-functions';
|
|
6
7
|
/**
|
|
7
8
|
* A cluster worker used by a poolifier `ClusterPool`.
|
|
8
9
|
*
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import { type MessagePort } from 'node:worker_threads';
|
|
3
|
-
import type { MessageValue
|
|
3
|
+
import type { MessageValue } from '../utility-types';
|
|
4
4
|
import { AbstractWorker } from './abstract-worker';
|
|
5
5
|
import type { WorkerOptions } from './worker-options';
|
|
6
|
+
import type { TaskFunctions, WorkerFunction } from './worker-functions';
|
|
6
7
|
/**
|
|
7
8
|
* A thread worker used by a poolifier `ThreadPool`.
|
|
8
9
|
*
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker synchronous function that can be executed.
|
|
3
|
+
*
|
|
4
|
+
* @typeParam Data - Type of data sent to the worker. This can only be serializable data.
|
|
5
|
+
* @typeParam Response - Type of execution response. This can only be serializable data.
|
|
6
|
+
*/
|
|
7
|
+
export type WorkerSyncFunction<Data = unknown, Response = unknown> = (data?: Data) => Response;
|
|
8
|
+
/**
|
|
9
|
+
* Worker asynchronous function that can be executed.
|
|
10
|
+
* This function must return a promise.
|
|
11
|
+
*
|
|
12
|
+
* @typeParam Data - Type of data sent to the worker. This can only be serializable data.
|
|
13
|
+
* @typeParam Response - Type of execution response. This can only be serializable data.
|
|
14
|
+
*/
|
|
15
|
+
export type WorkerAsyncFunction<Data = unknown, Response = unknown> = (data?: Data) => Promise<Response>;
|
|
16
|
+
/**
|
|
17
|
+
* Worker function that can be executed.
|
|
18
|
+
* This function can be synchronous or asynchronous.
|
|
19
|
+
*
|
|
20
|
+
* @typeParam Data - Type of data sent to the worker. This can only be serializable data.
|
|
21
|
+
* @typeParam Response - Type of execution response. This can only be serializable data.
|
|
22
|
+
*/
|
|
23
|
+
export type WorkerFunction<Data = unknown, Response = unknown> = WorkerSyncFunction<Data, Response> | WorkerAsyncFunction<Data, Response>;
|
|
24
|
+
/**
|
|
25
|
+
* Worker functions that can be executed.
|
|
26
|
+
* This object can contain synchronous or asynchronous functions.
|
|
27
|
+
* The key is the name of the function.
|
|
28
|
+
* The value is the function itself.
|
|
29
|
+
*
|
|
30
|
+
* @typeParam Data - Type of data sent to the worker. This can only be serializable data.
|
|
31
|
+
* @typeParam Response - Type of execution response. This can only be serializable data.
|
|
32
|
+
*/
|
|
33
|
+
export type TaskFunctions<Data = unknown, Response = unknown> = Record<string, WorkerFunction<Data, Response>>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "poolifier",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.4",
|
|
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",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"volta": {
|
|
28
28
|
"node": "20.2.0",
|
|
29
|
-
"pnpm": "8.6.
|
|
29
|
+
"pnpm": "8.6.1"
|
|
30
30
|
},
|
|
31
31
|
"repository": {
|
|
32
32
|
"type": "git",
|
|
@@ -85,8 +85,8 @@
|
|
|
85
85
|
"@rollup/plugin-terser": "^0.4.3",
|
|
86
86
|
"@rollup/plugin-typescript": "^11.1.1",
|
|
87
87
|
"@types/node": "^20.2.5",
|
|
88
|
-
"@typescript-eslint/eslint-plugin": "^5.59.
|
|
89
|
-
"@typescript-eslint/parser": "^5.59.
|
|
88
|
+
"@typescript-eslint/eslint-plugin": "^5.59.9",
|
|
89
|
+
"@typescript-eslint/parser": "^5.59.9",
|
|
90
90
|
"benny": "^3.7.1",
|
|
91
91
|
"c8": "^7.14.0",
|
|
92
92
|
"eslint": "^8.42.0",
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"eslint-define-config": "^1.20.0",
|
|
96
96
|
"eslint-import-resolver-typescript": "^3.5.5",
|
|
97
97
|
"eslint-plugin-import": "^2.27.5",
|
|
98
|
-
"eslint-plugin-jsdoc": "^46.2.
|
|
98
|
+
"eslint-plugin-jsdoc": "^46.2.6",
|
|
99
99
|
"eslint-plugin-n": "^16.0.0",
|
|
100
100
|
"eslint-plugin-promise": "^6.1.1",
|
|
101
101
|
"eslint-plugin-spellcheck": "^0.0.20",
|
|
@@ -107,15 +107,15 @@
|
|
|
107
107
|
"mocha": "^10.2.0",
|
|
108
108
|
"mochawesome": "^7.1.3",
|
|
109
109
|
"prettier": "^2.8.8",
|
|
110
|
-
"release-it": "^15.
|
|
111
|
-
"rollup": "^3.
|
|
110
|
+
"release-it": "^15.11.0",
|
|
111
|
+
"rollup": "^3.24.0",
|
|
112
112
|
"rollup-plugin-analyzer": "^4.0.0",
|
|
113
113
|
"rollup-plugin-command": "^1.1.3",
|
|
114
114
|
"rollup-plugin-delete": "^2.0.0",
|
|
115
115
|
"sinon": "^15.1.0",
|
|
116
116
|
"source-map-support": "^0.5.21",
|
|
117
117
|
"ts-standard": "^12.0.2",
|
|
118
|
-
"typedoc": "^0.24.
|
|
118
|
+
"typedoc": "^0.24.8",
|
|
119
119
|
"typescript": "^5.1.3"
|
|
120
120
|
},
|
|
121
121
|
"scripts": {
|