poolifier 2.4.8 → 2.4.10
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 +28 -27
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/index.mjs +1 -1
- package/lib/pools/abstract-pool.d.ts +15 -6
- package/lib/pools/pool.d.ts +27 -8
- package/lib/pools/selection-strategies/abstract-worker-choice-strategy.d.ts +4 -2
- package/lib/pools/selection-strategies/fair-share-worker-choice-strategy.d.ts +4 -1
- package/lib/pools/selection-strategies/less-busy-worker-choice-strategy.d.ts +4 -1
- package/lib/pools/selection-strategies/less-used-worker-choice-strategy.d.ts +4 -1
- package/lib/pools/selection-strategies/round-robin-worker-choice-strategy.d.ts +4 -1
- package/lib/pools/selection-strategies/selection-strategies-types.d.ts +8 -0
- package/lib/pools/selection-strategies/weighted-round-robin-worker-choice-strategy.d.ts +1 -6
- package/lib/pools/selection-strategies/worker-choice-strategy-context.d.ts +9 -3
- package/lib/pools/thread/fixed.d.ts +1 -1
- package/lib/pools/worker.d.ts +2 -2
- package/lib/utility-types.d.ts +25 -3
- package/lib/worker/abstract-worker.d.ts +5 -5
- package/lib/worker/cluster-worker.d.ts +2 -2
- package/lib/worker/thread-worker.d.ts +2 -2
- package/lib/worker/worker-options.d.ts +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div align="center">
|
|
2
|
-
<img src="./images/logo.png" width="340px" height="266px"/>
|
|
2
|
+
<img src="./images/logo.png" width="340px" height="266px"/>
|
|
3
3
|
</div>
|
|
4
4
|
|
|
5
5
|
<h2 align="center">Node Thread Pool and Cluster Pool :arrow_double_up: :on:</h2>
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
Poolifier is used to perform CPU intensive and I/O intensive tasks on nodejs servers, it implements worker pools (yes, more worker pool implementations, so you can choose which one fit better for you) using [worker-threads](https://nodejs.org/api/worker_threads.html#worker_threads_worker_threads) and cluster pools using [Node.js cluster](https://nodejs.org/api/cluster.html) modules.
|
|
33
33
|
With poolifier you can improve your **performance** and resolve problems related to the event loop.
|
|
34
34
|
Moreover you can execute your tasks using an API designed to improve the **developer experience**.
|
|
35
|
-
Please consult our
|
|
35
|
+
Please consult our [general guidelines](#general-guidance).
|
|
36
36
|
|
|
37
37
|
- Performance :racehorse: [benchmarks](./benchmarks/README.md)
|
|
38
38
|
- Security :bank: :cop: [](https://sonarcloud.io/dashboard?id=pioardi_poolifier) [](https://sonarcloud.io/dashboard?id=pioardi_poolifier)
|
|
@@ -81,7 +81,7 @@ Please consult our <a href="#general-guidance">general guidelines</a>
|
|
|
81
81
|
Node pool contains two [worker-threads](https://nodejs.org/api/worker_threads.html#worker_threads_worker_threads)/[cluster worker](https://nodejs.org/api/cluster.html#cluster_class_worker) pool implementations, you don't have to deal with worker-threads/cluster worker complexity.
|
|
82
82
|
The first implementation is a static worker pool, with a defined number of workers that are started at creation time and will be reused.
|
|
83
83
|
The second implementation is a dynamic worker pool with a number of worker started at creation time (these workers will be always active and reused) and other workers created when the load will increase (with an upper limit, these workers will be reused when active), the new created workers will be stopped after a configurable period of inactivity.
|
|
84
|
-
You have to implement your worker extending the ThreadWorker or ClusterWorker class
|
|
84
|
+
You have to implement your worker extending the ThreadWorker or ClusterWorker class.
|
|
85
85
|
|
|
86
86
|
## Installation
|
|
87
87
|
|
|
@@ -104,12 +104,11 @@ function yourFunction(data) {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
module.exports = new ThreadWorker(yourFunction, {
|
|
107
|
-
maxInactiveTime: 60000
|
|
108
|
-
async: false
|
|
107
|
+
maxInactiveTime: 60000
|
|
109
108
|
})
|
|
110
109
|
```
|
|
111
110
|
|
|
112
|
-
Instantiate your pool based on your
|
|
111
|
+
Instantiate your pool based on your needs :
|
|
113
112
|
|
|
114
113
|
```js
|
|
115
114
|
'use strict'
|
|
@@ -140,7 +139,7 @@ pool.execute({}).then(res => {
|
|
|
140
139
|
|
|
141
140
|
You can do the same with the classes ClusterWorker, FixedClusterPool and DynamicClusterPool.
|
|
142
141
|
|
|
143
|
-
**See examples folder for more details (in particular if you want to use a pool for [multiple functions](./examples/multiFunctionExample.js))
|
|
142
|
+
**See examples folder for more details (in particular if you want to use a pool for [multiple functions](./examples/multiFunctionExample.js))**.
|
|
144
143
|
**Now TypeScript is also supported, find how to use it into the example folder**.
|
|
145
144
|
|
|
146
145
|
Remember that workers can only send and receive serializable data.
|
|
@@ -149,9 +148,7 @@ Remember that workers can only send and receive serializable data.
|
|
|
149
148
|
|
|
150
149
|
Node versions >= 16.x are supported.
|
|
151
150
|
|
|
152
|
-
## API
|
|
153
|
-
|
|
154
|
-
### [Documentation](https://poolifier.github.io/poolifier/)
|
|
151
|
+
## [API](https://poolifier.github.io/poolifier/)
|
|
155
152
|
|
|
156
153
|
### `pool = new FixedThreadPool/FixedClusterPool(numberOfThreads/numberOfWorkers, filePath, opts)`
|
|
157
154
|
|
|
@@ -171,7 +168,7 @@ Node versions >= 16.x are supported.
|
|
|
171
168
|
- `WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN`: Submit tasks to worker using a weighted round robin scheduling algorithm based on tasks execution time
|
|
172
169
|
- `WorkerChoiceStrategies.FAIR_SHARE`: Submit tasks to worker using a fair share tasks scheduling algorithm based on tasks execution time
|
|
173
170
|
|
|
174
|
-
`WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN` and `WorkerChoiceStrategies.FAIR_SHARE` strategies are targeted to heavy and long tasks
|
|
171
|
+
`WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN` and `WorkerChoiceStrategies.FAIR_SHARE` strategies are targeted to heavy and long tasks.
|
|
175
172
|
Default: `WorkerChoiceStrategies.ROUND_ROBIN`
|
|
176
173
|
|
|
177
174
|
- `workerChoiceStrategyOptions` (optional) - The worker choice strategy options object to use in this pool.
|
|
@@ -179,17 +176,19 @@ Node versions >= 16.x are supported.
|
|
|
179
176
|
|
|
180
177
|
- `medRunTime` (optional) - Use the tasks median run time instead of the tasks average run time in worker choice strategies.
|
|
181
178
|
|
|
182
|
-
Default: { medRunTime: false }
|
|
179
|
+
Default: `{ medRunTime: false }`
|
|
183
180
|
|
|
184
|
-
- `enableEvents` (optional) - Events emission enablement in this pool.
|
|
185
|
-
|
|
181
|
+
- `enableEvents` (optional) - Events emission enablement in this pool.
|
|
182
|
+
Default: true
|
|
183
|
+
- `enableTasksQueue` (optional) - Tasks queue per worker enablement in this pool.
|
|
184
|
+
Default: false
|
|
186
185
|
|
|
187
|
-
- `tasksQueueOptions` (optional
|
|
186
|
+
- `tasksQueueOptions` (optional) - The worker tasks queue options object to use in this pool.
|
|
188
187
|
Properties:
|
|
189
188
|
|
|
190
189
|
- `concurrency` (optional) - The maximum number of tasks that can be executed concurrently on a worker.
|
|
191
190
|
|
|
192
|
-
Default: { concurrency: 1 }
|
|
191
|
+
Default: `{ concurrency: 1 }`
|
|
193
192
|
|
|
194
193
|
### `pool = new DynamicThreadPool/DynamicClusterPool(min, max, filePath, opts)`
|
|
195
194
|
|
|
@@ -200,8 +199,8 @@ Node versions >= 16.x are supported.
|
|
|
200
199
|
|
|
201
200
|
### `pool.execute(data)`
|
|
202
201
|
|
|
203
|
-
|
|
204
|
-
|
|
202
|
+
`data` (optional) An object that you want to pass to your worker implementation
|
|
203
|
+
This method is available on both pool implementations and returns a promise.
|
|
205
204
|
|
|
206
205
|
### `pool.destroy()`
|
|
207
206
|
|
|
@@ -213,14 +212,15 @@ This method will call the terminate method on each worker.
|
|
|
213
212
|
`fn` (mandatory) The function that you want to execute on the worker
|
|
214
213
|
`opts` (optional) An object with these properties:
|
|
215
214
|
|
|
216
|
-
- `maxInactiveTime` - Max time to wait tasks to work on
|
|
215
|
+
- `maxInactiveTime` (optional) - Max time to wait tasks to work on in milliseconds, after this period the new worker will die.
|
|
217
216
|
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.
|
|
218
217
|
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.
|
|
219
218
|
If `killBehavior` is set to `KillBehaviors.SOFT` your tasks have no timeout and your workers will not be terminated until your task is completed.
|
|
220
|
-
Default: 60000
|
|
219
|
+
Default: 60000
|
|
221
220
|
|
|
222
|
-
- `async` - true/false
|
|
223
|
-
|
|
221
|
+
- `async` (optional) - true/false. Set to true if your function contains async code pieces, else false.
|
|
222
|
+
Default: false
|
|
223
|
+
- `killBehavior` (optional) - Dictates if your async unit (worker/process) will be deleted in case that a task is active on it.
|
|
224
224
|
**KillBehaviors.SOFT**: If `currentTime - lastActiveTime` is greater than `maxInactiveTime` but a task is still running, then the worker **won't** be deleted.
|
|
225
225
|
**KillBehaviors.HARD**: If `currentTime - lastActiveTime` is greater than `maxInactiveTime` but a task is still running, then the worker will be deleted.
|
|
226
226
|
This option only apply to the newly created workers.
|
|
@@ -238,11 +238,11 @@ Please take a look at [which tasks run on the libuv thread pool](https://nodejs.
|
|
|
238
238
|
|
|
239
239
|
**If your task runs on libuv thread pool**, you can try to:
|
|
240
240
|
|
|
241
|
-
- Tune the libuv thread pool size setting the [UV_THREADPOOL_SIZE](https://nodejs.org/api/cli.html#cli_uv_threadpool_size_size)
|
|
241
|
+
- Tune the libuv thread pool size setting the [UV_THREADPOOL_SIZE](https://nodejs.org/api/cli.html#cli_uv_threadpool_size_size).
|
|
242
242
|
|
|
243
243
|
and/or
|
|
244
244
|
|
|
245
|
-
- Use poolifier cluster pool that spawning child processes will also increase the number of libuv threads since that any new child process comes with a separated libuv thread pool. **More threads does not mean more fast, so please tune your application
|
|
245
|
+
- Use poolifier cluster pool that spawning child processes will also increase the number of libuv threads since that any new child process comes with a separated libuv thread pool. **More threads does not mean more fast, so please tune your application**.
|
|
246
246
|
|
|
247
247
|
### Cluster vs Threads worker pools
|
|
248
248
|
|
|
@@ -254,7 +254,7 @@ Consider that by default Node.js already has great performance for I/O tasks (as
|
|
|
254
254
|
Cluster pools are built on top of Node.js [cluster](https://nodejs.org/api/cluster.html) module.
|
|
255
255
|
|
|
256
256
|
If your task contains code that runs on libuv plus code that is CPU intensive or I/O intensive you either split it either combine more strategies (i.e. tune the number of libuv threads and use cluster/thread pools).
|
|
257
|
-
But in general, **always profile your application
|
|
257
|
+
But in general, **always profile your application**.
|
|
258
258
|
|
|
259
259
|
### Fixed vs Dynamic pools
|
|
260
260
|
|
|
@@ -262,13 +262,14 @@ To choose your pool consider that with a FixedThreadPool/FixedClusterPool or a D
|
|
|
262
262
|
Increasing the memory footprint, your application will be ready to accept more tasks, but during idle time your application will consume more memory.
|
|
263
263
|
One good choose from my point of view is to profile your application using Fixed/Dynamic worker pool, and to see your application metrics when you increase/decrease the num of workers.
|
|
264
264
|
For example you could keep the memory footprint low choosing a DynamicThreadPool/DynamicClusterPool with 5 workers, and allow to create new workers until 50/100 when needed, this is the advantage to use the DynamicThreadPool/DynamicClusterPool.
|
|
265
|
-
But in general, **always profile your application
|
|
265
|
+
But in general, **always profile your application**.
|
|
266
266
|
|
|
267
267
|
## Contribute
|
|
268
268
|
|
|
269
|
-
See guidelines [CONTRIBUTING](CONTRIBUTING.md)
|
|
270
269
|
Choose your task here [2.4.x](https://github.com/orgs/poolifier/projects/1), propose an idea, a fix, an improvement.
|
|
271
270
|
|
|
271
|
+
See [CONTRIBUTING](CONTRIBUTING.md) guidelines.
|
|
272
|
+
|
|
272
273
|
## Team
|
|
273
274
|
|
|
274
275
|
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
package/lib/index.d.ts
CHANGED
|
@@ -16,5 +16,5 @@ export { ClusterWorker } from './worker/cluster-worker';
|
|
|
16
16
|
export { ThreadWorker } from './worker/thread-worker';
|
|
17
17
|
export { KillBehaviors } from './worker/worker-options';
|
|
18
18
|
export type { KillBehavior, WorkerOptions } from './worker/worker-options';
|
|
19
|
-
export type { Draft, PromiseResponseWrapper, MessageValue } from './utility-types';
|
|
19
|
+
export type { Draft, PromiseResponseWrapper, MessageValue, WorkerAsyncFunction, WorkerFunction, WorkerSyncFunction } from './utility-types';
|
|
20
20
|
export type { CircularArray } from './circular-array';
|
package/lib/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var e,r=require("node:events"),t=require("node:cluster"),s=require("node:crypto"),i=require("node:os"),o=require("node:worker_threads"),n=require("node:async_hooks");!function(e){e.FIXED="fixed",e.DYNAMIC="dynamic"}(e||(e={}));class a extends r{}const h=Object.freeze({full:"full",busy:"busy"}),u=Object.freeze((()=>{})),k={medRunTime:!1},c=Object.freeze({SOFT:"SOFT",HARD:"HARD"});const l=Object.freeze({ROUND_ROBIN:"ROUND_ROBIN",LESS_USED:"LESS_USED",LESS_BUSY:"LESS_BUSY",FAIR_SHARE:"FAIR_SHARE",WEIGHTED_ROUND_ROBIN:"WEIGHTED_ROUND_ROBIN"});class d{pool;opts;isDynamicPool;requiredStatistics={runTime:!1,avgRunTime:!1,medRunTime:!1};constructor(r,t=k){this.pool=r,this.opts=t,this.checkOptions(this.opts),this.isDynamicPool=this.pool.type===e.DYNAMIC,this.choose.bind(this)}checkOptions(e){this.requiredStatistics.avgRunTime&&!0===e.medRunTime&&(this.requiredStatistics.medRunTime=!0)}}class p extends d{requiredStatistics={runTime:!0,avgRunTime:!0,medRunTime:!1};workerLastVirtualTaskTimestamp=new Map;reset(){return this.workerLastVirtualTaskTimestamp.clear(),!0}choose(){let e,r=1/0;for(const[t]of this.pool.workerNodes.entries()){this.computeWorkerLastVirtualTaskTimestamp(t);const s=this.workerLastVirtualTaskTimestamp.get(t)?.end??0;s<r&&(r=s,e=t)}return e}remove(e){const r=this.workerLastVirtualTaskTimestamp.delete(e);for(const[r,t]of this.workerLastVirtualTaskTimestamp.entries())r>e&&this.workerLastVirtualTaskTimestamp.set(r-1,t);return r}computeWorkerLastVirtualTaskTimestamp(e){const r=Math.max(performance.now(),this.workerLastVirtualTaskTimestamp.get(e)?.end??-1/0),t=this.requiredStatistics.medRunTime?this.pool.workerNodes[e].tasksUsage.medRunTime:this.pool.workerNodes[e].tasksUsage.avgRunTime;this.workerLastVirtualTaskTimestamp.set(e,{start:r,end:r+(t??0)})}}class m extends d{requiredStatistics={runTime:!0,avgRunTime:!1,medRunTime:!1};reset(){return!0}choose(){const e=this.pool.findFreeWorkerNodeKey();if(-1!==e)return e;let r,t=1/0;for(const[e,s]of this.pool.workerNodes.entries()){const i=s.tasksUsage.runTime;if(0===i)return e;i<t&&(t=i,r=e)}return r}remove(e){return!0}}class g extends d{reset(){return!0}choose(){const e=this.pool.findFreeWorkerNodeKey();if(-1!==e)return e;let r,t=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<t&&(t=o,r=e)}return r}remove(e){return!0}}class T extends d{nextWorkerNodeId=0;reset(){return this.nextWorkerNodeId=0,!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.nextWorkerNodeId>this.pool.workerNodes.length-1?this.pool.workerNodes.length-1:this.nextWorkerNodeId),!0}}class w extends d{requiredStatistics={runTime:!0,avgRunTime:!0,medRunTime:!1};currentWorkerNodeId=0;defaultWorkerWeight;workersTaskRunTime=new Map;constructor(e,r){super(e,r),this.defaultWorkerWeight=this.computeWorkerWeight(),this.initWorkersTaskRunTime()}reset(){return this.currentWorkerNodeId=0,this.workersTaskRunTime.clear(),this.initWorkersTaskRunTime(),!0}choose(){const e=this.currentWorkerNodeId;this.isDynamicPool&&!this.workersTaskRunTime.has(e)&&this.initWorkerTaskRunTime(e);const r=this.workersTaskRunTime.get(e)?.runTime??0,t=this.workersTaskRunTime.get(e)?.weight??this.defaultWorkerWeight;return r<t?this.setWorkerTaskRunTime(e,t,r+(this.getWorkerVirtualTaskRunTime(e)??0)):(this.currentWorkerNodeId=this.currentWorkerNodeId===this.pool.workerNodes.length-1?0:this.currentWorkerNodeId+1,this.setWorkerTaskRunTime(this.currentWorkerNodeId,t,0)),e}remove(e){this.currentWorkerNodeId===e&&(0===this.pool.workerNodes.length?this.currentWorkerNodeId=0:this.currentWorkerNodeId=this.currentWorkerNodeId>this.pool.workerNodes.length-1?this.pool.workerNodes.length-1:this.currentWorkerNodeId);const r=this.workersTaskRunTime.delete(e);for(const[r,t]of this.workersTaskRunTime)r>e&&this.workersTaskRunTime.set(r-1,t);return r}initWorkersTaskRunTime(){for(const[e]of this.pool.workerNodes.entries())this.initWorkerTaskRunTime(e)}initWorkerTaskRunTime(e){this.setWorkerTaskRunTime(e,this.defaultWorkerWeight,0)}setWorkerTaskRunTime(e,r,t){this.workersTaskRunTime.set(e,{weight:r,runTime:t})}getWorkerVirtualTaskRunTime(e){return this.requiredStatistics.medRunTime?this.pool.workerNodes[e].tasksUsage.medRunTime:this.pool.workerNodes[e].tasksUsage.avgRunTime}computeWorkerWeight(){let e=0;for(const r of i.cpus()){const t=r.speed.toString().length-1;e+=1/(r.speed/Math.pow(10,t))*Math.pow(10,t)}return Math.round(e/i.cpus().length)}}class f{workerChoiceStrategyType;workerChoiceStrategies;constructor(e,r=l.ROUND_ROBIN,t=k){this.workerChoiceStrategyType=r,this.execute.bind(this),this.workerChoiceStrategies=new Map([[l.ROUND_ROBIN,new T(e,t)],[l.LESS_USED,new g(e,t)],[l.LESS_BUSY,new m(e,t)],[l.FAIR_SHARE,new p(e,t)],[l.WEIGHTED_ROUND_ROBIN,new w(e,t)]])}getRequiredStatistics(){return this.workerChoiceStrategies.get(this.workerChoiceStrategyType).requiredStatistics}setWorkerChoiceStrategy(e){this.workerChoiceStrategyType!==e&&(this.workerChoiceStrategyType=e),this.workerChoiceStrategies.get(this.workerChoiceStrategyType)?.reset()}execute(){return this.workerChoiceStrategies.get(this.workerChoiceStrategyType).choose()}remove(e){return this.workerChoiceStrategies.get(this.workerChoiceStrategyType).remove(e)}}class W extends Array{size;constructor(e=1024,...r){super(),this.checkSize(e),this.size=e,arguments.length>1&&this.push(...r)}push(...e){const r=super.push(...e);return r>this.size&&super.splice(0,r-this.size),this.length}unshift(...e){return super.unshift(...e)>this.size&&super.splice(this.size,e.length),this.length}concat(...e){const r=super.concat(e);return r.size=this.size,r.length>r.size&&r.splice(0,r.length-r.size),r}splice(e,r,...t){let s;return arguments.length>=3&&void 0!==r?(s=super.splice(e,r),this.push(...t)):s=2===arguments.length?super.splice(e,r):super.splice(e),s}resize(e){if(this.checkSize(e),0===e)this.length=0;else if(e<this.size)for(let r=e;r<this.size;r++)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{numberOfWorkers;filePath;opts;workerNodes=[];emitter;promiseResponseMap=new Map;workerChoiceStrategyContext;constructor(e,r,t){if(this.numberOfWorkers=e,this.filePath=r,this.opts=t,!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.bind(this),this.executeTask.bind(this),this.enqueueTask.bind(this),this.checkAndEmitEvents.bind(this),this.setupHook();for(let e=1;e<=this.numberOfWorkers;e++)this.createAndSetupWorker();!0===this.opts.enableEvents&&(this.emitter=new a),this.workerChoiceStrategyContext=new f(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(r){if(null==r)throw new Error("Cannot instantiate a pool without specifying the number of workers");if(!Number.isSafeInteger(r))throw new TypeError("Cannot instantiate a pool with a non integer number of workers");if(r<0)throw new RangeError("Cannot instantiate a pool with a negative number of workers");if(this.type===e.FIXED&&0===r)throw new Error("Cannot instantiate a fixed pool with no worker")}checkPoolOptions(e){if(this.opts.workerChoiceStrategy=e.workerChoiceStrategy??l.ROUND_ROBIN,this.checkValidWorkerChoiceStrategy(this.opts.workerChoiceStrategy),this.opts.workerChoiceStrategyOptions=e.workerChoiceStrategyOptions??k,this.opts.enableEvents=e.enableEvents??!0,this.opts.enableTasksQueue=e.enableTasksQueue??!1,this.opts.enableTasksQueue){if(e.tasksQueueOptions?.concurrency<=0)throw new Error(`Invalid worker tasks concurrency '${e.tasksQueueOptions.concurrency}'`);this.opts.tasksQueueOptions={concurrency:e.tasksQueueOptions?.concurrency??1}}}checkValidWorkerChoiceStrategy(e){if(!Object.values(l).includes(e))throw new Error(`Invalid worker choice strategy '${e}'`)}get numberOfRunningTasks(){return this.workerNodes.reduce(((e,r)=>e+r.tasksUsage.running),0)}get numberOfQueuedTasks(){return!1===this.opts.enableTasksQueue?0:this.workerNodes.reduce(((e,r)=>e+r.tasksQueue.length),0)}getWorkerNodeKey(e){return this.workerNodes.findIndex((r=>r.worker===e))}setWorkerChoiceStrategy(e){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,error:0});this.workerChoiceStrategyContext.setWorkerChoiceStrategy(e)}internalBusy(){return-1===this.findFreeWorkerNodeKey()}findFreeWorkerNodeKey(){return this.workerNodes.findIndex((e=>0===e.tasksUsage?.running))}async execute(e){const[r,t]=this.chooseWorkerNode(),i={data:e??{},id:s.randomUUID()},o=new Promise(((e,r)=>{this.promiseResponseMap.set(i.id,{resolve:e,reject:r,worker:t.worker})}));return!0===this.opts.enableTasksQueue&&(this.busy||this.workerNodes[r].tasksUsage.running>=this.opts.tasksQueueOptions.concurrency)?this.enqueueTask(r,i):this.executeTask(r,i),this.checkAndEmitEvents(),o}async destroy(){await Promise.all(this.workerNodes.map((async(e,r)=>{this.flushTasksQueue(r),await this.destroyWorker(e.worker)})))}setupHook(){}beforeTaskExecutionHook(e){++this.workerNodes[e].tasksUsage.running}afterTaskExecutionHook(e,r){const t=this.getWorkerTasksUsage(e);--t.running,++t.run,null!=r.error&&++t.error,this.workerChoiceStrategyContext.getRequiredStatistics().runTime&&(t.runTime+=r.runTime??0,this.workerChoiceStrategyContext.getRequiredStatistics().avgRunTime&&0!==t.run&&(t.avgRunTime=t.runTime/t.run),this.workerChoiceStrategyContext.getRequiredStatistics().medRunTime&&(t.runTimeHistory.push(r.runTime??0),t.medRunTime=(e=>{if(Array.isArray(e)&&1===e.length)return e[0];const r=e.slice().sort(((e,r)=>e-r)),t=Math.floor(r.length/2);return r.length%2==0?r[t/2]:(r[t-1]+r[t])/2})(t.runTimeHistory)))}chooseWorkerNode(){let r;if(this.type===e.DYNAMIC&&!this.full&&this.internalBusy()){const e=this.createAndSetupWorker();this.registerWorkerMessageListener(e,(r=>{var t;t=c.HARD,(r.kill===t||null!=r.kill&&0===this.getWorkerTasksUsage(e)?.running)&&(this.flushTasksQueueByWorker(e),this.destroyWorker(e))})),r=this.getWorkerNodeKey(e)}else r=this.workerChoiceStrategyContext.execute();return[r,this.workerNodes[r]]}createAndSetupWorker(){const e=this.createWorker();return e.on("message",this.opts.messageHandler??u),e.on("error",this.opts.errorHandler??u),e.on("online",this.opts.onlineHandler??u),e.on("exit",this.opts.exitHandler??u),e.once("exit",(()=>{this.removeWorkerNode(e)})),this.pushWorkerNode(e),this.afterWorkerSetup(e),e}workerListener(){return e=>{if(null!=e.id){const r=this.promiseResponseMap.get(e.id);if(null!=r){null!=e.error?r.reject(e.error):r.resolve(e.data),this.afterTaskExecutionHook(r.worker,e),this.promiseResponseMap.delete(e.id);const t=this.getWorkerNodeKey(r.worker);!0===this.opts.enableTasksQueue&&this.tasksQueueSize(t)>0&&this.executeTask(t,this.dequeueTask(t))}}}}checkAndEmitEvents(){!0===this.opts.enableEvents&&(this.busy&&this.emitter?.emit(h.busy),this.type===e.DYNAMIC&&this.full&&this.emitter?.emit(h.full))}setWorkerNodeTasksUsage(e,r){e.tasksUsage=r}getWorkerTasksUsage(e){const r=this.getWorkerNodeKey(e);if(-1!==r)return this.workerNodes[r].tasksUsage;throw new Error("Worker could not be found in the pool worker nodes")}pushWorkerNode(e){return this.workerNodes.push({worker:e,tasksUsage:{run:0,running:0,runTime:0,runTimeHistory:new W,avgRunTime:0,medRunTime:0,error:0},tasksQueue:[]})}setWorkerNode(e,r,t,s){this.workerNodes[e]={worker:r,tasksUsage:t,tasksQueue:s}}removeWorkerNode(e){const r=this.getWorkerNodeKey(e);this.workerNodes.splice(r,1),this.workerChoiceStrategyContext.remove(r)}executeTask(e,r){this.beforeTaskExecutionHook(e),this.sendToWorker(this.workerNodes[e].worker,r)}enqueueTask(e,r){return this.workerNodes[e].tasksQueue.push(r)}dequeueTask(e){return this.workerNodes[e].tasksQueue.shift()}tasksQueueSize(e){return this.workerNodes[e].tasksQueue.length}flushTasksQueue(e){if(this.tasksQueueSize(e)>0)for(const r of this.workerNodes[e].tasksQueue)this.executeTask(e,r)}flushTasksQueueByWorker(e){const r=this.getWorkerNodeKey(e);this.flushTasksQueue(r)}}class N extends y{opts;constructor(e,r,t={}){super(e,r,t),this.opts=t}setupHook(){t.setupPrimary({...this.opts.settings,exec:this.filePath})}isMain(){return t.isPrimary}destroyWorker(e){this.sendToWorker(e,{kill:1}),e.kill()}sendToWorker(e,r){e.send(r)}registerWorkerMessageListener(e,r){e.on("message",r)}createWorker(){return t.fork(this.opts.env)}afterWorkerSetup(e){this.registerWorkerMessageListener(e,super.workerListener())}get type(){return e.FIXED}get full(){return this.workerNodes.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}class S extends y{constructor(e,r,t={}){super(e,r,t)}isMain(){return o.isMainThread}async destroyWorker(e){this.sendToWorker(e,{kill:1}),await e.terminate()}sendToWorker(e,r){e.postMessage(r)}registerWorkerMessageListener(e,r){e.port2?.on("message",r)}createWorker(){return new o.Worker(this.filePath,{env:o.SHARE_ENV})}afterWorkerSetup(e){const{port1:r,port2:t}=new o.MessageChannel;e.postMessage({parent:r},[r]),e.port1=r,e.port2=t,this.registerWorkerMessageListener(e,super.workerListener())}get type(){return e.FIXED}get full(){return this.workerNodes.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}const R=6e4,x=c.SOFT;class I extends n.AsyncResource{isMain;mainWorker;opts;lastTaskTimestamp;aliveInterval;constructor(e,r,t,s,i={killBehavior:x,maxInactiveTime:R}){super(e),this.isMain=r,this.mainWorker=s,this.opts=i,this.checkFunctionInput(t),this.checkWorkerOptions(this.opts),this.isMain||(this.lastTaskTimestamp=performance.now(),this.aliveInterval=setInterval(this.checkAlive.bind(this),(this.opts.maxInactiveTime??R)/2),this.checkAlive.bind(this)()),this.mainWorker?.on("message",(e=>{this.messageListener(e,t)}))}messageListener(e,r){null!=e.data&&null!=e.id?!0===this.opts.async?this.runInAsyncScope(this.runAsync.bind(this),this,r,e):this.runInAsyncScope(this.run.bind(this),this,r,e):null!=e.parent?this.mainWorker=e.parent:null!=e.kill&&(null!=this.aliveInterval&&clearInterval(this.aliveInterval),this.emitDestroy())}checkWorkerOptions(e){this.opts.killBehavior=e.killBehavior??x,this.opts.maxInactiveTime=e.maxInactiveTime??R,this.opts.async=e.async??!1}checkFunctionInput(e){if(null==e)throw new Error("fn parameter is mandatory");if("function"!=typeof e)throw new TypeError("fn parameter is not a function")}getMainWorker(){if(null==this.mainWorker)throw new Error("Main worker was not set");return this.mainWorker}checkAlive(){performance.now()-this.lastTaskTimestamp>(this.opts.maxInactiveTime??R)&&this.sendToMainWorker({kill:this.opts.killBehavior})}handleError(e){return e}run(e,r){try{const t=performance.now(),s=e(r.data),i=performance.now()-t;this.sendToMainWorker({data:s,id:r.id,runTime:i})}catch(e){const t=this.handleError(e);this.sendToMainWorker({error:t,id:r.id})}finally{!this.isMain&&(this.lastTaskTimestamp=performance.now())}}runAsync(e,r){const t=performance.now();e(r.data).then((e=>{const s=performance.now()-t;return this.sendToMainWorker({data:e,id:r.id,runTime:s}),null})).catch((e=>{const t=this.handleError(e);this.sendToMainWorker({error:t,id:r.id})})).finally((()=>{!this.isMain&&(this.lastTaskTimestamp=performance.now())})).catch(u)}}exports.ClusterWorker=class extends I{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 N{max;constructor(e,r,t,s={}){super(e,t,s),this.max=r}get type(){return e.DYNAMIC}get full(){return this.workerNodes.length===this.max}get busy(){return this.full&&this.internalBusy()}},exports.DynamicThreadPool=class extends S{max;constructor(e,r,t,s={}){super(e,t,s),this.max=r}get type(){return e.DYNAMIC}get full(){return this.workerNodes.length===this.max}get busy(){return this.full&&this.internalBusy()}},exports.FixedClusterPool=N,exports.FixedThreadPool=S,exports.KillBehaviors=c,exports.PoolEvents=h,exports.ThreadWorker=class extends I{constructor(e,r={}){super("worker-thread-pool:poolifier",o.isMainThread,e,o.parentPort,r)}sendToMainWorker(e){this.getMainWorker().postMessage(e)}},exports.WorkerChoiceStrategies=l;
|
|
1
|
+
"use strict";var e,t=require("node:events"),r=require("node:cluster"),s=require("node:crypto"),i=require("node:os"),o=require("node:worker_threads"),n=require("node:async_hooks");!function(e){e.FIXED="fixed",e.DYNAMIC="dynamic"}(e||(e={}));class h extends t{}const a=Object.freeze({full:"full",busy:"busy"}),u=Object.freeze((()=>{})),k={medRunTime:!1},c=Object.freeze({SOFT:"SOFT",HARD:"HARD"});const l=Object.freeze({ROUND_ROBIN:"ROUND_ROBIN",LESS_USED:"LESS_USED",LESS_BUSY:"LESS_BUSY",FAIR_SHARE:"FAIR_SHARE",WEIGHTED_ROUND_ROBIN:"WEIGHTED_ROUND_ROBIN"});class d{pool;opts;isDynamicPool;requiredStatistics={runTime:!1,avgRunTime:!1,medRunTime:!1};constructor(t,r=k){this.pool=t,this.opts=r,this.isDynamicPool=this.pool.type===e.DYNAMIC,this.choose=this.choose.bind(this)}checkOptions(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)}setOptions(e){e=e??k,this.checkOptions(e),this.opts=e}}class p extends d{requiredStatistics={runTime:!0,avgRunTime:!0,medRunTime:!1};workerLastVirtualTaskTimestamp=new Map;constructor(e,t=k){super(e,t),this.checkOptions(this.opts)}reset(){return this.workerLastVirtualTaskTimestamp.clear(),!0}choose(){let e,t=1/0;for(const[r]of this.pool.workerNodes.entries()){this.computeWorkerLastVirtualTaskTimestamp(r);const s=this.workerLastVirtualTaskTimestamp.get(r)?.end??0;s<t&&(t=s,e=r)}return e}remove(e){const t=this.workerLastVirtualTaskTimestamp.delete(e);for(const[t,r]of this.workerLastVirtualTaskTimestamp.entries())t>e&&this.workerLastVirtualTaskTimestamp.set(t-1,r);return t}computeWorkerLastVirtualTaskTimestamp(e){const t=Math.max(performance.now(),this.workerLastVirtualTaskTimestamp.get(e)?.end??-1/0),r=this.requiredStatistics.medRunTime?this.pool.workerNodes[e].tasksUsage.medRunTime:this.pool.workerNodes[e].tasksUsage.avgRunTime;this.workerLastVirtualTaskTimestamp.set(e,{start:t,end:t+(r??0)})}}class m extends d{requiredStatistics={runTime:!0,avgRunTime:!1,medRunTime:!1};constructor(e,t=k){super(e,t),this.checkOptions(this.opts)}reset(){return!0}choose(){const e=this.pool.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(e){return!0}}class T extends d{constructor(e,t=k){super(e,t),this.checkOptions(this.opts)}reset(){return!0}choose(){const e=this.pool.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(e){return!0}}class g extends d{nextWorkerNodeId=0;constructor(e,t=k){super(e,t),this.checkOptions(this.opts)}reset(){return this.nextWorkerNodeId=0,!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.nextWorkerNodeId>this.pool.workerNodes.length-1?this.pool.workerNodes.length-1:this.nextWorkerNodeId),!0}}class w extends d{requiredStatistics={runTime:!0,avgRunTime:!0,medRunTime:!1};currentWorkerNodeId=0;defaultWorkerWeight;workersTaskRunTime=new Map;constructor(e,t=k){super(e,t),this.checkOptions(this.opts),this.defaultWorkerWeight=this.computeWorkerWeight(),this.initWorkersTaskRunTime()}reset(){return this.currentWorkerNodeId=0,this.workersTaskRunTime.clear(),this.initWorkersTaskRunTime(),!0}choose(){const e=this.currentWorkerNodeId;this.isDynamicPool&&!this.workersTaskRunTime.has(e)&&this.initWorkerTaskRunTime(e);const t=this.workersTaskRunTime.get(e)?.runTime??0,r=this.workersTaskRunTime.get(e)?.weight??this.defaultWorkerWeight;return t<r?this.setWorkerTaskRunTime(e,r,t+(this.getWorkerVirtualTaskRunTime(e)??0)):(this.currentWorkerNodeId=this.currentWorkerNodeId===this.pool.workerNodes.length-1?0:this.currentWorkerNodeId+1,this.setWorkerTaskRunTime(this.currentWorkerNodeId,r,0)),e}remove(e){this.currentWorkerNodeId===e&&(0===this.pool.workerNodes.length?this.currentWorkerNodeId=0:this.currentWorkerNodeId=this.currentWorkerNodeId>this.pool.workerNodes.length-1?this.pool.workerNodes.length-1:this.currentWorkerNodeId);const t=this.workersTaskRunTime.delete(e);for(const[t,r]of this.workersTaskRunTime)t>e&&this.workersTaskRunTime.set(t-1,r);return t}initWorkersTaskRunTime(){for(const[e]of this.pool.workerNodes.entries())this.initWorkerTaskRunTime(e)}initWorkerTaskRunTime(e){this.setWorkerTaskRunTime(e,this.defaultWorkerWeight,0)}setWorkerTaskRunTime(e,t,r){this.workersTaskRunTime.set(e,{weight:t,runTime:r})}getWorkerVirtualTaskRunTime(e){return this.requiredStatistics.medRunTime?this.pool.workerNodes[e].tasksUsage.medRunTime:this.pool.workerNodes[e].tasksUsage.avgRunTime}computeWorkerWeight(){let e=0;for(const t of i.cpus()){const r=t.speed.toString().length-1;e+=1/(t.speed/Math.pow(10,r))*Math.pow(10,r)}return Math.round(e/i.cpus().length)}}class f{workerChoiceStrategy;workerChoiceStrategies;constructor(e,t=l.ROUND_ROBIN,r=k){this.workerChoiceStrategy=t,this.execute=this.execute.bind(this),this.workerChoiceStrategies=new Map([[l.ROUND_ROBIN,new(g.bind(this))(e,r)],[l.LESS_USED,new(T.bind(this))(e,r)],[l.LESS_BUSY,new(m.bind(this))(e,r)],[l.FAIR_SHARE,new(p.bind(this))(e,r)],[l.WEIGHTED_ROUND_ROBIN,new(w.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()}execute(){return this.workerChoiceStrategies.get(this.workerChoiceStrategy).choose()}remove(e){return this.workerChoiceStrategies.get(this.workerChoiceStrategy).remove(e)}setOptions(e){this.workerChoiceStrategies.forEach((t=>{t.setOptions(e)}))}}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 y{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 f(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(t){if(null==t)throw new Error("Cannot instantiate a pool without specifying the number of workers");if(!Number.isSafeInteger(t))throw new TypeError("Cannot instantiate a pool with a non integer number of workers");if(t<0)throw new RangeError("Cannot instantiate a pool with a negative number of workers");if(this.type===e.FIXED&&0===t)throw new Error("Cannot instantiate a fixed pool with no worker")}checkPoolOptions(e){this.opts.workerChoiceStrategy=e.workerChoiceStrategy??l.ROUND_ROBIN,this.checkValidWorkerChoiceStrategy(this.opts.workerChoiceStrategy),this.opts.workerChoiceStrategyOptions=e.workerChoiceStrategyOptions??k,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(l).includes(e))throw new Error(`Invalid worker choice strategy '${e}'`)}checkValidTasksQueueOptions(e){if(e?.concurrency<=0)throw new Error(`Invalid worker tasks concurrency '${e.concurrency}'`)}get numberOfRunningTasks(){return this.workerNodes.reduce(((e,t)=>e+t.tasksUsage.running),0)}get numberOfQueuedTasks(){return!1===this.opts.enableTasksQueue?0:this.workerNodes.reduce(((e,t)=>e+t.tasksQueue.length),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,error:0});this.workerChoiceStrategyContext.setWorkerChoiceStrategy(this.opts.workerChoiceStrategy),null!=t&&this.setWorkerChoiceStrategyOptions(t)}setWorkerChoiceStrategyOptions(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}}internalBusy(){return-1===this.findFreeWorkerNodeKey()}findFreeWorkerNodeKey(){return this.workerNodes.findIndex((e=>0===e.tasksUsage?.running))}async execute(e){const[t,r]=this.chooseWorkerNode(),i={data:e??{},id:s.randomUUID()},o=new Promise(((e,t)=>{this.promiseResponseMap.set(i.id,{resolve:e,reject:t,worker:r.worker})}));return!0===this.opts.enableTasksQueue&&(this.busy||this.workerNodes[t].tasksUsage.running>=this.opts.tasksQueueOptions.concurrency)?this.enqueueTask(t,i):this.executeTask(t,i),this.checkAndEmitEvents(),o}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.getWorkerTasksUsage(e);--r.running,++r.run,null!=t.error&&++r.error,this.workerChoiceStrategyContext.getRequiredStatistics().runTime&&(r.runTime+=t.runTime??0,this.workerChoiceStrategyContext.getRequiredStatistics().avgRunTime&&0!==r.run&&(r.avgRunTime=r.runTime/r.run),this.workerChoiceStrategyContext.getRequiredStatistics().medRunTime&&(r.runTimeHistory.push(t.runTime??0),r.medRunTime=(e=>{if(Array.isArray(e)&&1===e.length)return e[0];const t=e.slice().sort(((e,t)=>e-t)),r=Math.floor(t.length/2);return t.length%2==0?t[r/2]:(t[r-1]+t[r])/2})(r.runTimeHistory)))}chooseWorkerNode(){let t;if(this.type===e.DYNAMIC&&!this.full&&this.internalBusy()){const e=this.createAndSetupWorker();this.registerWorkerMessageListener(e,(t=>{var r;r=c.HARD,(t.kill===r||null!=t.kill&&0===this.getWorkerTasksUsage(e)?.running)&&(this.flushTasksQueueByWorker(e),this.destroyWorker(e))})),t=this.getWorkerNodeKey(e)}else t=this.workerChoiceStrategyContext.execute();return[t,this.workerNodes[t]]}createAndSetupWorker(){const e=this.createWorker();return e.on("message",this.opts.messageHandler??u),e.on("error",this.opts.errorHandler??u),e.on("online",this.opts.onlineHandler??u),e.on("exit",this.opts.exitHandler??u),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):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(){!0===this.opts.enableEvents&&(this.busy&&this.emitter?.emit(a.busy),this.type===e.DYNAMIC&&this.full&&this.emitter?.emit(a.full))}setWorkerNodeTasksUsage(e,t){e.tasksUsage=t}getWorkerTasksUsage(e){const t=this.getWorkerNodeKey(e);if(-1!==t)return this.workerNodes[t].tasksUsage;throw new Error("Worker could not be found in the pool worker nodes")}pushWorkerNode(e){return this.workerNodes.push({worker:e,tasksUsage:{run:0,running:0,runTime:0,runTimeHistory:new W,avgRunTime:0,medRunTime:0,error:0},tasksQueue:[]})}setWorkerNode(e,t,r,s){this.workerNodes[e]={worker:t,tasksUsage:r,tasksQueue:s}}removeWorkerNode(e){const t=this.getWorkerNodeKey(e);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.push(t)}dequeueTask(e){return this.workerNodes[e].tasksQueue.shift()}tasksQueueSize(e){return this.workerNodes[e].tasksQueue.length}flushTasksQueue(e){if(this.tasksQueueSize(e)>0)for(const t of this.workerNodes[e].tasksQueue)this.executeTask(e,t)}flushTasksQueueByWorker(e){const t=this.getWorkerNodeKey(e);this.flushTasksQueue(t)}flushTasksQueues(){for(const[e]of this.workerNodes.entries())this.flushTasksQueue(e)}}class N extends y{opts;constructor(e,t,r={}){super(e,t,r),this.opts=r}setupHook(){r.setupPrimary({...this.opts.settings,exec:this.filePath})}isMain(){return r.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 r.fork(this.opts.env)}afterWorkerSetup(e){this.registerWorkerMessageListener(e,super.workerListener())}get type(){return e.FIXED}get full(){return this.workerNodes.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}class S extends y{constructor(e,t,r={}){super(e,t,r)}isMain(){return o.isMainThread}async destroyWorker(e){this.sendToWorker(e,{kill:1}),await e.terminate()}sendToWorker(e,t){e.postMessage(t)}registerWorkerMessageListener(e,t){e.port2?.on("message",t)}createWorker(){return new o.Worker(this.filePath,{env:o.SHARE_ENV})}afterWorkerSetup(e){const{port1:t,port2:r}=new o.MessageChannel;e.postMessage({parent:t},[t]),e.port1=t,e.port2=r,this.registerWorkerMessageListener(e,super.workerListener())}get type(){return e.FIXED}get full(){return this.workerNodes.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}const R=6e4,x=c.SOFT;class I extends n.AsyncResource{isMain;mainWorker;opts;lastTaskTimestamp;aliveInterval;constructor(e,t,r,s,i={killBehavior:x,maxInactiveTime:R}){super(e),this.isMain=t,this.mainWorker=s,this.opts=i,this.checkWorkerOptions(this.opts),this.checkFunctionInput(r),this.isMain||(this.lastTaskTimestamp=performance.now(),this.aliveInterval=setInterval(this.checkAlive.bind(this),(this.opts.maxInactiveTime??R)/2),this.checkAlive.bind(this)()),this.mainWorker?.on("message",(e=>{this.messageListener(e,r)}))}messageListener(e,t){null!=e.id&&null!=e.data?!0===this.opts.async?this.runInAsyncScope(this.runAsync.bind(this),this,t,e):this.runInAsyncScope(this.run.bind(this),this,t,e):null!=e.parent?this.mainWorker=e.parent:null!=e.kill&&(null!=this.aliveInterval&&clearInterval(this.aliveInterval),this.emitDestroy())}checkWorkerOptions(e){this.opts.killBehavior=e.killBehavior??x,this.opts.maxInactiveTime=e.maxInactiveTime??R,this.opts.async=e.async??!1}checkFunctionInput(e){if(null==e)throw new Error("fn parameter is mandatory");if("function"!=typeof e)throw new TypeError("fn parameter is not a function");if("AsyncFunction"===e.constructor.name&&!1===this.opts.async)throw new Error("fn parameter is an async function, please set the async option to true")}getMainWorker(){if(null==this.mainWorker)throw new Error("Main worker was not set");return this.mainWorker}checkAlive(){performance.now()-this.lastTaskTimestamp>(this.opts.maxInactiveTime??R)&&this.sendToMainWorker({kill:this.opts.killBehavior})}handleError(e){return e}run(e,t){try{const r=performance.now(),s=e(t.data),i=performance.now()-r;this.sendToMainWorker({data:s,id:t.id,runTime:i})}catch(e){const r=this.handleError(e);this.sendToMainWorker({error:r,id:t.id})}finally{!this.isMain&&(this.lastTaskTimestamp=performance.now())}}runAsync(e,t){const r=performance.now();e(t.data).then((e=>{const s=performance.now()-r;return this.sendToMainWorker({data:e,id:t.id,runTime:s}),null})).catch((e=>{const r=this.handleError(e);this.sendToMainWorker({error:r,id:t.id})})).finally((()=>{!this.isMain&&(this.lastTaskTimestamp=performance.now())})).catch(u)}}exports.ClusterWorker=class extends I{constructor(e,t={}){super("worker-cluster-pool:poolifier",r.isPrimary,e,r.worker,t)}sendToMainWorker(e){this.getMainWorker().send(e)}handleError(e){return e instanceof Error?e.message:e}},exports.DynamicClusterPool=class extends N{max;constructor(e,t,r,s={}){super(e,r,s),this.max=t}get type(){return e.DYNAMIC}get full(){return this.workerNodes.length===this.max}get busy(){return this.full&&this.internalBusy()}},exports.DynamicThreadPool=class extends S{max;constructor(e,t,r,s={}){super(e,r,s),this.max=t}get type(){return e.DYNAMIC}get full(){return this.workerNodes.length===this.max}get busy(){return this.full&&this.internalBusy()}},exports.FixedClusterPool=N,exports.FixedThreadPool=S,exports.KillBehaviors=c,exports.PoolEvents=a,exports.ThreadWorker=class extends I{constructor(e,t={}){super("worker-thread-pool:poolifier",o.isMainThread,e,o.parentPort,t)}sendToMainWorker(e){this.getMainWorker().postMessage(e)}},exports.WorkerChoiceStrategies=l;
|
package/lib/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"node:events";import r from"node:cluster";import t 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";var k;!function(e){e.FIXED="fixed",e.DYNAMIC="dynamic"}(k||(k={}));class c extends e{}const l=Object.freeze({full:"full",busy:"busy"}),d=Object.freeze((()=>{})),p={medRunTime:!1},m=Object.freeze({SOFT:"SOFT",HARD:"HARD"});const g=Object.freeze({ROUND_ROBIN:"ROUND_ROBIN",LESS_USED:"LESS_USED",LESS_BUSY:"LESS_BUSY",FAIR_SHARE:"FAIR_SHARE",WEIGHTED_ROUND_ROBIN:"WEIGHTED_ROUND_ROBIN"});class T{pool;opts;isDynamicPool;requiredStatistics={runTime:!1,avgRunTime:!1,medRunTime:!1};constructor(e,r=p){this.pool=e,this.opts=r,this.checkOptions(this.opts),this.isDynamicPool=this.pool.type===k.DYNAMIC,this.choose.bind(this)}checkOptions(e){this.requiredStatistics.avgRunTime&&!0===e.medRunTime&&(this.requiredStatistics.medRunTime=!0)}}class w extends T{requiredStatistics={runTime:!0,avgRunTime:!0,medRunTime:!1};workerLastVirtualTaskTimestamp=new Map;reset(){return this.workerLastVirtualTaskTimestamp.clear(),!0}choose(){let e,r=1/0;for(const[t]of this.pool.workerNodes.entries()){this.computeWorkerLastVirtualTaskTimestamp(t);const s=this.workerLastVirtualTaskTimestamp.get(t)?.end??0;s<r&&(r=s,e=t)}return e}remove(e){const r=this.workerLastVirtualTaskTimestamp.delete(e);for(const[r,t]of this.workerLastVirtualTaskTimestamp.entries())r>e&&this.workerLastVirtualTaskTimestamp.set(r-1,t);return r}computeWorkerLastVirtualTaskTimestamp(e){const r=Math.max(performance.now(),this.workerLastVirtualTaskTimestamp.get(e)?.end??-1/0),t=this.requiredStatistics.medRunTime?this.pool.workerNodes[e].tasksUsage.medRunTime:this.pool.workerNodes[e].tasksUsage.avgRunTime;this.workerLastVirtualTaskTimestamp.set(e,{start:r,end:r+(t??0)})}}class f extends T{requiredStatistics={runTime:!0,avgRunTime:!1,medRunTime:!1};reset(){return!0}choose(){const e=this.pool.findFreeWorkerNodeKey();if(-1!==e)return e;let r,t=1/0;for(const[e,s]of this.pool.workerNodes.entries()){const i=s.tasksUsage.runTime;if(0===i)return e;i<t&&(t=i,r=e)}return r}remove(e){return!0}}class W extends T{reset(){return!0}choose(){const e=this.pool.findFreeWorkerNodeKey();if(-1!==e)return e;let r,t=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<t&&(t=o,r=e)}return r}remove(e){return!0}}class y extends T{nextWorkerNodeId=0;reset(){return this.nextWorkerNodeId=0,!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.nextWorkerNodeId>this.pool.workerNodes.length-1?this.pool.workerNodes.length-1:this.nextWorkerNodeId),!0}}class N extends T{requiredStatistics={runTime:!0,avgRunTime:!0,medRunTime:!1};currentWorkerNodeId=0;defaultWorkerWeight;workersTaskRunTime=new Map;constructor(e,r){super(e,r),this.defaultWorkerWeight=this.computeWorkerWeight(),this.initWorkersTaskRunTime()}reset(){return this.currentWorkerNodeId=0,this.workersTaskRunTime.clear(),this.initWorkersTaskRunTime(),!0}choose(){const e=this.currentWorkerNodeId;this.isDynamicPool&&!this.workersTaskRunTime.has(e)&&this.initWorkerTaskRunTime(e);const r=this.workersTaskRunTime.get(e)?.runTime??0,t=this.workersTaskRunTime.get(e)?.weight??this.defaultWorkerWeight;return r<t?this.setWorkerTaskRunTime(e,t,r+(this.getWorkerVirtualTaskRunTime(e)??0)):(this.currentWorkerNodeId=this.currentWorkerNodeId===this.pool.workerNodes.length-1?0:this.currentWorkerNodeId+1,this.setWorkerTaskRunTime(this.currentWorkerNodeId,t,0)),e}remove(e){this.currentWorkerNodeId===e&&(0===this.pool.workerNodes.length?this.currentWorkerNodeId=0:this.currentWorkerNodeId=this.currentWorkerNodeId>this.pool.workerNodes.length-1?this.pool.workerNodes.length-1:this.currentWorkerNodeId);const r=this.workersTaskRunTime.delete(e);for(const[r,t]of this.workersTaskRunTime)r>e&&this.workersTaskRunTime.set(r-1,t);return r}initWorkersTaskRunTime(){for(const[e]of this.pool.workerNodes.entries())this.initWorkerTaskRunTime(e)}initWorkerTaskRunTime(e){this.setWorkerTaskRunTime(e,this.defaultWorkerWeight,0)}setWorkerTaskRunTime(e,r,t){this.workersTaskRunTime.set(e,{weight:r,runTime:t})}getWorkerVirtualTaskRunTime(e){return this.requiredStatistics.medRunTime?this.pool.workerNodes[e].tasksUsage.medRunTime:this.pool.workerNodes[e].tasksUsage.avgRunTime}computeWorkerWeight(){let e=0;for(const r of s()){const t=r.speed.toString().length-1;e+=1/(r.speed/Math.pow(10,t))*Math.pow(10,t)}return Math.round(e/s().length)}}class S{workerChoiceStrategyType;workerChoiceStrategies;constructor(e,r=g.ROUND_ROBIN,t=p){this.workerChoiceStrategyType=r,this.execute.bind(this),this.workerChoiceStrategies=new Map([[g.ROUND_ROBIN,new y(e,t)],[g.LESS_USED,new W(e,t)],[g.LESS_BUSY,new f(e,t)],[g.FAIR_SHARE,new w(e,t)],[g.WEIGHTED_ROUND_ROBIN,new N(e,t)]])}getRequiredStatistics(){return this.workerChoiceStrategies.get(this.workerChoiceStrategyType).requiredStatistics}setWorkerChoiceStrategy(e){this.workerChoiceStrategyType!==e&&(this.workerChoiceStrategyType=e),this.workerChoiceStrategies.get(this.workerChoiceStrategyType)?.reset()}execute(){return this.workerChoiceStrategies.get(this.workerChoiceStrategyType).choose()}remove(e){return this.workerChoiceStrategies.get(this.workerChoiceStrategyType).remove(e)}}class R extends Array{size;constructor(e=1024,...r){super(),this.checkSize(e),this.size=e,arguments.length>1&&this.push(...r)}push(...e){const r=super.push(...e);return r>this.size&&super.splice(0,r-this.size),this.length}unshift(...e){return super.unshift(...e)>this.size&&super.splice(this.size,e.length),this.length}concat(...e){const r=super.concat(e);return r.size=this.size,r.length>r.size&&r.splice(0,r.length-r.size),r}splice(e,r,...t){let s;return arguments.length>=3&&void 0!==r?(s=super.splice(e,r),this.push(...t)):s=2===arguments.length?super.splice(e,r):super.splice(e),s}resize(e){if(this.checkSize(e),0===e)this.length=0;else if(e<this.size)for(let r=e;r<this.size;r++)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 I{numberOfWorkers;filePath;opts;workerNodes=[];emitter;promiseResponseMap=new Map;workerChoiceStrategyContext;constructor(e,r,t){if(this.numberOfWorkers=e,this.filePath=r,this.opts=t,!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.bind(this),this.executeTask.bind(this),this.enqueueTask.bind(this),this.checkAndEmitEvents.bind(this),this.setupHook();for(let e=1;e<=this.numberOfWorkers;e++)this.createAndSetupWorker();!0===this.opts.enableEvents&&(this.emitter=new c),this.workerChoiceStrategyContext=new S(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 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(this.opts.workerChoiceStrategy=e.workerChoiceStrategy??g.ROUND_ROBIN,this.checkValidWorkerChoiceStrategy(this.opts.workerChoiceStrategy),this.opts.workerChoiceStrategyOptions=e.workerChoiceStrategyOptions??p,this.opts.enableEvents=e.enableEvents??!0,this.opts.enableTasksQueue=e.enableTasksQueue??!1,this.opts.enableTasksQueue){if(e.tasksQueueOptions?.concurrency<=0)throw new Error(`Invalid worker tasks concurrency '${e.tasksQueueOptions.concurrency}'`);this.opts.tasksQueueOptions={concurrency:e.tasksQueueOptions?.concurrency??1}}}checkValidWorkerChoiceStrategy(e){if(!Object.values(g).includes(e))throw new Error(`Invalid worker choice strategy '${e}'`)}get numberOfRunningTasks(){return this.workerNodes.reduce(((e,r)=>e+r.tasksUsage.running),0)}get numberOfQueuedTasks(){return!1===this.opts.enableTasksQueue?0:this.workerNodes.reduce(((e,r)=>e+r.tasksQueue.length),0)}getWorkerNodeKey(e){return this.workerNodes.findIndex((r=>r.worker===e))}setWorkerChoiceStrategy(e){this.checkValidWorkerChoiceStrategy(e),this.opts.workerChoiceStrategy=e;for(const e of this.workerNodes)this.setWorkerNodeTasksUsage(e,{run:0,running:0,runTime:0,runTimeHistory:new R,avgRunTime:0,medRunTime:0,error:0});this.workerChoiceStrategyContext.setWorkerChoiceStrategy(e)}internalBusy(){return-1===this.findFreeWorkerNodeKey()}findFreeWorkerNodeKey(){return this.workerNodes.findIndex((e=>0===e.tasksUsage?.running))}async execute(e){const[r,s]=this.chooseWorkerNode(),i={data:e??{},id:t.randomUUID()},o=new Promise(((e,r)=>{this.promiseResponseMap.set(i.id,{resolve:e,reject:r,worker:s.worker})}));return!0===this.opts.enableTasksQueue&&(this.busy||this.workerNodes[r].tasksUsage.running>=this.opts.tasksQueueOptions.concurrency)?this.enqueueTask(r,i):this.executeTask(r,i),this.checkAndEmitEvents(),o}async destroy(){await Promise.all(this.workerNodes.map((async(e,r)=>{this.flushTasksQueue(r),await this.destroyWorker(e.worker)})))}setupHook(){}beforeTaskExecutionHook(e){++this.workerNodes[e].tasksUsage.running}afterTaskExecutionHook(e,r){const t=this.getWorkerTasksUsage(e);--t.running,++t.run,null!=r.error&&++t.error,this.workerChoiceStrategyContext.getRequiredStatistics().runTime&&(t.runTime+=r.runTime??0,this.workerChoiceStrategyContext.getRequiredStatistics().avgRunTime&&0!==t.run&&(t.avgRunTime=t.runTime/t.run),this.workerChoiceStrategyContext.getRequiredStatistics().medRunTime&&(t.runTimeHistory.push(r.runTime??0),t.medRunTime=(e=>{if(Array.isArray(e)&&1===e.length)return e[0];const r=e.slice().sort(((e,r)=>e-r)),t=Math.floor(r.length/2);return r.length%2==0?r[t/2]:(r[t-1]+r[t])/2})(t.runTimeHistory)))}chooseWorkerNode(){let e;if(this.type===k.DYNAMIC&&!this.full&&this.internalBusy()){const r=this.createAndSetupWorker();this.registerWorkerMessageListener(r,(e=>{var t;t=m.HARD,(e.kill===t||null!=e.kill&&0===this.getWorkerTasksUsage(r)?.running)&&(this.flushTasksQueueByWorker(r),this.destroyWorker(r))})),e=this.getWorkerNodeKey(r)}else e=this.workerChoiceStrategyContext.execute();return[e,this.workerNodes[e]]}createAndSetupWorker(){const e=this.createWorker();return e.on("message",this.opts.messageHandler??d),e.on("error",this.opts.errorHandler??d),e.on("online",this.opts.onlineHandler??d),e.on("exit",this.opts.exitHandler??d),e.once("exit",(()=>{this.removeWorkerNode(e)})),this.pushWorkerNode(e),this.afterWorkerSetup(e),e}workerListener(){return e=>{if(null!=e.id){const r=this.promiseResponseMap.get(e.id);if(null!=r){null!=e.error?r.reject(e.error):r.resolve(e.data),this.afterTaskExecutionHook(r.worker,e),this.promiseResponseMap.delete(e.id);const t=this.getWorkerNodeKey(r.worker);!0===this.opts.enableTasksQueue&&this.tasksQueueSize(t)>0&&this.executeTask(t,this.dequeueTask(t))}}}}checkAndEmitEvents(){!0===this.opts.enableEvents&&(this.busy&&this.emitter?.emit(l.busy),this.type===k.DYNAMIC&&this.full&&this.emitter?.emit(l.full))}setWorkerNodeTasksUsage(e,r){e.tasksUsage=r}getWorkerTasksUsage(e){const r=this.getWorkerNodeKey(e);if(-1!==r)return this.workerNodes[r].tasksUsage;throw new Error("Worker could not be found in the pool worker nodes")}pushWorkerNode(e){return this.workerNodes.push({worker:e,tasksUsage:{run:0,running:0,runTime:0,runTimeHistory:new R,avgRunTime:0,medRunTime:0,error:0},tasksQueue:[]})}setWorkerNode(e,r,t,s){this.workerNodes[e]={worker:r,tasksUsage:t,tasksQueue:s}}removeWorkerNode(e){const r=this.getWorkerNodeKey(e);this.workerNodes.splice(r,1),this.workerChoiceStrategyContext.remove(r)}executeTask(e,r){this.beforeTaskExecutionHook(e),this.sendToWorker(this.workerNodes[e].worker,r)}enqueueTask(e,r){return this.workerNodes[e].tasksQueue.push(r)}dequeueTask(e){return this.workerNodes[e].tasksQueue.shift()}tasksQueueSize(e){return this.workerNodes[e].tasksQueue.length}flushTasksQueue(e){if(this.tasksQueueSize(e)>0)for(const r of this.workerNodes[e].tasksQueue)this.executeTask(e,r)}flushTasksQueueByWorker(e){const r=this.getWorkerNodeKey(e);this.flushTasksQueue(r)}}class x extends I{opts;constructor(e,r,t={}){super(e,r,t),this.opts=t}setupHook(){r.setupPrimary({...this.opts.settings,exec:this.filePath})}isMain(){return r.isPrimary}destroyWorker(e){this.sendToWorker(e,{kill:1}),e.kill()}sendToWorker(e,r){e.send(r)}registerWorkerMessageListener(e,r){e.on("message",r)}createWorker(){return r.fork(this.opts.env)}afterWorkerSetup(e){this.registerWorkerMessageListener(e,super.workerListener())}get type(){return k.FIXED}get full(){return this.workerNodes.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}class v extends x{max;constructor(e,r,t,s={}){super(e,t,s),this.max=r}get type(){return k.DYNAMIC}get full(){return this.workerNodes.length===this.max}get busy(){return this.full&&this.internalBusy()}}class C extends I{constructor(e,r,t={}){super(e,r,t)}isMain(){return i}async destroyWorker(e){this.sendToWorker(e,{kill:1}),await e.terminate()}sendToWorker(e,r){e.postMessage(r)}registerWorkerMessageListener(e,r){e.port2?.on("message",r)}createWorker(){return new o(this.filePath,{env:n})}afterWorkerSetup(e){const{port1:r,port2:t}=new a;e.postMessage({parent:r},[r]),e.port1=r,e.port2=t,this.registerWorkerMessageListener(e,super.workerListener())}get type(){return k.FIXED}get full(){return this.workerNodes.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}class E extends C{max;constructor(e,r,t,s={}){super(e,t,s),this.max=r}get type(){return k.DYNAMIC}get full(){return this.workerNodes.length===this.max}get busy(){return this.full&&this.internalBusy()}}const b=6e4,O=m.SOFT;class M extends u{isMain;mainWorker;opts;lastTaskTimestamp;aliveInterval;constructor(e,r,t,s,i={killBehavior:O,maxInactiveTime:b}){super(e),this.isMain=r,this.mainWorker=s,this.opts=i,this.checkFunctionInput(t),this.checkWorkerOptions(this.opts),this.isMain||(this.lastTaskTimestamp=performance.now(),this.aliveInterval=setInterval(this.checkAlive.bind(this),(this.opts.maxInactiveTime??b)/2),this.checkAlive.bind(this)()),this.mainWorker?.on("message",(e=>{this.messageListener(e,t)}))}messageListener(e,r){null!=e.data&&null!=e.id?!0===this.opts.async?this.runInAsyncScope(this.runAsync.bind(this),this,r,e):this.runInAsyncScope(this.run.bind(this),this,r,e):null!=e.parent?this.mainWorker=e.parent:null!=e.kill&&(null!=this.aliveInterval&&clearInterval(this.aliveInterval),this.emitDestroy())}checkWorkerOptions(e){this.opts.killBehavior=e.killBehavior??O,this.opts.maxInactiveTime=e.maxInactiveTime??b,this.opts.async=e.async??!1}checkFunctionInput(e){if(null==e)throw new Error("fn parameter is mandatory");if("function"!=typeof e)throw new TypeError("fn parameter is not a function")}getMainWorker(){if(null==this.mainWorker)throw new Error("Main worker was not set");return this.mainWorker}checkAlive(){performance.now()-this.lastTaskTimestamp>(this.opts.maxInactiveTime??b)&&this.sendToMainWorker({kill:this.opts.killBehavior})}handleError(e){return e}run(e,r){try{const t=performance.now(),s=e(r.data),i=performance.now()-t;this.sendToMainWorker({data:s,id:r.id,runTime:i})}catch(e){const t=this.handleError(e);this.sendToMainWorker({error:t,id:r.id})}finally{!this.isMain&&(this.lastTaskTimestamp=performance.now())}}runAsync(e,r){const t=performance.now();e(r.data).then((e=>{const s=performance.now()-t;return this.sendToMainWorker({data:e,id:r.id,runTime:s}),null})).catch((e=>{const t=this.handleError(e);this.sendToMainWorker({error:t,id:r.id})})).finally((()=>{!this.isMain&&(this.lastTaskTimestamp=performance.now())})).catch(d)}}class U extends M{constructor(e,t={}){super("worker-cluster-pool:poolifier",r.isPrimary,e,r.worker,t)}sendToMainWorker(e){this.getMainWorker().send(e)}handleError(e){return e instanceof Error?e.message:e}}class D extends M{constructor(e,r={}){super("worker-thread-pool:poolifier",i,e,h,r)}sendToMainWorker(e){this.getMainWorker().postMessage(e)}}export{U as ClusterWorker,v as DynamicClusterPool,E as DynamicThreadPool,x as FixedClusterPool,C as FixedThreadPool,m as KillBehaviors,l as PoolEvents,D as ThreadWorker,g as WorkerChoiceStrategies};
|
|
1
|
+
import e from"node:events";import t from"node:cluster";import s from"node:crypto";import{cpus as r}from"node:os";import{isMainThread as i,Worker as o,SHARE_ENV as n,MessageChannel as h,parentPort as a}from"node:worker_threads";import{AsyncResource as u}from"node:async_hooks";var k;!function(e){e.FIXED="fixed",e.DYNAMIC="dynamic"}(k||(k={}));class c extends e{}const l=Object.freeze({full:"full",busy:"busy"}),d=Object.freeze((()=>{})),p={medRunTime:!1},m=Object.freeze({SOFT:"SOFT",HARD:"HARD"});const T=Object.freeze({ROUND_ROBIN:"ROUND_ROBIN",LESS_USED:"LESS_USED",LESS_BUSY:"LESS_BUSY",FAIR_SHARE:"FAIR_SHARE",WEIGHTED_ROUND_ROBIN:"WEIGHTED_ROUND_ROBIN"});class g{pool;opts;isDynamicPool;requiredStatistics={runTime:!1,avgRunTime:!1,medRunTime:!1};constructor(e,t=p){this.pool=e,this.opts=t,this.isDynamicPool=this.pool.type===k.DYNAMIC,this.choose=this.choose.bind(this)}checkOptions(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)}setOptions(e){e=e??p,this.checkOptions(e),this.opts=e}}class w extends g{requiredStatistics={runTime:!0,avgRunTime:!0,medRunTime:!1};workerLastVirtualTaskTimestamp=new Map;constructor(e,t=p){super(e,t),this.checkOptions(this.opts)}reset(){return this.workerLastVirtualTaskTimestamp.clear(),!0}choose(){let e,t=1/0;for(const[s]of this.pool.workerNodes.entries()){this.computeWorkerLastVirtualTaskTimestamp(s);const r=this.workerLastVirtualTaskTimestamp.get(s)?.end??0;r<t&&(t=r,e=s)}return e}remove(e){const t=this.workerLastVirtualTaskTimestamp.delete(e);for(const[t,s]of this.workerLastVirtualTaskTimestamp.entries())t>e&&this.workerLastVirtualTaskTimestamp.set(t-1,s);return t}computeWorkerLastVirtualTaskTimestamp(e){const t=Math.max(performance.now(),this.workerLastVirtualTaskTimestamp.get(e)?.end??-1/0),s=this.requiredStatistics.medRunTime?this.pool.workerNodes[e].tasksUsage.medRunTime:this.pool.workerNodes[e].tasksUsage.avgRunTime;this.workerLastVirtualTaskTimestamp.set(e,{start:t,end:t+(s??0)})}}class f extends g{requiredStatistics={runTime:!0,avgRunTime:!1,medRunTime:!1};constructor(e,t=p){super(e,t),this.checkOptions(this.opts)}reset(){return!0}choose(){const e=this.pool.findFreeWorkerNodeKey();if(-1!==e)return e;let t,s=1/0;for(const[e,r]of this.pool.workerNodes.entries()){const i=r.tasksUsage.runTime;if(0===i)return e;i<s&&(s=i,t=e)}return t}remove(e){return!0}}class W extends g{constructor(e,t=p){super(e,t),this.checkOptions(this.opts)}reset(){return!0}choose(){const e=this.pool.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.run+i.running;if(0===o)return e;o<s&&(s=o,t=e)}return t}remove(e){return!0}}class y extends g{nextWorkerNodeId=0;constructor(e,t=p){super(e,t),this.checkOptions(this.opts)}reset(){return this.nextWorkerNodeId=0,!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.nextWorkerNodeId>this.pool.workerNodes.length-1?this.pool.workerNodes.length-1:this.nextWorkerNodeId),!0}}class N extends g{requiredStatistics={runTime:!0,avgRunTime:!0,medRunTime:!1};currentWorkerNodeId=0;defaultWorkerWeight;workersTaskRunTime=new Map;constructor(e,t=p){super(e,t),this.checkOptions(this.opts),this.defaultWorkerWeight=this.computeWorkerWeight(),this.initWorkersTaskRunTime()}reset(){return this.currentWorkerNodeId=0,this.workersTaskRunTime.clear(),this.initWorkersTaskRunTime(),!0}choose(){const e=this.currentWorkerNodeId;this.isDynamicPool&&!this.workersTaskRunTime.has(e)&&this.initWorkerTaskRunTime(e);const t=this.workersTaskRunTime.get(e)?.runTime??0,s=this.workersTaskRunTime.get(e)?.weight??this.defaultWorkerWeight;return t<s?this.setWorkerTaskRunTime(e,s,t+(this.getWorkerVirtualTaskRunTime(e)??0)):(this.currentWorkerNodeId=this.currentWorkerNodeId===this.pool.workerNodes.length-1?0:this.currentWorkerNodeId+1,this.setWorkerTaskRunTime(this.currentWorkerNodeId,s,0)),e}remove(e){this.currentWorkerNodeId===e&&(0===this.pool.workerNodes.length?this.currentWorkerNodeId=0:this.currentWorkerNodeId=this.currentWorkerNodeId>this.pool.workerNodes.length-1?this.pool.workerNodes.length-1:this.currentWorkerNodeId);const t=this.workersTaskRunTime.delete(e);for(const[t,s]of this.workersTaskRunTime)t>e&&this.workersTaskRunTime.set(t-1,s);return t}initWorkersTaskRunTime(){for(const[e]of this.pool.workerNodes.entries())this.initWorkerTaskRunTime(e)}initWorkerTaskRunTime(e){this.setWorkerTaskRunTime(e,this.defaultWorkerWeight,0)}setWorkerTaskRunTime(e,t,s){this.workersTaskRunTime.set(e,{weight:t,runTime:s})}getWorkerVirtualTaskRunTime(e){return this.requiredStatistics.medRunTime?this.pool.workerNodes[e].tasksUsage.medRunTime:this.pool.workerNodes[e].tasksUsage.avgRunTime}computeWorkerWeight(){let e=0;for(const t of r()){const s=t.speed.toString().length-1;e+=1/(t.speed/Math.pow(10,s))*Math.pow(10,s)}return Math.round(e/r().length)}}class S{workerChoiceStrategy;workerChoiceStrategies;constructor(e,t=T.ROUND_ROBIN,s=p){this.workerChoiceStrategy=t,this.execute=this.execute.bind(this),this.workerChoiceStrategies=new Map([[T.ROUND_ROBIN,new(y.bind(this))(e,s)],[T.LESS_USED,new(W.bind(this))(e,s)],[T.LESS_BUSY,new(f.bind(this))(e,s)],[T.FAIR_SHARE,new(w.bind(this))(e,s)],[T.WEIGHTED_ROUND_ROBIN,new(N.bind(this))(e,s)]])}getRequiredStatistics(){return this.workerChoiceStrategies.get(this.workerChoiceStrategy).requiredStatistics}setWorkerChoiceStrategy(e){this.workerChoiceStrategy!==e&&(this.workerChoiceStrategy=e),this.workerChoiceStrategies.get(this.workerChoiceStrategy)?.reset()}execute(){return this.workerChoiceStrategies.get(this.workerChoiceStrategy).choose()}remove(e){return this.workerChoiceStrategies.get(this.workerChoiceStrategy).remove(e)}setOptions(e){this.workerChoiceStrategies.forEach((t=>{t.setOptions(e)}))}}class R 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 I{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),this.setupHook();for(let e=1;e<=this.numberOfWorkers;e++)this.createAndSetupWorker();!0===this.opts.enableEvents&&(this.emitter=new c),this.workerChoiceStrategyContext=new S(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 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){this.opts.workerChoiceStrategy=e.workerChoiceStrategy??T.ROUND_ROBIN,this.checkValidWorkerChoiceStrategy(this.opts.workerChoiceStrategy),this.opts.workerChoiceStrategyOptions=e.workerChoiceStrategyOptions??p,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}'`)}checkValidTasksQueueOptions(e){if(e?.concurrency<=0)throw new Error(`Invalid worker tasks concurrency '${e.concurrency}'`)}get numberOfRunningTasks(){return this.workerNodes.reduce(((e,t)=>e+t.tasksUsage.running),0)}get numberOfQueuedTasks(){return!1===this.opts.enableTasksQueue?0:this.workerNodes.reduce(((e,t)=>e+t.tasksQueue.length),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 R,avgRunTime:0,medRunTime:0,error:0});this.workerChoiceStrategyContext.setWorkerChoiceStrategy(this.opts.workerChoiceStrategy),null!=t&&this.setWorkerChoiceStrategyOptions(t)}setWorkerChoiceStrategyOptions(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}}internalBusy(){return-1===this.findFreeWorkerNodeKey()}findFreeWorkerNodeKey(){return this.workerNodes.findIndex((e=>0===e.tasksUsage?.running))}async execute(e){const[t,r]=this.chooseWorkerNode(),i={data:e??{},id:s.randomUUID()},o=new Promise(((e,t)=>{this.promiseResponseMap.set(i.id,{resolve:e,reject:t,worker:r.worker})}));return!0===this.opts.enableTasksQueue&&(this.busy||this.workerNodes[t].tasksUsage.running>=this.opts.tasksQueueOptions.concurrency)?this.enqueueTask(t,i):this.executeTask(t,i),this.checkAndEmitEvents(),o}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.getWorkerTasksUsage(e);--s.running,++s.run,null!=t.error&&++s.error,this.workerChoiceStrategyContext.getRequiredStatistics().runTime&&(s.runTime+=t.runTime??0,this.workerChoiceStrategyContext.getRequiredStatistics().avgRunTime&&0!==s.run&&(s.avgRunTime=s.runTime/s.run),this.workerChoiceStrategyContext.getRequiredStatistics().medRunTime&&(s.runTimeHistory.push(t.runTime??0),s.medRunTime=(e=>{if(Array.isArray(e)&&1===e.length)return e[0];const t=e.slice().sort(((e,t)=>e-t)),s=Math.floor(t.length/2);return t.length%2==0?t[s/2]:(t[s-1]+t[s])/2})(s.runTimeHistory)))}chooseWorkerNode(){let e;if(this.type===k.DYNAMIC&&!this.full&&this.internalBusy()){const t=this.createAndSetupWorker();this.registerWorkerMessageListener(t,(e=>{var s;s=m.HARD,(e.kill===s||null!=e.kill&&0===this.getWorkerTasksUsage(t)?.running)&&(this.flushTasksQueueByWorker(t),this.destroyWorker(t))})),e=this.getWorkerNodeKey(t)}else e=this.workerChoiceStrategyContext.execute();return[e,this.workerNodes[e]]}createAndSetupWorker(){const e=this.createWorker();return e.on("message",this.opts.messageHandler??d),e.on("error",this.opts.errorHandler??d),e.on("online",this.opts.onlineHandler??d),e.on("exit",this.opts.exitHandler??d),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):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(){!0===this.opts.enableEvents&&(this.busy&&this.emitter?.emit(l.busy),this.type===k.DYNAMIC&&this.full&&this.emitter?.emit(l.full))}setWorkerNodeTasksUsage(e,t){e.tasksUsage=t}getWorkerTasksUsage(e){const t=this.getWorkerNodeKey(e);if(-1!==t)return this.workerNodes[t].tasksUsage;throw new Error("Worker could not be found in the pool worker nodes")}pushWorkerNode(e){return this.workerNodes.push({worker:e,tasksUsage:{run:0,running:0,runTime:0,runTimeHistory:new R,avgRunTime:0,medRunTime:0,error:0},tasksQueue:[]})}setWorkerNode(e,t,s,r){this.workerNodes[e]={worker:t,tasksUsage:s,tasksQueue:r}}removeWorkerNode(e){const t=this.getWorkerNodeKey(e);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.push(t)}dequeueTask(e){return this.workerNodes[e].tasksQueue.shift()}tasksQueueSize(e){return this.workerNodes[e].tasksQueue.length}flushTasksQueue(e){if(this.tasksQueueSize(e)>0)for(const t of this.workerNodes[e].tasksQueue)this.executeTask(e,t)}flushTasksQueueByWorker(e){const t=this.getWorkerNodeKey(e);this.flushTasksQueue(t)}flushTasksQueues(){for(const[e]of this.workerNodes.entries())this.flushTasksQueue(e)}}class x extends I{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 k.FIXED}get full(){return this.workerNodes.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}class O extends x{max;constructor(e,t,s,r={}){super(e,s,r),this.max=t}get type(){return k.DYNAMIC}get full(){return this.workerNodes.length===this.max}get busy(){return this.full&&this.internalBusy()}}class v extends I{constructor(e,t,s={}){super(e,t,s)}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:s}=new h;e.postMessage({parent:t},[t]),e.port1=t,e.port2=s,this.registerWorkerMessageListener(e,super.workerListener())}get type(){return k.FIXED}get full(){return this.workerNodes.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}class b extends v{max;constructor(e,t,s,r={}){super(e,s,r),this.max=t}get type(){return k.DYNAMIC}get full(){return this.workerNodes.length===this.max}get busy(){return this.full&&this.internalBusy()}}const C=6e4,E=m.SOFT;class M extends u{isMain;mainWorker;opts;lastTaskTimestamp;aliveInterval;constructor(e,t,s,r,i={killBehavior:E,maxInactiveTime:C}){super(e),this.isMain=t,this.mainWorker=r,this.opts=i,this.checkWorkerOptions(this.opts),this.checkFunctionInput(s),this.isMain||(this.lastTaskTimestamp=performance.now(),this.aliveInterval=setInterval(this.checkAlive.bind(this),(this.opts.maxInactiveTime??C)/2),this.checkAlive.bind(this)()),this.mainWorker?.on("message",(e=>{this.messageListener(e,s)}))}messageListener(e,t){null!=e.id&&null!=e.data?!0===this.opts.async?this.runInAsyncScope(this.runAsync.bind(this),this,t,e):this.runInAsyncScope(this.run.bind(this),this,t,e):null!=e.parent?this.mainWorker=e.parent:null!=e.kill&&(null!=this.aliveInterval&&clearInterval(this.aliveInterval),this.emitDestroy())}checkWorkerOptions(e){this.opts.killBehavior=e.killBehavior??E,this.opts.maxInactiveTime=e.maxInactiveTime??C,this.opts.async=e.async??!1}checkFunctionInput(e){if(null==e)throw new Error("fn parameter is mandatory");if("function"!=typeof e)throw new TypeError("fn parameter is not a function");if("AsyncFunction"===e.constructor.name&&!1===this.opts.async)throw new Error("fn parameter is an async function, please set the async option to true")}getMainWorker(){if(null==this.mainWorker)throw new Error("Main worker was not set");return this.mainWorker}checkAlive(){performance.now()-this.lastTaskTimestamp>(this.opts.maxInactiveTime??C)&&this.sendToMainWorker({kill:this.opts.killBehavior})}handleError(e){return e}run(e,t){try{const s=performance.now(),r=e(t.data),i=performance.now()-s;this.sendToMainWorker({data:r,id:t.id,runTime:i})}catch(e){const s=this.handleError(e);this.sendToMainWorker({error:s,id:t.id})}finally{!this.isMain&&(this.lastTaskTimestamp=performance.now())}}runAsync(e,t){const s=performance.now();e(t.data).then((e=>{const r=performance.now()-s;return this.sendToMainWorker({data:e,id:t.id,runTime:r}),null})).catch((e=>{const s=this.handleError(e);this.sendToMainWorker({error:s,id:t.id})})).finally((()=>{!this.isMain&&(this.lastTaskTimestamp=performance.now())})).catch(d)}}class Q extends M{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 U extends M{constructor(e,t={}){super("worker-thread-pool:poolifier",i,e,a,t)}sendToMainWorker(e){this.getMainWorker().postMessage(e)}}export{Q as ClusterWorker,O as DynamicClusterPool,b as DynamicThreadPool,x as FixedClusterPool,v as FixedThreadPool,m as KillBehaviors,l as PoolEvents,U as ThreadWorker,T as WorkerChoiceStrategies};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { MessageValue, PromiseResponseWrapper } from '../utility-types';
|
|
2
|
-
import { type IPool, type PoolOptions, PoolType } from './pool';
|
|
2
|
+
import { type IPool, type PoolOptions, type TasksQueueOptions, PoolType } from './pool';
|
|
3
3
|
import { PoolEmitter } from './pool';
|
|
4
4
|
import type { IWorker, WorkerNode } from './worker';
|
|
5
|
-
import { type WorkerChoiceStrategy } from './selection-strategies/selection-strategies-types';
|
|
5
|
+
import { type WorkerChoiceStrategy, type WorkerChoiceStrategyOptions } from './selection-strategies/selection-strategies-types';
|
|
6
6
|
import { WorkerChoiceStrategyContext } from './selection-strategies/worker-choice-strategy-context';
|
|
7
7
|
/**
|
|
8
8
|
* Base class that implements some shared logic for all poolifier pools.
|
|
@@ -38,7 +38,7 @@ export declare abstract class AbstractPool<Worker extends IWorker, Data = unknow
|
|
|
38
38
|
* Constructs a new poolifier pool.
|
|
39
39
|
*
|
|
40
40
|
* @param numberOfWorkers - Number of workers that this pool should manage.
|
|
41
|
-
* @param filePath - Path to the worker
|
|
41
|
+
* @param filePath - Path to the worker file.
|
|
42
42
|
* @param opts - Options for the pool.
|
|
43
43
|
*/
|
|
44
44
|
constructor(numberOfWorkers: number, filePath: string, opts: PoolOptions<Worker>);
|
|
@@ -46,6 +46,7 @@ export declare abstract class AbstractPool<Worker extends IWorker, Data = unknow
|
|
|
46
46
|
private checkNumberOfWorkers;
|
|
47
47
|
private checkPoolOptions;
|
|
48
48
|
private checkValidWorkerChoiceStrategy;
|
|
49
|
+
private checkValidTasksQueueOptions;
|
|
49
50
|
/** @inheritDoc */
|
|
50
51
|
abstract get type(): PoolType;
|
|
51
52
|
/**
|
|
@@ -64,7 +65,14 @@ export declare abstract class AbstractPool<Worker extends IWorker, Data = unknow
|
|
|
64
65
|
*/
|
|
65
66
|
private getWorkerNodeKey;
|
|
66
67
|
/** @inheritDoc */
|
|
67
|
-
setWorkerChoiceStrategy(workerChoiceStrategy: WorkerChoiceStrategy): void;
|
|
68
|
+
setWorkerChoiceStrategy(workerChoiceStrategy: WorkerChoiceStrategy, workerChoiceStrategyOptions?: WorkerChoiceStrategyOptions): void;
|
|
69
|
+
/** @inheritDoc */
|
|
70
|
+
setWorkerChoiceStrategyOptions(workerChoiceStrategyOptions: WorkerChoiceStrategyOptions): void;
|
|
71
|
+
/** @inheritDoc */
|
|
72
|
+
enableTasksQueue(enable: boolean, tasksQueueOptions?: TasksQueueOptions): void;
|
|
73
|
+
/** @inheritDoc */
|
|
74
|
+
setTasksQueueOptions(tasksQueueOptions: TasksQueueOptions): void;
|
|
75
|
+
private buildTasksQueueOptions;
|
|
68
76
|
/**
|
|
69
77
|
* Whether the pool is full or not.
|
|
70
78
|
*
|
|
@@ -81,7 +89,7 @@ export declare abstract class AbstractPool<Worker extends IWorker, Data = unknow
|
|
|
81
89
|
/** @inheritDoc */
|
|
82
90
|
findFreeWorkerNodeKey(): number;
|
|
83
91
|
/** @inheritDoc */
|
|
84
|
-
execute(data
|
|
92
|
+
execute(data?: Data): Promise<Response>;
|
|
85
93
|
/** @inheritDoc */
|
|
86
94
|
destroy(): Promise<void>;
|
|
87
95
|
/**
|
|
@@ -174,7 +182,7 @@ export declare abstract class AbstractPool<Worker extends IWorker, Data = unknow
|
|
|
174
182
|
* Gets the given worker its tasks usage in the pool.
|
|
175
183
|
*
|
|
176
184
|
* @param worker - The worker.
|
|
177
|
-
* @throws
|
|
185
|
+
* @throws Error if the worker is not found in the pool worker nodes.
|
|
178
186
|
* @returns The worker tasks usage.
|
|
179
187
|
*/
|
|
180
188
|
private getWorkerTasksUsage;
|
|
@@ -206,4 +214,5 @@ export declare abstract class AbstractPool<Worker extends IWorker, Data = unknow
|
|
|
206
214
|
private tasksQueueSize;
|
|
207
215
|
private flushTasksQueue;
|
|
208
216
|
private flushTasksQueueByWorker;
|
|
217
|
+
private flushTasksQueues;
|
|
209
218
|
}
|
package/lib/pools/pool.d.ts
CHANGED
|
@@ -69,6 +69,8 @@ export interface PoolOptions<Worker extends IWorker> {
|
|
|
69
69
|
exitHandler?: ExitHandler<Worker>;
|
|
70
70
|
/**
|
|
71
71
|
* The worker choice strategy to use in this pool.
|
|
72
|
+
*
|
|
73
|
+
* @defaultValue WorkerChoiceStrategies.ROUND_ROBIN
|
|
72
74
|
*/
|
|
73
75
|
workerChoiceStrategy?: WorkerChoiceStrategy;
|
|
74
76
|
/**
|
|
@@ -84,14 +86,11 @@ export interface PoolOptions<Worker extends IWorker> {
|
|
|
84
86
|
/**
|
|
85
87
|
* Pool worker tasks queue.
|
|
86
88
|
*
|
|
87
|
-
* @experimental
|
|
88
89
|
* @defaultValue false
|
|
89
90
|
*/
|
|
90
91
|
enableTasksQueue?: boolean;
|
|
91
92
|
/**
|
|
92
93
|
* Pool worker tasks queue options.
|
|
93
|
-
*
|
|
94
|
-
* @experimental
|
|
95
94
|
*/
|
|
96
95
|
tasksQueueOptions?: TasksQueueOptions;
|
|
97
96
|
}
|
|
@@ -133,12 +132,12 @@ export interface IPool<Worker extends IWorker, Data = unknown, Response = unknow
|
|
|
133
132
|
*/
|
|
134
133
|
findFreeWorkerNodeKey: () => number;
|
|
135
134
|
/**
|
|
136
|
-
*
|
|
135
|
+
* Executes the function specified in the worker constructor with the task data input parameter.
|
|
137
136
|
*
|
|
138
|
-
* @param data - The input for the specified
|
|
139
|
-
* @returns Promise that will be
|
|
137
|
+
* @param data - The task input data for the specified worker function. This can only be serializable data.
|
|
138
|
+
* @returns Promise that will be fulfilled when the task is completed.
|
|
140
139
|
*/
|
|
141
|
-
execute: (data
|
|
140
|
+
execute: (data?: Data) => Promise<Response>;
|
|
142
141
|
/**
|
|
143
142
|
* Shutdowns every current worker in this pool.
|
|
144
143
|
*/
|
|
@@ -147,6 +146,26 @@ export interface IPool<Worker extends IWorker, Data = unknown, Response = unknow
|
|
|
147
146
|
* Sets the worker choice strategy in this pool.
|
|
148
147
|
*
|
|
149
148
|
* @param workerChoiceStrategy - The worker choice strategy.
|
|
149
|
+
* @param workerChoiceStrategyOptions - The worker choice strategy options.
|
|
150
|
+
*/
|
|
151
|
+
setWorkerChoiceStrategy: (workerChoiceStrategy: WorkerChoiceStrategy, workerChoiceStrategyOptions?: WorkerChoiceStrategyOptions) => void;
|
|
152
|
+
/**
|
|
153
|
+
* Sets the worker choice strategy options in this pool.
|
|
154
|
+
*
|
|
155
|
+
* @param workerChoiceStrategyOptions - The worker choice strategy options.
|
|
156
|
+
*/
|
|
157
|
+
setWorkerChoiceStrategyOptions: (workerChoiceStrategyOptions: WorkerChoiceStrategyOptions) => void;
|
|
158
|
+
/**
|
|
159
|
+
* Enables/disables the worker tasks queue in this pool.
|
|
160
|
+
*
|
|
161
|
+
* @param enable - Whether to enable or disable the worker tasks queue.
|
|
162
|
+
* @param tasksQueueOptions - The worker tasks queue options.
|
|
163
|
+
*/
|
|
164
|
+
enableTasksQueue: (enable: boolean, tasksQueueOptions?: TasksQueueOptions) => void;
|
|
165
|
+
/**
|
|
166
|
+
* Sets the worker tasks queue options in this pool.
|
|
167
|
+
*
|
|
168
|
+
* @param tasksQueueOptions - The worker tasks queue options.
|
|
150
169
|
*/
|
|
151
|
-
|
|
170
|
+
setTasksQueueOptions: (tasksQueueOptions: TasksQueueOptions) => void;
|
|
152
171
|
}
|
|
@@ -10,7 +10,7 @@ import type { IWorkerChoiceStrategy, RequiredStatistics, WorkerChoiceStrategyOpt
|
|
|
10
10
|
*/
|
|
11
11
|
export declare abstract class AbstractWorkerChoiceStrategy<Worker extends IWorker, Data = unknown, Response = unknown> implements IWorkerChoiceStrategy {
|
|
12
12
|
protected readonly pool: IPool<Worker, Data, Response>;
|
|
13
|
-
protected
|
|
13
|
+
protected opts: WorkerChoiceStrategyOptions;
|
|
14
14
|
/** @inheritDoc */
|
|
15
15
|
protected readonly isDynamicPool: boolean;
|
|
16
16
|
/** @inheritDoc */
|
|
@@ -22,11 +22,13 @@ export declare abstract class AbstractWorkerChoiceStrategy<Worker extends IWorke
|
|
|
22
22
|
* @param opts - The worker choice strategy options.
|
|
23
23
|
*/
|
|
24
24
|
constructor(pool: IPool<Worker, Data, Response>, opts?: WorkerChoiceStrategyOptions);
|
|
25
|
-
|
|
25
|
+
protected checkOptions(opts: WorkerChoiceStrategyOptions): void;
|
|
26
26
|
/** @inheritDoc */
|
|
27
27
|
abstract reset(): boolean;
|
|
28
28
|
/** @inheritDoc */
|
|
29
29
|
abstract choose(): number;
|
|
30
30
|
/** @inheritDoc */
|
|
31
31
|
abstract remove(workerNodeKey: number): boolean;
|
|
32
|
+
/** @inheritDoc */
|
|
33
|
+
setOptions(opts: WorkerChoiceStrategyOptions): void;
|
|
32
34
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { IPool } from '../pool';
|
|
1
2
|
import type { IWorker } from '../worker';
|
|
2
3
|
import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy';
|
|
3
|
-
import type { IWorkerChoiceStrategy, RequiredStatistics } from './selection-strategies-types';
|
|
4
|
+
import type { IWorkerChoiceStrategy, RequiredStatistics, WorkerChoiceStrategyOptions } from './selection-strategies-types';
|
|
4
5
|
/**
|
|
5
6
|
* Selects the next worker with a fair share scheduling algorithm.
|
|
6
7
|
* Loosely modeled after the fair queueing algorithm: https://en.wikipedia.org/wiki/Fair_queuing.
|
|
@@ -17,6 +18,8 @@ export declare class FairShareWorkerChoiceStrategy<Worker extends IWorker, Data
|
|
|
17
18
|
*/
|
|
18
19
|
private readonly workerLastVirtualTaskTimestamp;
|
|
19
20
|
/** @inheritDoc */
|
|
21
|
+
constructor(pool: IPool<Worker, Data, Response>, opts?: WorkerChoiceStrategyOptions);
|
|
22
|
+
/** @inheritDoc */
|
|
20
23
|
reset(): boolean;
|
|
21
24
|
/** @inheritDoc */
|
|
22
25
|
choose(): number;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { IPool } from '../pool';
|
|
1
2
|
import type { IWorker } from '../worker';
|
|
2
3
|
import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy';
|
|
3
|
-
import type { IWorkerChoiceStrategy, RequiredStatistics } from './selection-strategies-types';
|
|
4
|
+
import type { IWorkerChoiceStrategy, RequiredStatistics, WorkerChoiceStrategyOptions } from './selection-strategies-types';
|
|
4
5
|
/**
|
|
5
6
|
* Selects the less busy worker.
|
|
6
7
|
*
|
|
@@ -12,6 +13,8 @@ export declare class LessBusyWorkerChoiceStrategy<Worker extends IWorker, Data =
|
|
|
12
13
|
/** @inheritDoc */
|
|
13
14
|
readonly requiredStatistics: RequiredStatistics;
|
|
14
15
|
/** @inheritDoc */
|
|
16
|
+
constructor(pool: IPool<Worker, Data, Response>, opts?: WorkerChoiceStrategyOptions);
|
|
17
|
+
/** @inheritDoc */
|
|
15
18
|
reset(): boolean;
|
|
16
19
|
/** @inheritDoc */
|
|
17
20
|
choose(): number;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { IPool } from '../pool';
|
|
1
2
|
import type { IWorker } from '../worker';
|
|
2
3
|
import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy';
|
|
3
|
-
import type { IWorkerChoiceStrategy } from './selection-strategies-types';
|
|
4
|
+
import type { IWorkerChoiceStrategy, WorkerChoiceStrategyOptions } from './selection-strategies-types';
|
|
4
5
|
/**
|
|
5
6
|
* Selects the less used worker.
|
|
6
7
|
*
|
|
@@ -9,6 +10,8 @@ import type { IWorkerChoiceStrategy } from './selection-strategies-types';
|
|
|
9
10
|
* @typeParam Response - Type of execution response. This can only be serializable data.
|
|
10
11
|
*/
|
|
11
12
|
export declare class LessUsedWorkerChoiceStrategy<Worker extends IWorker, Data = unknown, Response = unknown> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy {
|
|
13
|
+
/** @inheritDoc */
|
|
14
|
+
constructor(pool: IPool<Worker, Data, Response>, opts?: WorkerChoiceStrategyOptions);
|
|
12
15
|
/** @inheritDoc */
|
|
13
16
|
reset(): boolean;
|
|
14
17
|
/** @inheritDoc */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { IPool } from '../pool';
|
|
1
2
|
import type { IWorker } from '../worker';
|
|
2
3
|
import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy';
|
|
3
|
-
import type { IWorkerChoiceStrategy } from './selection-strategies-types';
|
|
4
|
+
import type { IWorkerChoiceStrategy, WorkerChoiceStrategyOptions } from './selection-strategies-types';
|
|
4
5
|
/**
|
|
5
6
|
* Selects the next worker in a round robin fashion.
|
|
6
7
|
*
|
|
@@ -14,6 +15,8 @@ export declare class RoundRobinWorkerChoiceStrategy<Worker extends IWorker, Data
|
|
|
14
15
|
*/
|
|
15
16
|
private nextWorkerNodeId;
|
|
16
17
|
/** @inheritDoc */
|
|
18
|
+
constructor(pool: IPool<Worker, Data, Response>, opts?: WorkerChoiceStrategyOptions);
|
|
19
|
+
/** @inheritDoc */
|
|
17
20
|
reset(): boolean;
|
|
18
21
|
/** @inheritDoc */
|
|
19
22
|
choose(): number;
|
|
@@ -33,6 +33,8 @@ export type WorkerChoiceStrategy = keyof typeof WorkerChoiceStrategies;
|
|
|
33
33
|
export interface WorkerChoiceStrategyOptions {
|
|
34
34
|
/**
|
|
35
35
|
* Use tasks median run time instead of average run time.
|
|
36
|
+
*
|
|
37
|
+
* @defaultValue false
|
|
36
38
|
*/
|
|
37
39
|
medRunTime?: boolean;
|
|
38
40
|
}
|
|
@@ -77,4 +79,10 @@ export interface IWorkerChoiceStrategy {
|
|
|
77
79
|
* @param workerNodeKey - The worker node key.
|
|
78
80
|
*/
|
|
79
81
|
remove: (workerNodeKey: number) => boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Sets the worker choice strategy options.
|
|
84
|
+
*
|
|
85
|
+
* @param opts - The worker choice strategy options.
|
|
86
|
+
*/
|
|
87
|
+
setOptions: (opts: WorkerChoiceStrategyOptions) => void;
|
|
80
88
|
}
|
|
@@ -25,12 +25,7 @@ export declare class WeightedRoundRobinWorkerChoiceStrategy<Worker extends IWork
|
|
|
25
25
|
* Workers' virtual task runtime.
|
|
26
26
|
*/
|
|
27
27
|
private readonly workersTaskRunTime;
|
|
28
|
-
/**
|
|
29
|
-
* Constructs a worker choice strategy that selects with a weighted round robin scheduling algorithm.
|
|
30
|
-
*
|
|
31
|
-
* @param pool - The pool instance.
|
|
32
|
-
* @param opts - The worker choice strategy options.
|
|
33
|
-
*/
|
|
28
|
+
/** @inheritDoc */
|
|
34
29
|
constructor(pool: IPool<Worker, Data, Response>, opts?: WorkerChoiceStrategyOptions);
|
|
35
30
|
/** @inheritDoc */
|
|
36
31
|
reset(): boolean;
|
|
@@ -9,16 +9,16 @@ import type { RequiredStatistics, WorkerChoiceStrategy, WorkerChoiceStrategyOpti
|
|
|
9
9
|
* @typeParam Response - Type of execution response. This can only be serializable data.
|
|
10
10
|
*/
|
|
11
11
|
export declare class WorkerChoiceStrategyContext<Worker extends IWorker, Data = unknown, Response = unknown> {
|
|
12
|
-
private
|
|
12
|
+
private workerChoiceStrategy;
|
|
13
13
|
private readonly workerChoiceStrategies;
|
|
14
14
|
/**
|
|
15
15
|
* Worker choice strategy context constructor.
|
|
16
16
|
*
|
|
17
17
|
* @param pool - The pool instance.
|
|
18
|
-
* @param
|
|
18
|
+
* @param workerChoiceStrategy - The worker choice strategy.
|
|
19
19
|
* @param opts - The worker choice strategy options.
|
|
20
20
|
*/
|
|
21
|
-
constructor(pool: IPool<Worker, Data, Response>,
|
|
21
|
+
constructor(pool: IPool<Worker, Data, Response>, workerChoiceStrategy?: WorkerChoiceStrategy, opts?: WorkerChoiceStrategyOptions);
|
|
22
22
|
/**
|
|
23
23
|
* Gets the worker choice strategy in the context required statistics.
|
|
24
24
|
*
|
|
@@ -44,4 +44,10 @@ export declare class WorkerChoiceStrategyContext<Worker extends IWorker, Data =
|
|
|
44
44
|
* @returns `true` if the removal is successful, `false` otherwise.
|
|
45
45
|
*/
|
|
46
46
|
remove(workerNodeKey: number): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Sets the worker choice strategies in the context options.
|
|
49
|
+
*
|
|
50
|
+
* @param opts - The worker choice strategy options.
|
|
51
|
+
*/
|
|
52
|
+
setOptions(opts: WorkerChoiceStrategyOptions): void;
|
|
47
53
|
}
|
|
@@ -36,7 +36,7 @@ export declare class FixedThreadPool<Data = unknown, Response = unknown> extends
|
|
|
36
36
|
/** @inheritDoc */
|
|
37
37
|
protected sendToWorker(worker: ThreadWorkerWithMessageChannel, message: MessageValue<Data>): void;
|
|
38
38
|
/** @inheritDoc */
|
|
39
|
-
protected registerWorkerMessageListener<Message extends Data | Response>(
|
|
39
|
+
protected registerWorkerMessageListener<Message extends Data | Response>(worker: ThreadWorkerWithMessageChannel, listener: (message: MessageValue<Message>) => void): void;
|
|
40
40
|
/** @inheritDoc */
|
|
41
41
|
protected createWorker(): ThreadWorkerWithMessageChannel;
|
|
42
42
|
/** @inheritDoc */
|
package/lib/pools/worker.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export type ExitHandler<Worker extends IWorker> = (this: Worker, code: number) =
|
|
|
23
23
|
*/
|
|
24
24
|
export interface Task<Data = unknown> {
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
26
|
+
* Task input data that will be passed to the worker.
|
|
27
27
|
*/
|
|
28
28
|
readonly data?: Data;
|
|
29
29
|
/**
|
|
@@ -74,7 +74,7 @@ export interface IWorker {
|
|
|
74
74
|
* Register an event listener.
|
|
75
75
|
*
|
|
76
76
|
* @param event - The event.
|
|
77
|
-
* @param handler - The event
|
|
77
|
+
* @param handler - The event handler.
|
|
78
78
|
*/
|
|
79
79
|
on: ((event: 'message', handler: MessageHandler<this>) => void) & ((event: 'error', handler: ErrorHandler<this>) => void) & ((event: 'online', handler: OnlineHandler<this>) => void) & ((event: 'exit', handler: ExitHandler<this>) => void);
|
|
80
80
|
/**
|
package/lib/utility-types.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ import type { KillBehavior } from './worker/worker-options';
|
|
|
6
6
|
import type { IWorker, Task } from './pools/worker';
|
|
7
7
|
/**
|
|
8
8
|
* Make all properties in T non-readonly.
|
|
9
|
+
*
|
|
10
|
+
* @typeParam T - Type in which properties will be non-readonly.
|
|
9
11
|
*/
|
|
10
12
|
export type Draft<T> = {
|
|
11
13
|
-readonly [P in keyof T]?: T[P];
|
|
@@ -32,12 +34,32 @@ export interface MessageValue<Data = unknown, MainWorker extends ClusterWorker |
|
|
|
32
34
|
readonly runTime?: number;
|
|
33
35
|
/**
|
|
34
36
|
* Reference to main worker.
|
|
35
|
-
*
|
|
36
|
-
* Only for internal use.
|
|
37
|
-
* @internal
|
|
38
37
|
*/
|
|
39
38
|
readonly parent?: MainWorker;
|
|
40
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Worker synchronous function that can be executed.
|
|
42
|
+
*
|
|
43
|
+
* @typeParam Data - Type of data sent to the worker. This can only be serializable data.
|
|
44
|
+
* @typeParam Response - Type of execution response. This can only be serializable data.
|
|
45
|
+
*/
|
|
46
|
+
export type WorkerSyncFunction<Data = unknown, Response = unknown> = (data?: Data) => Response;
|
|
47
|
+
/**
|
|
48
|
+
* Worker asynchronous function that can be executed.
|
|
49
|
+
* This function must return a promise.
|
|
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 WorkerAsyncFunction<Data = unknown, Response = unknown> = (data?: Data) => Promise<Response>;
|
|
55
|
+
/**
|
|
56
|
+
* Worker function that can be executed.
|
|
57
|
+
* This function can be synchronous or asynchronous.
|
|
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 WorkerFunction<Data = unknown, Response = unknown> = WorkerSyncFunction<Data, Response> | WorkerAsyncFunction<Data, Response>;
|
|
41
63
|
/**
|
|
42
64
|
* An object holding the execution response promise resolve/reject callbacks.
|
|
43
65
|
*
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { AsyncResource } from 'node:async_hooks';
|
|
6
6
|
import type { Worker } from 'node:cluster';
|
|
7
7
|
import type { MessagePort } from 'node:worker_threads';
|
|
8
|
-
import type { MessageValue } from '../utility-types';
|
|
8
|
+
import type { MessageValue, WorkerAsyncFunction, WorkerFunction, WorkerSyncFunction } from '../utility-types';
|
|
9
9
|
import type { WorkerOptions } from './worker-options';
|
|
10
10
|
/**
|
|
11
11
|
* Base class that implements some shared logic for all poolifier workers.
|
|
@@ -35,14 +35,14 @@ export declare abstract class AbstractWorker<MainWorker extends Worker | Message
|
|
|
35
35
|
* @param mainWorker - Reference to main worker.
|
|
36
36
|
* @param opts - Options for the worker.
|
|
37
37
|
*/
|
|
38
|
-
constructor(type: string, isMain: boolean, fn:
|
|
38
|
+
constructor(type: string, isMain: boolean, fn: WorkerFunction<Data, Response>, mainWorker: MainWorker | undefined | null, opts?: WorkerOptions);
|
|
39
39
|
/**
|
|
40
40
|
* Worker message listener.
|
|
41
41
|
*
|
|
42
42
|
* @param message - Message received.
|
|
43
43
|
* @param fn - Function processed by the worker when the pool's `execution` function is invoked.
|
|
44
44
|
*/
|
|
45
|
-
protected messageListener(message: MessageValue<Data, MainWorker>, fn:
|
|
45
|
+
protected messageListener(message: MessageValue<Data, MainWorker>, fn: WorkerFunction<Data, Response>): void;
|
|
46
46
|
private checkWorkerOptions;
|
|
47
47
|
/**
|
|
48
48
|
* Checks if the `fn` parameter is passed to the constructor.
|
|
@@ -79,12 +79,12 @@ export declare abstract class AbstractWorker<MainWorker extends Worker | Message
|
|
|
79
79
|
* @param fn - Function that will be executed.
|
|
80
80
|
* @param message - Input data for the given function.
|
|
81
81
|
*/
|
|
82
|
-
protected run(fn:
|
|
82
|
+
protected run(fn: WorkerSyncFunction<Data, Response>, message: MessageValue<Data>): void;
|
|
83
83
|
/**
|
|
84
84
|
* Runs the given function asynchronously.
|
|
85
85
|
*
|
|
86
86
|
* @param fn - Function that will be executed.
|
|
87
87
|
* @param message - Input data for the given function.
|
|
88
88
|
*/
|
|
89
|
-
protected runAsync(fn:
|
|
89
|
+
protected runAsync(fn: WorkerAsyncFunction<Data, Response>, message: MessageValue<Data>): void;
|
|
90
90
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import type { Worker } from 'node:cluster';
|
|
3
|
-
import type { MessageValue } from '../utility-types';
|
|
3
|
+
import type { MessageValue, WorkerFunction } from '../utility-types';
|
|
4
4
|
import { AbstractWorker } from './abstract-worker';
|
|
5
5
|
import type { WorkerOptions } from './worker-options';
|
|
6
6
|
/**
|
|
@@ -24,7 +24,7 @@ export declare class ClusterWorker<Data = unknown, Response = unknown> extends A
|
|
|
24
24
|
* @param fn - Function processed by the worker when the pool's `execution` function is invoked.
|
|
25
25
|
* @param opts - Options for the worker.
|
|
26
26
|
*/
|
|
27
|
-
constructor(fn:
|
|
27
|
+
constructor(fn: WorkerFunction<Data, Response>, opts?: WorkerOptions);
|
|
28
28
|
/** @inheritDoc */
|
|
29
29
|
protected sendToMainWorker(message: MessageValue<Response>): void;
|
|
30
30
|
/** @inheritDoc */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import type { MessagePort } from 'node:worker_threads';
|
|
3
|
-
import type { MessageValue } from '../utility-types';
|
|
3
|
+
import type { MessageValue, WorkerFunction } from '../utility-types';
|
|
4
4
|
import { AbstractWorker } from './abstract-worker';
|
|
5
5
|
import type { WorkerOptions } from './worker-options';
|
|
6
6
|
/**
|
|
@@ -24,7 +24,7 @@ export declare class ThreadWorker<Data = unknown, Response = unknown> extends Ab
|
|
|
24
24
|
* @param fn - Function processed by the worker when the pool's `execution` function is invoked.
|
|
25
25
|
* @param opts - Options for the worker.
|
|
26
26
|
*/
|
|
27
|
-
constructor(fn:
|
|
27
|
+
constructor(fn: WorkerFunction<Data, Response>, opts?: WorkerOptions);
|
|
28
28
|
/** @inheritDoc */
|
|
29
29
|
protected sendToMainWorker(message: MessageValue<Response>): void;
|
|
30
30
|
}
|
|
@@ -38,7 +38,7 @@ export interface WorkerOptions {
|
|
|
38
38
|
* when this timeout expires your tasks is interrupted and the worker is killed if is not part of the minimum size of the pool.
|
|
39
39
|
* - If `killBehavior` is set to `KillBehaviors.SOFT` your tasks have no timeout and your workers will not be terminated until your task is completed.
|
|
40
40
|
*
|
|
41
|
-
* @defaultValue 60000
|
|
41
|
+
* @defaultValue 60000
|
|
42
42
|
*/
|
|
43
43
|
maxInactiveTime?: number;
|
|
44
44
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "poolifier",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.10",
|
|
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",
|
|
@@ -73,8 +73,8 @@
|
|
|
73
73
|
"lib"
|
|
74
74
|
],
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@commitlint/cli": "^17.
|
|
77
|
-
"@commitlint/config-conventional": "^17.
|
|
76
|
+
"@commitlint/cli": "^17.6.1",
|
|
77
|
+
"@commitlint/config-conventional": "^17.6.1",
|
|
78
78
|
"@release-it/bumper": "^4.0.2",
|
|
79
79
|
"@release-it/keep-a-changelog": "^3.1.0",
|
|
80
80
|
"@rollup/plugin-terser": "^0.4.1",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"eslint": "^8.38.0",
|
|
88
88
|
"eslint-config-standard": "^17.0.0",
|
|
89
89
|
"eslint-config-standard-with-typescript": "^34.0.1",
|
|
90
|
-
"eslint-define-config": "^1.
|
|
90
|
+
"eslint-define-config": "^1.18.0",
|
|
91
91
|
"eslint-import-resolver-typescript": "^3.5.5",
|
|
92
92
|
"eslint-plugin-import": "^2.27.5",
|
|
93
93
|
"eslint-plugin-jsdoc": "^41.1.1",
|