poolifier 2.4.9 → 2.4.11
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 +29 -28
- package/lib/circular-array.d.ts +4 -0
- 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 +3 -5
- package/lib/pools/pool.d.ts +4 -14
- package/lib/pools/selection-strategies/abstract-worker-choice-strategy.d.ts +30 -0
- package/lib/pools/thread/fixed.d.ts +1 -1
- package/lib/pools/worker.d.ts +2 -2
- package/lib/utility-types.d.ts +23 -0
- package/lib/worker/abstract-worker.d.ts +11 -11
- 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 +21 -17
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>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<img alt="Actions Status" src="https://github.com/poolifier/poolifier/workflows/NodeCI/badge.svg"></a>
|
|
12
12
|
<a href="https://sonarcloud.io/dashboard?id=pioardi_poolifier">
|
|
13
13
|
<img alt="Quality Gate Status" src="https://sonarcloud.io/api/project_badges/measure?project=pioardi_poolifier&metric=alert_status"></a>
|
|
14
|
-
<a href="https://sonarcloud.io/
|
|
14
|
+
<a href="https://sonarcloud.io/dashboard?id=pioardi_poolifier">
|
|
15
15
|
<img alt="Code Coverage" src="https://sonarcloud.io/api/project_badges/measure?project=pioardi_poolifier&metric=coverage"></a>
|
|
16
16
|
<a href="https://standardjs.com">
|
|
17
17
|
<img alt="Javascript Standard Style Guide" src="https://img.shields.io/badge/code_style-standard-brightgreen.svg"></a>
|
|
@@ -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/circular-array.d.ts
CHANGED
|
@@ -4,9 +4,13 @@
|
|
|
4
4
|
export declare class CircularArray<T> extends Array<T> {
|
|
5
5
|
size: number;
|
|
6
6
|
constructor(size?: number, ...items: T[]);
|
|
7
|
+
/** @inheritDoc */
|
|
7
8
|
push(...items: T[]): number;
|
|
9
|
+
/** @inheritDoc */
|
|
8
10
|
unshift(...items: T[]): number;
|
|
11
|
+
/** @inheritDoc */
|
|
9
12
|
concat(...items: Array<T | ConcatArray<T>>): CircularArray<T>;
|
|
13
|
+
/** @inheritDoc */
|
|
10
14
|
splice(start: number, deleteCount?: number, ...items: T[]): T[];
|
|
11
15
|
resize(size: number): void;
|
|
12
16
|
empty(): boolean;
|
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,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 a extends t{}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(t,r=k){this.pool=t,this.opts=r,this.isDynamicPool=this.pool.type===e.DYNAMIC,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.bind(this),this.workerChoiceStrategies=new Map([[l.ROUND_ROBIN,new g(e,r)],[l.LESS_USED,new T(e,r)],[l.LESS_BUSY,new m(e,r)],[l.FAIR_SHARE,new p(e,r)],[l.WEIGHTED_ROUND_ROBIN,new w(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.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(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){if(!0===this.opts.enableTasksQueue&&!e)for(const[e]of this.workerNodes.entries())this.flushTasksQueue(e);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(h.busy),this.type===e.DYNAMIC&&this.full&&this.emitter?.emit(h.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)}}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=h,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;
|
|
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 a extends t{}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;toggleFindLastFreeWorkerNodeKey=!1;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}findFreeWorkerNodeKey(){return this.toggleFindLastFreeWorkerNodeKey?(this.toggleFindLastFreeWorkerNodeKey=!1,this.findLastFreeWorkerNodeKey()):(this.toggleFindLastFreeWorkerNodeKey=!0,this.findFirstFreeWorkerNodeKey())}findFirstFreeWorkerNodeKey(){return this.pool.workerNodes.findIndex((e=>0===e.tasksUsage?.running))}findLastFreeWorkerNodeKey(){for(let e=this.pool.workerNodes.length-1;e>=0;e--)if(0===this.pool.workerNodes[e].tasksUsage?.running)return e;return-1}}class 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)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.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 g extends d{constructor(e,t=k){super(e,t),this.checkOptions(this.opts)}reset(){return!0}choose(){const e=this.findFreeWorkerNodeKey();if(-1!==e)return e;let t,r=1/0;for(const[e,s]of this.pool.workerNodes.entries()){const i=s.tasksUsage,o=i.run+i.running;if(0===o)return e;o<r&&(r=o,t=e)}return t}remove(e){return!0}}class T 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(T.bind(this))(e,r)],[l.LESS_USED,new(g.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 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(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.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(h.busy),this.type===e.DYNAMIC&&this.full&&this.emitter?.emit(h.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)}))}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")}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())}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=h,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 t from"node:cluster";import r from"node:crypto";import{cpus as s}from"node:os";import{isMainThread as i,Worker as o,SHARE_ENV as n,MessageChannel as a,parentPort as h}from"node:worker_threads";import{AsyncResource as u}from"node:async_hooks";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,t=p){this.pool=e,this.opts=t,this.isDynamicPool=this.pool.type===k.DYNAMIC,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 T{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[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 f extends T{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,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 W extends T{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,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 y extends T{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 T{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,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 s()){const r=t.speed.toString().length-1;e+=1/(t.speed/Math.pow(10,r))*Math.pow(10,r)}return Math.round(e/s().length)}}class S{workerChoiceStrategy;workerChoiceStrategies;constructor(e,t=g.ROUND_ROBIN,r=p){this.workerChoiceStrategy=t,this.execute.bind(this),this.workerChoiceStrategies=new Map([[g.ROUND_ROBIN,new y(e,r)],[g.LESS_USED,new W(e,r)],[g.LESS_BUSY,new f(e,r)],[g.FAIR_SHARE,new w(e,r)],[g.WEIGHTED_ROUND_ROBIN,new N(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 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,...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 I{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.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){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&&(this.checkValidTasksQueueOptions(e.tasksQueueOptions),this.opts.tasksQueueOptions=this.buildTasksQueueOptions(e.tasksQueueOptions))}checkValidWorkerChoiceStrategy(e){if(!Object.values(g).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){if(!0===this.opts.enableTasksQueue&&!e)for(const[e]of this.workerNodes.entries())this.flushTasksQueue(e);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,s]=this.chooseWorkerNode(),i={data:e??{},id:r.randomUUID()},o=new Promise(((e,t)=>{this.promiseResponseMap.set(i.id,{resolve:e,reject:t,worker:s.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 e;if(this.type===k.DYNAMIC&&!this.full&&this.internalBusy()){const t=this.createAndSetupWorker();this.registerWorkerMessageListener(t,(e=>{var r;r=m.HARD,(e.kill===r||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 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(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,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)}}class O extends I{opts;constructor(e,t,r={}){super(e,t,r),this.opts=r}setupHook(){t.setupPrimary({...this.opts.settings,exec:this.filePath})}isMain(){return t.isPrimary}destroyWorker(e){this.sendToWorker(e,{kill:1}),e.kill()}sendToWorker(e,t){e.send(t)}registerWorkerMessageListener(e,t){e.on("message",t)}createWorker(){return t.fork(this.opts.env)}afterWorkerSetup(e){this.registerWorkerMessageListener(e,super.workerListener())}get type(){return k.FIXED}get full(){return this.workerNodes.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}class x extends O{max;constructor(e,t,r,s={}){super(e,r,s),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,r={}){super(e,t,r)}isMain(){return i}async destroyWorker(e){this.sendToWorker(e,{kill:1}),await e.terminate()}sendToWorker(e,t){e.postMessage(t)}registerWorkerMessageListener(e,t){e.port2?.on("message",t)}createWorker(){return new o(this.filePath,{env:n})}afterWorkerSetup(e){const{port1:t,port2:r}=new a;e.postMessage({parent:t},[t]),e.port1=t,e.port2=r,this.registerWorkerMessageListener(e,super.workerListener())}get type(){return k.FIXED}get full(){return this.workerNodes.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}class C extends v{max;constructor(e,t,r,s={}){super(e,r,s),this.max=t}get type(){return k.DYNAMIC}get full(){return this.workerNodes.length===this.max}get busy(){return this.full&&this.internalBusy()}}const b=6e4,E=m.SOFT;class M extends u{isMain;mainWorker;opts;lastTaskTimestamp;aliveInterval;constructor(e,t,r,s,i={killBehavior:E,maxInactiveTime:b}){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??b)/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??E,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");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??b)&&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(d)}}class Q extends M{constructor(e,r={}){super("worker-cluster-pool:poolifier",t.isPrimary,e,t.worker,r)}sendToMainWorker(e){this.getMainWorker().send(e)}handleError(e){return e instanceof Error?e.message:e}}class U extends M{constructor(e,t={}){super("worker-thread-pool:poolifier",i,e,h,t)}sendToMainWorker(e){this.getMainWorker().postMessage(e)}}export{Q as ClusterWorker,x as DynamicClusterPool,C as DynamicThreadPool,O as FixedClusterPool,v as FixedThreadPool,m as KillBehaviors,l as PoolEvents,U as ThreadWorker,g as WorkerChoiceStrategies};
|
|
1
|
+
import e from"node:events";import t from"node:cluster";import r from"node:crypto";import{cpus as s}from"node:os";import{isMainThread as i,Worker as o,SHARE_ENV as n,MessageChannel as 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 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;toggleFindLastFreeWorkerNodeKey=!1;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}findFreeWorkerNodeKey(){return this.toggleFindLastFreeWorkerNodeKey?(this.toggleFindLastFreeWorkerNodeKey=!1,this.findLastFreeWorkerNodeKey()):(this.toggleFindLastFreeWorkerNodeKey=!0,this.findFirstFreeWorkerNodeKey())}findFirstFreeWorkerNodeKey(){return this.pool.workerNodes.findIndex((e=>0===e.tasksUsage?.running))}findLastFreeWorkerNodeKey(){for(let e=this.pool.workerNodes.length-1;e>=0;e--)if(0===this.pool.workerNodes[e].tasksUsage?.running)return e;return-1}}class w extends T{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[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)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 f extends T{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.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 W extends T{constructor(e,t=p){super(e,t),this.checkOptions(this.opts)}reset(){return!0}choose(){const e=this.findFreeWorkerNodeKey();if(-1!==e)return e;let t,r=1/0;for(const[e,s]of this.pool.workerNodes.entries()){const i=s.tasksUsage,o=i.run+i.running;if(0===o)return e;o<r&&(r=o,t=e)}return t}remove(e){return!0}}class y extends T{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 T{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,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 s()){const r=t.speed.toString().length-1;e+=1/(t.speed/Math.pow(10,r))*Math.pow(10,r)}return Math.round(e/s().length)}}class S{workerChoiceStrategy;workerChoiceStrategies;constructor(e,t=g.ROUND_ROBIN,r=p){this.workerChoiceStrategy=t,this.execute=this.execute.bind(this),this.workerChoiceStrategies=new Map([[g.ROUND_ROBIN,new(y.bind(this))(e,r)],[g.LESS_USED,new(W.bind(this))(e,r)],[g.LESS_BUSY,new(f.bind(this))(e,r)],[g.FAIR_SHARE,new(w.bind(this))(e,r)],[g.WEIGHTED_ROUND_ROBIN,new(N.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 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,...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 I{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 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??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&&(this.checkValidTasksQueueOptions(e.tasksQueueOptions),this.opts.tasksQueueOptions=this.buildTasksQueueOptions(e.tasksQueueOptions))}checkValidWorkerChoiceStrategy(e){if(!Object.values(g).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.workerNodes.findIndex((e=>0===e.tasksUsage?.running))}async execute(e){const[t,s]=this.chooseWorkerNode(),i={data:e??{},id:r.randomUUID()},o=new Promise(((e,t)=>{this.promiseResponseMap.set(i.id,{resolve:e,reject:t,worker:s.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 e;if(this.type===k.DYNAMIC&&!this.full&&this.internalBusy()){const t=this.createAndSetupWorker();this.registerWorkerMessageListener(t,(e=>{var r;r=m.HARD,(e.kill===r||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 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(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,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 x extends I{opts;constructor(e,t,r={}){super(e,t,r),this.opts=r}setupHook(){t.setupPrimary({...this.opts.settings,exec:this.filePath})}isMain(){return t.isPrimary}destroyWorker(e){this.sendToWorker(e,{kill:1}),e.kill()}sendToWorker(e,t){e.send(t)}registerWorkerMessageListener(e,t){e.on("message",t)}createWorker(){return t.fork(this.opts.env)}afterWorkerSetup(e){this.registerWorkerMessageListener(e,super.workerListener())}get type(){return k.FIXED}get full(){return this.workerNodes.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}class O extends x{max;constructor(e,t,r,s={}){super(e,r,s),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,r={}){super(e,t,r)}isMain(){return i}async destroyWorker(e){this.sendToWorker(e,{kill:1}),await e.terminate()}sendToWorker(e,t){e.postMessage(t)}registerWorkerMessageListener(e,t){e.port2?.on("message",t)}createWorker(){return new o(this.filePath,{env:n})}afterWorkerSetup(e){const{port1:t,port2:r}=new h;e.postMessage({parent:t},[t]),e.port1=t,e.port2=r,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,r,s={}){super(e,r,s),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,r,s,i={killBehavior:E,maxInactiveTime:C}){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??C)/2),this.checkAlive.bind(this)()),this.mainWorker?.on("message",(e=>{this.messageListener(e,r)}))}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")}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())}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 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(d)}}class Q extends M{constructor(e,r={}){super("worker-cluster-pool:poolifier",t.isPrimary,e,t.worker,r)}sendToMainWorker(e){this.getMainWorker().send(e)}handleError(e){return e instanceof Error?e.message:e}}class 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,g as WorkerChoiceStrategies};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { MessageValue, PromiseResponseWrapper } from '../utility-types';
|
|
2
|
-
import { type IPool, type PoolOptions, type TasksQueueOptions, PoolType } from './pool';
|
|
3
|
-
import { PoolEmitter } from './pool';
|
|
2
|
+
import { PoolEmitter, type IPool, type PoolOptions, type TasksQueueOptions, PoolType } from './pool';
|
|
4
3
|
import type { IWorker, WorkerNode } from './worker';
|
|
5
4
|
import { type WorkerChoiceStrategy, type WorkerChoiceStrategyOptions } from './selection-strategies/selection-strategies-types';
|
|
6
5
|
import { WorkerChoiceStrategyContext } from './selection-strategies/worker-choice-strategy-context';
|
|
@@ -87,9 +86,7 @@ export declare abstract class AbstractPool<Worker extends IWorker, Data = unknow
|
|
|
87
86
|
protected abstract get busy(): boolean;
|
|
88
87
|
protected internalBusy(): boolean;
|
|
89
88
|
/** @inheritDoc */
|
|
90
|
-
|
|
91
|
-
/** @inheritDoc */
|
|
92
|
-
execute(data: Data): Promise<Response>;
|
|
89
|
+
execute(data?: Data): Promise<Response>;
|
|
93
90
|
/** @inheritDoc */
|
|
94
91
|
destroy(): Promise<void>;
|
|
95
92
|
/**
|
|
@@ -214,4 +211,5 @@ export declare abstract class AbstractPool<Worker extends IWorker, Data = unknow
|
|
|
214
211
|
private tasksQueueSize;
|
|
215
212
|
private flushTasksQueue;
|
|
216
213
|
private flushTasksQueueByWorker;
|
|
214
|
+
private flushTasksQueues;
|
|
217
215
|
}
|
package/lib/pools/pool.d.ts
CHANGED
|
@@ -122,22 +122,12 @@ export interface IPool<Worker extends IWorker, Data = unknown, Response = unknow
|
|
|
122
122
|
*/
|
|
123
123
|
readonly emitter?: PoolEmitter;
|
|
124
124
|
/**
|
|
125
|
-
*
|
|
125
|
+
* Executes the function specified in the worker constructor with the task data input parameter.
|
|
126
126
|
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* If no free worker is found, `-1` is returned.
|
|
130
|
-
*
|
|
131
|
-
* @returns A worker node key if there is one, `-1` otherwise.
|
|
132
|
-
*/
|
|
133
|
-
findFreeWorkerNodeKey: () => number;
|
|
134
|
-
/**
|
|
135
|
-
* Executes the function specified in the constructor with the task data input parameter.
|
|
136
|
-
*
|
|
137
|
-
* @param data - The task input data for the specified function. This can only be serializable data.
|
|
138
|
-
* @returns Promise that will be resolved when the task is successfully completed.
|
|
127
|
+
* @param data - The task input data for the specified worker function. This can only be serializable data.
|
|
128
|
+
* @returns Promise that will be fulfilled when the task is completed.
|
|
139
129
|
*/
|
|
140
|
-
execute: (data
|
|
130
|
+
execute: (data?: Data) => Promise<Response>;
|
|
141
131
|
/**
|
|
142
132
|
* Shutdowns every current worker in this pool.
|
|
143
133
|
*/
|
|
@@ -11,6 +11,10 @@ import type { IWorkerChoiceStrategy, RequiredStatistics, WorkerChoiceStrategyOpt
|
|
|
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
13
|
protected opts: WorkerChoiceStrategyOptions;
|
|
14
|
+
/**
|
|
15
|
+
* Toggles finding the last free worker node key.
|
|
16
|
+
*/
|
|
17
|
+
private toggleFindLastFreeWorkerNodeKey;
|
|
14
18
|
/** @inheritDoc */
|
|
15
19
|
protected readonly isDynamicPool: boolean;
|
|
16
20
|
/** @inheritDoc */
|
|
@@ -31,4 +35,30 @@ export declare abstract class AbstractWorkerChoiceStrategy<Worker extends IWorke
|
|
|
31
35
|
abstract remove(workerNodeKey: number): boolean;
|
|
32
36
|
/** @inheritDoc */
|
|
33
37
|
setOptions(opts: WorkerChoiceStrategyOptions): void;
|
|
38
|
+
/**
|
|
39
|
+
* Finds a free worker node key.
|
|
40
|
+
*
|
|
41
|
+
* @returns The free worker node key or `-1` if there is no free worker node.
|
|
42
|
+
*/
|
|
43
|
+
protected findFreeWorkerNodeKey(): number;
|
|
44
|
+
/**
|
|
45
|
+
* Finds the first free worker node key based on the number of tasks the worker has applied.
|
|
46
|
+
*
|
|
47
|
+
* If a worker is found with `0` running tasks, it is detected as free and its worker node key is returned.
|
|
48
|
+
*
|
|
49
|
+
* If no free worker is found, `-1` is returned.
|
|
50
|
+
*
|
|
51
|
+
* @returns A worker node key if there is one, `-1` otherwise.
|
|
52
|
+
*/
|
|
53
|
+
private findFirstFreeWorkerNodeKey;
|
|
54
|
+
/**
|
|
55
|
+
* Finds the last free worker node key based on the number of tasks the worker has applied.
|
|
56
|
+
*
|
|
57
|
+
* If a worker is found with `0` running tasks, it is detected as free and its worker node key is returned.
|
|
58
|
+
*
|
|
59
|
+
* If no free worker is found, `-1` is returned.
|
|
60
|
+
*
|
|
61
|
+
* @returns A worker node key if there is one, `-1` otherwise.
|
|
62
|
+
*/
|
|
63
|
+
private findLastFreeWorkerNodeKey;
|
|
34
64
|
}
|
|
@@ -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
|
@@ -74,11 +74,11 @@ 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
|
/**
|
|
81
|
-
* Register a listener to the exit event that will only performed once.
|
|
81
|
+
* Register a listener to the exit event that will only be performed once.
|
|
82
82
|
*
|
|
83
83
|
* @param event - `'exit'`.
|
|
84
84
|
* @param handler - The exit handler.
|
package/lib/utility-types.d.ts
CHANGED
|
@@ -37,6 +37,29 @@ export interface MessageValue<Data = unknown, MainWorker extends ClusterWorker |
|
|
|
37
37
|
*/
|
|
38
38
|
readonly parent?: MainWorker;
|
|
39
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>;
|
|
40
63
|
/**
|
|
41
64
|
* An object holding the execution response promise resolve/reject callbacks.
|
|
42
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,7 @@ 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:
|
|
39
|
-
/**
|
|
40
|
-
* Worker message listener.
|
|
41
|
-
*
|
|
42
|
-
* @param message - Message received.
|
|
43
|
-
* @param fn - Function processed by the worker when the pool's `execution` function is invoked.
|
|
44
|
-
*/
|
|
45
|
-
protected messageListener(message: MessageValue<Data, MainWorker>, fn: (data: Data) => Response | Promise<Response>): void;
|
|
38
|
+
constructor(type: string, isMain: boolean, fn: WorkerFunction<Data, Response>, mainWorker: MainWorker | undefined | null, opts?: WorkerOptions);
|
|
46
39
|
private checkWorkerOptions;
|
|
47
40
|
/**
|
|
48
41
|
* Checks if the `fn` parameter is passed to the constructor.
|
|
@@ -50,6 +43,13 @@ export declare abstract class AbstractWorker<MainWorker extends Worker | Message
|
|
|
50
43
|
* @param fn - The function that should be defined.
|
|
51
44
|
*/
|
|
52
45
|
private checkFunctionInput;
|
|
46
|
+
/**
|
|
47
|
+
* Worker message listener.
|
|
48
|
+
*
|
|
49
|
+
* @param message - Message received.
|
|
50
|
+
* @param fn - Function processed by the worker when the pool's `execution` function is invoked.
|
|
51
|
+
*/
|
|
52
|
+
protected messageListener(message: MessageValue<Data, MainWorker>, fn: WorkerFunction<Data, Response>): void;
|
|
53
53
|
/**
|
|
54
54
|
* Returns the main worker.
|
|
55
55
|
*
|
|
@@ -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.11",
|
|
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",
|
|
@@ -21,7 +21,11 @@
|
|
|
21
21
|
]
|
|
22
22
|
},
|
|
23
23
|
"engines": {
|
|
24
|
-
"node": ">=16.0.0"
|
|
24
|
+
"node": ">=16.0.0",
|
|
25
|
+
"pnpm": ">= 8.0.0"
|
|
26
|
+
},
|
|
27
|
+
"volta": {
|
|
28
|
+
"node": "20.0.0"
|
|
25
29
|
},
|
|
26
30
|
"repository": {
|
|
27
31
|
"type": "git",
|
|
@@ -79,18 +83,18 @@
|
|
|
79
83
|
"@release-it/keep-a-changelog": "^3.1.0",
|
|
80
84
|
"@rollup/plugin-terser": "^0.4.1",
|
|
81
85
|
"@rollup/plugin-typescript": "^11.1.0",
|
|
82
|
-
"@types/node": "^18.
|
|
83
|
-
"@typescript-eslint/eslint-plugin": "^5.
|
|
84
|
-
"@typescript-eslint/parser": "^5.
|
|
86
|
+
"@types/node": "^18.16.0",
|
|
87
|
+
"@typescript-eslint/eslint-plugin": "^5.59.0",
|
|
88
|
+
"@typescript-eslint/parser": "^5.59.0",
|
|
85
89
|
"benny": "^3.7.1",
|
|
86
90
|
"c8": "^7.13.0",
|
|
87
|
-
"eslint": "^8.
|
|
91
|
+
"eslint": "^8.39.0",
|
|
88
92
|
"eslint-config-standard": "^17.0.0",
|
|
89
93
|
"eslint-config-standard-with-typescript": "^34.0.1",
|
|
90
94
|
"eslint-define-config": "^1.18.0",
|
|
91
95
|
"eslint-import-resolver-typescript": "^3.5.5",
|
|
92
96
|
"eslint-plugin-import": "^2.27.5",
|
|
93
|
-
"eslint-plugin-jsdoc": "^
|
|
97
|
+
"eslint-plugin-jsdoc": "^43.0.7",
|
|
94
98
|
"eslint-plugin-n": "^15.7.0",
|
|
95
99
|
"eslint-plugin-promise": "^6.1.1",
|
|
96
100
|
"eslint-plugin-spellcheck": "^0.0.20",
|
|
@@ -101,29 +105,29 @@
|
|
|
101
105
|
"microtime": "^3.1.1",
|
|
102
106
|
"mocha": "^10.2.0",
|
|
103
107
|
"mochawesome": "^7.1.3",
|
|
104
|
-
"prettier": "^2.8.
|
|
108
|
+
"prettier": "^2.8.8",
|
|
105
109
|
"prettier-plugin-organize-imports": "^3.2.2",
|
|
106
110
|
"release-it": "^15.10.1",
|
|
107
|
-
"rollup": "^3.20.
|
|
111
|
+
"rollup": "^3.20.7",
|
|
108
112
|
"rollup-plugin-analyzer": "^4.0.0",
|
|
109
113
|
"rollup-plugin-command": "^1.1.3",
|
|
110
114
|
"rollup-plugin-delete": "^2.0.0",
|
|
111
|
-
"sinon": "^15.0.
|
|
115
|
+
"sinon": "^15.0.4",
|
|
112
116
|
"source-map-support": "^0.5.21",
|
|
113
117
|
"ts-standard": "^12.0.2",
|
|
114
|
-
"typedoc": "^0.24.
|
|
118
|
+
"typedoc": "^0.24.5",
|
|
115
119
|
"typescript": "^5.0.4"
|
|
116
120
|
},
|
|
117
121
|
"scripts": {
|
|
118
|
-
"preinstall": "npx only-allow pnpm",
|
|
122
|
+
"preinstall": "npx --yes only-allow pnpm",
|
|
119
123
|
"build": "rollup --config --environment BUILD:development",
|
|
120
124
|
"build:typedoc": "rollup --config --environment BUILD:development --environment DOCUMENTATION",
|
|
121
125
|
"build:prod": "rollup --config",
|
|
122
|
-
"benchmark": "pnpm
|
|
123
|
-
"benchmark:debug": "pnpm
|
|
124
|
-
"benchmark:prod": "pnpm
|
|
125
|
-
"test": "pnpm
|
|
126
|
-
"test:debug": "pnpm
|
|
126
|
+
"benchmark": "pnpm build && node -r source-map-support/register benchmarks/internal/bench.js",
|
|
127
|
+
"benchmark:debug": "pnpm build && node -r source-map-support/register --inspect benchmarks/internal/bench.js",
|
|
128
|
+
"benchmark:prod": "pnpm build:prod && node -r source-map-support/register benchmarks/internal/bench.js",
|
|
129
|
+
"test": "pnpm build && c8 mocha 'tests/**/*.test.js'",
|
|
130
|
+
"test:debug": "pnpm build && mocha --no-parallel --inspect 'tests/**/*.test.js'",
|
|
127
131
|
"coverage": "c8 report --reporter=lcov",
|
|
128
132
|
"coverage:html": "c8 report --reporter=html",
|
|
129
133
|
"format": "prettier . --cache --write; ts-standard . --fix",
|