poolifier 2.4.11 → 2.4.12
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 +4 -4
- package/lib/index.d.ts +3 -2
- package/lib/index.js +1 -1
- package/lib/index.mjs +1 -1
- package/lib/pools/abstract-pool.d.ts +5 -11
- package/lib/pools/cluster/dynamic.d.ts +2 -0
- package/lib/pools/cluster/fixed.d.ts +2 -0
- package/lib/pools/pool.d.ts +6 -1
- package/lib/pools/selection-strategies/abstract-worker-choice-strategy.d.ts +3 -3
- package/lib/pools/selection-strategies/fair-share-worker-choice-strategy.d.ts +6 -4
- package/lib/pools/selection-strategies/less-busy-worker-choice-strategy.d.ts +3 -1
- package/lib/pools/selection-strategies/less-used-worker-choice-strategy.d.ts +3 -1
- package/lib/pools/selection-strategies/round-robin-worker-choice-strategy.d.ts +2 -0
- package/lib/pools/selection-strategies/selection-strategies-types.d.ts +20 -2
- package/lib/pools/selection-strategies/weighted-round-robin-worker-choice-strategy.d.ts +6 -7
- package/lib/pools/selection-strategies/worker-choice-strategy-context.d.ts +7 -1
- package/lib/pools/thread/dynamic.d.ts +2 -0
- package/lib/pools/thread/fixed.d.ts +2 -0
- package/lib/pools/worker.d.ts +7 -2
- package/lib/queue.d.ts +31 -0
- package/lib/utility-types.d.ts +11 -1
- package/lib/utils.d.ts +1 -0
- package/lib/worker/abstract-worker.d.ts +18 -9
- package/lib/worker/cluster-worker.d.ts +3 -3
- package/lib/worker/thread-worker.d.ts +3 -3
- package/lib/worker/worker-options.d.ts +1 -0
- package/package.json +16 -16
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
## Why Poolifier?
|
|
31
31
|
|
|
32
|
-
Poolifier is used to perform CPU intensive and I/O intensive tasks on nodejs servers, it implements worker pools
|
|
32
|
+
Poolifier is used to perform CPU intensive and I/O intensive tasks on nodejs servers, it implements worker pools 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
35
|
Please consult our [general guidelines](#general-guidance).
|
|
@@ -139,8 +139,7 @@ pool.execute({}).then(res => {
|
|
|
139
139
|
|
|
140
140
|
You can do the same with the classes ClusterWorker, FixedClusterPool and DynamicClusterPool.
|
|
141
141
|
|
|
142
|
-
**See examples folder for more details (in particular if you want to use a pool
|
|
143
|
-
**Now TypeScript is also supported, find how to use it into the example folder**.
|
|
142
|
+
**See examples folder for more details (in particular if you want to use a pool with [multiple worker functions](./examples/multiFunctionExample.js))**.
|
|
144
143
|
|
|
145
144
|
Remember that workers can only send and receive serializable data.
|
|
146
145
|
|
|
@@ -175,6 +174,7 @@ Node versions >= 16.x are supported.
|
|
|
175
174
|
Properties:
|
|
176
175
|
|
|
177
176
|
- `medRunTime` (optional) - Use the tasks median run time instead of the tasks average run time in worker choice strategies.
|
|
177
|
+
- `weights` (optional) - The worker weights to use in the weighted round robin worker choice strategy: `{ 0: 200, 1: 300, ..., n: 100 }`
|
|
178
178
|
|
|
179
179
|
Default: `{ medRunTime: false }`
|
|
180
180
|
|
|
@@ -209,7 +209,7 @@ This method will call the terminate method on each worker.
|
|
|
209
209
|
|
|
210
210
|
### `class YourWorker extends ThreadWorker/ClusterWorker`
|
|
211
211
|
|
|
212
|
-
`
|
|
212
|
+
`taskFunctions` (mandatory) The task function(s) that you want to execute on the worker
|
|
213
213
|
`opts` (optional) An object with these properties:
|
|
214
214
|
|
|
215
215
|
- `maxInactiveTime` (optional) - Max time to wait tasks to work on in milliseconds, after this period the new worker will die.
|
package/lib/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { FixedClusterPool } from './pools/cluster/fixed';
|
|
|
3
3
|
export type { ClusterPoolOptions } from './pools/cluster/fixed';
|
|
4
4
|
export type { AbstractPool } from './pools/abstract-pool';
|
|
5
5
|
export { PoolEvents } from './pools/pool';
|
|
6
|
-
export type { IPool, PoolEmitter,
|
|
6
|
+
export type { IPool, PoolEmitter, PoolEvent, PoolOptions, PoolType, TasksQueueOptions } from './pools/pool';
|
|
7
7
|
export type { ErrorHandler, ExitHandler, IWorker, MessageHandler, OnlineHandler, Task, TasksUsage, WorkerNode } from './pools/worker';
|
|
8
8
|
export { WorkerChoiceStrategies } from './pools/selection-strategies/selection-strategies-types';
|
|
9
9
|
export type { IWorkerChoiceStrategy, RequiredStatistics, WorkerChoiceStrategy, WorkerChoiceStrategyOptions } from './pools/selection-strategies/selection-strategies-types';
|
|
@@ -16,5 +16,6 @@ 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,
|
|
19
|
+
export type { Draft, MessageValue, PromiseResponseWrapper, TaskFunctions, WorkerAsyncFunction, WorkerFunction, WorkerSyncFunction } from './utility-types';
|
|
20
20
|
export type { CircularArray } from './circular-array';
|
|
21
|
+
export type { Queue } from './queue';
|
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;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;
|
|
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((()=>{})),c={medRunTime:!1},k=e=>"object"==typeof e&&null!==e&&e?.constructor===Object&&"[object Object]"===Object.prototype.toString.call(e),l=Object.freeze({SOFT:"SOFT",HARD:"HARD"});class d 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 p{items;head;tail;constructor(){this.items={},this.head=0,this.tail=0}get size(){return this.tail-this.head}enqueue(e){return this.items[this.tail]=e,this.tail++,this.size}dequeue(){if(this.size<=0)return;const e=this.items[this.head];return delete this.items[this.head],this.head++,this.head===this.tail&&(this.head=0,this.tail=0),e}}const m=Object.freeze({ROUND_ROBIN:"ROUND_ROBIN",LESS_USED:"LESS_USED",LESS_BUSY:"LESS_BUSY",FAIR_SHARE:"FAIR_SHARE",WEIGHTED_ROUND_ROBIN:"WEIGHTED_ROUND_ROBIN"});class g{pool;opts;toggleFindLastFreeWorkerNodeKey=!1;requiredStatistics={runTime:!1,avgRunTime:!1,medRunTime:!1};constructor(e,t=c){this.pool=e,this.opts=t,this.choose=this.choose.bind(this)}checkOptions(e){if(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),null!=e.weights&&Object.keys(e.weights).length<this.pool.size)throw new Error("Worker choice strategy options must have a weight for each worker node.")}setOptions(e){e=e??c,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 g{requiredStatistics={runTime:!0,avgRunTime:!0,medRunTime:!1};workersVirtualTaskTimestamp=[];constructor(e,t=c){super(e,t),this.checkOptions(this.opts)}reset(){return this.workersVirtualTaskTimestamp=[],!0}update(e){return this.computeWorkerVirtualTaskTimestamp(e),!0}choose(){let e,t=1/0;for(const[r]of this.pool.workerNodes.entries()){const s=this.workersVirtualTaskTimestamp[r]?.end??0;s<t&&(t=s,e=r)}return e}remove(e){return this.workersVirtualTaskTimestamp.splice(e,1),!0}computeWorkerVirtualTaskTimestamp(e){const t=Math.max(performance.now(),this.workersVirtualTaskTimestamp[e]?.end??-1/0),r=this.requiredStatistics.medRunTime?this.pool.workerNodes[e].tasksUsage.medRunTime:this.pool.workerNodes[e].tasksUsage.avgRunTime;this.workersVirtualTaskTimestamp[e]={start:t,end:t+r}}}class T extends g{requiredStatistics={runTime:!0,avgRunTime:!1,medRunTime:!1};constructor(e,t=c){super(e,t),this.checkOptions(this.opts)}reset(){return!0}update(){return!0}choose(){const e=this.findFreeWorkerNodeKey();if(-1!==e)return e;let t,r=1/0;for(const[e,s]of this.pool.workerNodes.entries()){const i=s.tasksUsage.runTime;if(0===i)return e;i<r&&(r=i,t=e)}return t}remove(){return!0}}class f extends g{constructor(e,t=c){super(e,t),this.checkOptions(this.opts)}reset(){return!0}update(){return!0}choose(){const e=this.findFreeWorkerNodeKey();if(-1!==e)return e;let t,r=1/0;for(const[e,s]of this.pool.workerNodes.entries()){const i=s.tasksUsage,o=i.run+i.running;if(0===o)return e;o<r&&(r=o,t=e)}return t}remove(){return!0}}class y extends g{nextWorkerNodeId=0;constructor(e,t=c){super(e,t),this.checkOptions(this.opts)}reset(){return this.nextWorkerNodeId=0,!0}update(){return!0}choose(){const e=this.nextWorkerNodeId;return this.nextWorkerNodeId=this.nextWorkerNodeId===this.pool.workerNodes.length-1?0:this.nextWorkerNodeId+1,e}remove(e){return this.nextWorkerNodeId===e&&(0===this.pool.workerNodes.length?this.nextWorkerNodeId=0:this.nextWorkerNodeId=this.nextWorkerNodeId>this.pool.workerNodes.length-1?this.pool.workerNodes.length-1:this.nextWorkerNodeId),!0}}class W extends g{requiredStatistics={runTime:!0,avgRunTime:!0,medRunTime:!1};currentWorkerNodeId=0;defaultWorkerWeight;workerVirtualTaskRunTime=0;constructor(e,t=c){super(e,t),this.checkOptions(this.opts),this.defaultWorkerWeight=this.computeDefaultWorkerWeight()}reset(){return this.currentWorkerNodeId=0,this.workerVirtualTaskRunTime=0,!0}update(){return!0}choose(){const e=this.currentWorkerNodeId,t=this.workerVirtualTaskRunTime;return t<(this.opts.weights?.[e]??this.defaultWorkerWeight)?this.workerVirtualTaskRunTime=t+this.getWorkerVirtualTaskRunTime(e):(this.currentWorkerNodeId=this.currentWorkerNodeId===this.pool.workerNodes.length-1?0:this.currentWorkerNodeId+1,this.workerVirtualTaskRunTime=0),e}remove(e){return this.currentWorkerNodeId===e&&(0===this.pool.workerNodes.length?this.currentWorkerNodeId=0:this.currentWorkerNodeId=this.currentWorkerNodeId>this.pool.workerNodes.length-1?this.pool.workerNodes.length-1:this.currentWorkerNodeId,this.workerVirtualTaskRunTime=0),!0}getWorkerVirtualTaskRunTime(e){return this.requiredStatistics.medRunTime?this.pool.workerNodes[e].tasksUsage.medRunTime:this.pool.workerNodes[e].tasksUsage.avgRunTime}computeDefaultWorkerWeight(){let e=0;for(const t of i.cpus()){const r=t.speed.toString().length-1;e+=1/(t.speed/Math.pow(10,r))*Math.pow(10,r)}return Math.round(e/i.cpus().length)}}class N{workerChoiceStrategy;workerChoiceStrategies;constructor(e,t=m.ROUND_ROBIN,r=c){this.workerChoiceStrategy=t,this.execute=this.execute.bind(this),this.workerChoiceStrategies=new Map([[m.ROUND_ROBIN,new(y.bind(this))(e,r)],[m.LESS_USED,new(f.bind(this))(e,r)],[m.LESS_BUSY,new(T.bind(this))(e,r)],[m.FAIR_SHARE,new(w.bind(this))(e,r)],[m.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()}update(e){return this.workerChoiceStrategies.get(this.workerChoiceStrategy).update(e)}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 S{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 N(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 safe 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){if(!k(e))throw new TypeError("Invalid pool options: must be a plain object");this.opts.workerChoiceStrategy=e.workerChoiceStrategy??m.ROUND_ROBIN,this.checkValidWorkerChoiceStrategy(this.opts.workerChoiceStrategy),this.opts.workerChoiceStrategyOptions=e.workerChoiceStrategyOptions??c,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(m).includes(e))throw new Error(`Invalid worker choice strategy '${e}'`)}checkValidWorkerChoiceStrategyOptions(e){if(!k(e))throw new TypeError("Invalid worker choice strategy options: must be a plain object")}checkValidTasksQueueOptions(e){if(null!=e&&!k(e))throw new TypeError("Invalid tasks queue options: must be a plain object");if(e?.concurrency<=0)throw new Error(`Invalid worker tasks concurrency '${e.concurrency}'`)}get 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.size),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 d,avgRunTime:0,medRunTime:0,error:0});this.workerChoiceStrategyContext.setWorkerChoiceStrategy(this.opts.workerChoiceStrategy),null!=t&&this.setWorkerChoiceStrategyOptions(t)}setWorkerChoiceStrategyOptions(e){this.checkValidWorkerChoiceStrategyOptions(e),this.opts.workerChoiceStrategyOptions=e,this.workerChoiceStrategyContext.setOptions(this.opts.workerChoiceStrategyOptions)}enableTasksQueue(e,t){!0!==this.opts.enableTasksQueue||e||this.flushTasksQueues(),this.opts.enableTasksQueue=e,this.setTasksQueueOptions(t)}setTasksQueueOptions(e){!0===this.opts.enableTasksQueue?(this.checkValidTasksQueueOptions(e),this.opts.tasksQueueOptions=this.buildTasksQueueOptions(e)):delete this.opts.tasksQueueOptions}buildTasksQueueOptions(e){return{concurrency:e?.concurrency??1}}internalBusy(){return-1===this.workerNodes.findIndex((e=>0===e.tasksUsage.running))}async execute(e,t){const[r,i]=this.chooseWorkerNode(),o={name:t,data:e??{},id:s.randomUUID()},n=new Promise(((e,t)=>{this.promiseResponseMap.set(o.id,{resolve:e,reject:t,worker:i.worker})}));return!0===this.opts.enableTasksQueue&&(this.busy||this.workerNodes[r].tasksUsage.running>=this.opts.tasksQueueOptions.concurrency)?this.enqueueTask(r,o):this.executeTask(r,o),this.checkAndEmitEvents(),n}async destroy(){await Promise.all(this.workerNodes.map((async(e,t)=>{this.flushTasksQueue(t),await this.destroyWorker(e.worker)})))}setupHook(){}beforeTaskExecutionHook(e){++this.workerNodes[e].tasksUsage.running}afterTaskExecutionHook(e,t){const r=this.getWorkerNodeKey(e),s=this.workerNodes[r].tasksUsage;--s.running,++s.run,null!=t.error&&++s.error,this.workerChoiceStrategyContext.getRequiredStatistics().runTime&&(s.runTime+=t.runTime??0,this.workerChoiceStrategyContext.getRequiredStatistics().avgRunTime&&0!==s.run&&(s.avgRunTime=s.runTime/s.run),this.workerChoiceStrategyContext.getRequiredStatistics().medRunTime&&(s.runTimeHistory.push(t.runTime??0),s.medRunTime=(e=>{if(Array.isArray(e)&&1===e.length)return e[0];const t=e.slice().sort(((e,t)=>e-t)),r=Math.floor(t.length/2);return t.length%2==0?t[r]:(t[r-1]+t[r])/2})(s.runTimeHistory))),this.workerChoiceStrategyContext.update(r)}chooseWorkerNode(){let t;if(this.type===e.DYNAMIC&&!this.full&&this.internalBusy()){const e=this.createAndSetupWorker();this.registerWorkerMessageListener(e,(t=>{const r=this.getWorkerNodeKey(e);var s;s=l.HARD,(t.kill===s||null!=t.kill&&0===this.workerNodes[r].tasksUsage.running)&&(this.flushTasksQueue(r),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}pushWorkerNode(e){return this.workerNodes.push({worker:e,tasksUsage:{run:0,running:0,runTime:0,runTimeHistory:new d,avgRunTime:0,medRunTime:0,error:0},tasksQueue:new p})}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.enqueue(t)}dequeueTask(e){return this.workerNodes[e].tasksQueue.dequeue()}tasksQueueSize(e){return this.workerNodes[e].tasksQueue.size}flushTasksQueue(e){if(this.tasksQueueSize(e)>0)for(let t=0;t<this.tasksQueueSize(e);t++)this.executeTask(e,this.dequeueTask(e))}flushTasksQueues(){for(const[e]of this.workerNodes.entries())this.flushTasksQueue(e)}}class b extends S{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 size(){return this.numberOfWorkers}get full(){return this.workerNodes.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}class x extends S{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 size(){return this.numberOfWorkers}get full(){return this.workerNodes.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}const O="default",R=6e4,v=l.SOFT;class C extends n.AsyncResource{isMain;mainWorker;opts;taskFunctions;lastTaskTimestamp;aliveInterval;constructor(e,t,r,s,i={killBehavior:v,maxInactiveTime:R}){super(e),this.isMain=t,this.mainWorker=s,this.opts=i,this.checkWorkerOptions(this.opts),this.checkTaskFunctions(r),this.isMain||(this.lastTaskTimestamp=performance.now(),this.aliveInterval=setInterval(this.checkAlive.bind(this),(this.opts.maxInactiveTime??R)/2),this.checkAlive.bind(this)()),this.mainWorker?.on("message",this.messageListener.bind(this))}checkWorkerOptions(e){this.opts.killBehavior=e.killBehavior??v,this.opts.maxInactiveTime=e.maxInactiveTime??R,this.opts.async=e.async??!1}checkTaskFunctions(e){if(null==e)throw new Error("taskFunctions parameter is mandatory");if(this.taskFunctions=new Map,"function"==typeof e)this.taskFunctions.set(O,e.bind(this));else{if(!k(e))throw new TypeError("taskFunctions parameter is not a function or a plain object");{let t=!0;for(const[r,s]of Object.entries(e)){if("function"!=typeof s)throw new TypeError("A taskFunctions parameter object value is not a function");this.taskFunctions.set(r,s.bind(this)),t&&(this.taskFunctions.set(O,s.bind(this)),t=!1)}if(t)throw new Error("taskFunctions parameter object is empty")}}}messageListener(e){if(null!=e.id&&null!=e.data){const t=this.getTaskFunction(e.name);"AsyncFunction"===t?.constructor.name?this.runInAsyncScope(this.runAsync.bind(this),this,t,e):this.runInAsyncScope(this.runSync.bind(this),this,t,e)}else null!=e.parent?this.mainWorker=e.parent:null!=e.kill&&(null!=this.aliveInterval&&clearInterval(this.aliveInterval),this.emitDestroy())}getMainWorker(){if(null==this.mainWorker)throw new Error("Main worker was not set");return this.mainWorker}checkAlive(){performance.now()-this.lastTaskTimestamp>(this.opts.maxInactiveTime??R)&&this.sendToMainWorker({kill:this.opts.killBehavior})}handleError(e){return e}runSync(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)}getTaskFunction(e){e=e??O;const t=this.taskFunctions.get(e);if(null==t)throw new Error(`Task function "${e}" not found`);return t}}exports.ClusterWorker=class extends C{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 b{max;constructor(e,t,r,s={}){super(e,r,s),this.max=t}get type(){return e.DYNAMIC}get size(){return this.max}get full(){return this.workerNodes.length===this.max}get busy(){return this.full&&this.internalBusy()}},exports.DynamicThreadPool=class extends x{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 size(){return this.max}get busy(){return this.full&&this.internalBusy()}},exports.FixedClusterPool=b,exports.FixedThreadPool=x,exports.KillBehaviors=l,exports.PoolEvents=h,exports.ThreadWorker=class extends C{constructor(e,t={}){super("worker-thread-pool:poolifier",o.isMainThread,e,o.parentPort,t)}sendToMainWorker(e){this.getMainWorker().postMessage(e)}},exports.WorkerChoiceStrategies=m;
|
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 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
|
+
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 c;!function(e){e.FIXED="fixed",e.DYNAMIC="dynamic"}(c||(c={}));class k extends e{}const l=Object.freeze({full:"full",busy:"busy"}),d=Object.freeze((()=>{})),p={medRunTime:!1},m=e=>"object"==typeof e&&null!==e&&e?.constructor===Object&&"[object Object]"===Object.prototype.toString.call(e),g=Object.freeze({SOFT:"SOFT",HARD:"HARD"});class w extends Array{size;constructor(e=1024,...t){super(),this.checkSize(e),this.size=e,arguments.length>1&&this.push(...t)}push(...e){const t=super.push(...e);return t>this.size&&super.splice(0,t-this.size),this.length}unshift(...e){return super.unshift(...e)>this.size&&super.splice(this.size,e.length),this.length}concat(...e){const t=super.concat(e);return t.size=this.size,t.length>t.size&&t.splice(0,t.length-t.size),t}splice(e,t,...r){let s;return arguments.length>=3&&void 0!==t?(s=super.splice(e,t),this.push(...r)):s=2===arguments.length?super.splice(e,t):super.splice(e),s}resize(e){if(this.checkSize(e),0===e)this.length=0;else if(e<this.size)for(let t=e;t<this.size;t++)super.pop();this.size=e}empty(){return 0===this.length}full(){return this.length===this.size}checkSize(e){if(!Number.isSafeInteger(e))throw new TypeError(`Invalid circular array size: ${e} is not a safe integer`);if(e<0)throw new RangeError(`Invalid circular array size: ${e} < 0`)}}class T{items;head;tail;constructor(){this.items={},this.head=0,this.tail=0}get size(){return this.tail-this.head}enqueue(e){return this.items[this.tail]=e,this.tail++,this.size}dequeue(){if(this.size<=0)return;const e=this.items[this.head];return delete this.items[this.head],this.head++,this.head===this.tail&&(this.head=0,this.tail=0),e}}const f=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 y{pool;opts;toggleFindLastFreeWorkerNodeKey=!1;requiredStatistics={runTime:!1,avgRunTime:!1,medRunTime:!1};constructor(e,t=p){this.pool=e,this.opts=t,this.choose=this.choose.bind(this)}checkOptions(e){if(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),null!=e.weights&&Object.keys(e.weights).length<this.pool.size)throw new Error("Worker choice strategy options must have a weight for each worker node.")}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 y{requiredStatistics={runTime:!0,avgRunTime:!0,medRunTime:!1};workersVirtualTaskTimestamp=[];constructor(e,t=p){super(e,t),this.checkOptions(this.opts)}reset(){return this.workersVirtualTaskTimestamp=[],!0}update(e){return this.computeWorkerVirtualTaskTimestamp(e),!0}choose(){let e,t=1/0;for(const[r]of this.pool.workerNodes.entries()){const s=this.workersVirtualTaskTimestamp[r]?.end??0;s<t&&(t=s,e=r)}return e}remove(e){return this.workersVirtualTaskTimestamp.splice(e,1),!0}computeWorkerVirtualTaskTimestamp(e){const t=Math.max(performance.now(),this.workersVirtualTaskTimestamp[e]?.end??-1/0),r=this.requiredStatistics.medRunTime?this.pool.workerNodes[e].tasksUsage.medRunTime:this.pool.workerNodes[e].tasksUsage.avgRunTime;this.workersVirtualTaskTimestamp[e]={start:t,end:t+r}}}class N extends y{requiredStatistics={runTime:!0,avgRunTime:!1,medRunTime:!1};constructor(e,t=p){super(e,t),this.checkOptions(this.opts)}reset(){return!0}update(){return!0}choose(){const e=this.findFreeWorkerNodeKey();if(-1!==e)return e;let t,r=1/0;for(const[e,s]of this.pool.workerNodes.entries()){const i=s.tasksUsage.runTime;if(0===i)return e;i<r&&(r=i,t=e)}return t}remove(){return!0}}class S extends y{constructor(e,t=p){super(e,t),this.checkOptions(this.opts)}reset(){return!0}update(){return!0}choose(){const e=this.findFreeWorkerNodeKey();if(-1!==e)return e;let t,r=1/0;for(const[e,s]of this.pool.workerNodes.entries()){const i=s.tasksUsage,o=i.run+i.running;if(0===o)return e;o<r&&(r=o,t=e)}return t}remove(){return!0}}class b extends y{nextWorkerNodeId=0;constructor(e,t=p){super(e,t),this.checkOptions(this.opts)}reset(){return this.nextWorkerNodeId=0,!0}update(){return!0}choose(){const e=this.nextWorkerNodeId;return this.nextWorkerNodeId=this.nextWorkerNodeId===this.pool.workerNodes.length-1?0:this.nextWorkerNodeId+1,e}remove(e){return this.nextWorkerNodeId===e&&(0===this.pool.workerNodes.length?this.nextWorkerNodeId=0:this.nextWorkerNodeId=this.nextWorkerNodeId>this.pool.workerNodes.length-1?this.pool.workerNodes.length-1:this.nextWorkerNodeId),!0}}class O extends y{requiredStatistics={runTime:!0,avgRunTime:!0,medRunTime:!1};currentWorkerNodeId=0;defaultWorkerWeight;workerVirtualTaskRunTime=0;constructor(e,t=p){super(e,t),this.checkOptions(this.opts),this.defaultWorkerWeight=this.computeDefaultWorkerWeight()}reset(){return this.currentWorkerNodeId=0,this.workerVirtualTaskRunTime=0,!0}update(){return!0}choose(){const e=this.currentWorkerNodeId,t=this.workerVirtualTaskRunTime;return t<(this.opts.weights?.[e]??this.defaultWorkerWeight)?this.workerVirtualTaskRunTime=t+this.getWorkerVirtualTaskRunTime(e):(this.currentWorkerNodeId=this.currentWorkerNodeId===this.pool.workerNodes.length-1?0:this.currentWorkerNodeId+1,this.workerVirtualTaskRunTime=0),e}remove(e){return this.currentWorkerNodeId===e&&(0===this.pool.workerNodes.length?this.currentWorkerNodeId=0:this.currentWorkerNodeId=this.currentWorkerNodeId>this.pool.workerNodes.length-1?this.pool.workerNodes.length-1:this.currentWorkerNodeId,this.workerVirtualTaskRunTime=0),!0}getWorkerVirtualTaskRunTime(e){return this.requiredStatistics.medRunTime?this.pool.workerNodes[e].tasksUsage.medRunTime:this.pool.workerNodes[e].tasksUsage.avgRunTime}computeDefaultWorkerWeight(){let e=0;for(const t of s()){const r=t.speed.toString().length-1;e+=1/(t.speed/Math.pow(10,r))*Math.pow(10,r)}return Math.round(e/s().length)}}class R{workerChoiceStrategy;workerChoiceStrategies;constructor(e,t=f.ROUND_ROBIN,r=p){this.workerChoiceStrategy=t,this.execute=this.execute.bind(this),this.workerChoiceStrategies=new Map([[f.ROUND_ROBIN,new(b.bind(this))(e,r)],[f.LESS_USED,new(S.bind(this))(e,r)],[f.LESS_BUSY,new(N.bind(this))(e,r)],[f.FAIR_SHARE,new(W.bind(this))(e,r)],[f.WEIGHTED_ROUND_ROBIN,new(O.bind(this))(e,r)]])}getRequiredStatistics(){return this.workerChoiceStrategies.get(this.workerChoiceStrategy).requiredStatistics}setWorkerChoiceStrategy(e){this.workerChoiceStrategy!==e&&(this.workerChoiceStrategy=e),this.workerChoiceStrategies.get(this.workerChoiceStrategy)?.reset()}update(e){return this.workerChoiceStrategies.get(this.workerChoiceStrategy).update(e)}execute(){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 x{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 k),this.workerChoiceStrategyContext=new R(this,this.opts.workerChoiceStrategy,this.opts.workerChoiceStrategyOptions)}checkFilePath(e){if(null==e||"string"==typeof e&&0===e.trim().length)throw new Error("Please specify a file with a worker implementation")}checkNumberOfWorkers(e){if(null==e)throw new Error("Cannot instantiate a pool without specifying the number of workers");if(!Number.isSafeInteger(e))throw new TypeError("Cannot instantiate a pool with a non safe integer number of workers");if(e<0)throw new RangeError("Cannot instantiate a pool with a negative number of workers");if(this.type===c.FIXED&&0===e)throw new Error("Cannot instantiate a fixed pool with no worker")}checkPoolOptions(e){if(!m(e))throw new TypeError("Invalid pool options: must be a plain object");this.opts.workerChoiceStrategy=e.workerChoiceStrategy??f.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(f).includes(e))throw new Error(`Invalid worker choice strategy '${e}'`)}checkValidWorkerChoiceStrategyOptions(e){if(!m(e))throw new TypeError("Invalid worker choice strategy options: must be a plain object")}checkValidTasksQueueOptions(e){if(null!=e&&!m(e))throw new TypeError("Invalid tasks queue options: must be a plain object");if(e?.concurrency<=0)throw new Error(`Invalid worker tasks concurrency '${e.concurrency}'`)}get 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.size),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.checkValidWorkerChoiceStrategyOptions(e),this.opts.workerChoiceStrategyOptions=e,this.workerChoiceStrategyContext.setOptions(this.opts.workerChoiceStrategyOptions)}enableTasksQueue(e,t){!0!==this.opts.enableTasksQueue||e||this.flushTasksQueues(),this.opts.enableTasksQueue=e,this.setTasksQueueOptions(t)}setTasksQueueOptions(e){!0===this.opts.enableTasksQueue?(this.checkValidTasksQueueOptions(e),this.opts.tasksQueueOptions=this.buildTasksQueueOptions(e)):delete this.opts.tasksQueueOptions}buildTasksQueueOptions(e){return{concurrency:e?.concurrency??1}}internalBusy(){return-1===this.workerNodes.findIndex((e=>0===e.tasksUsage.running))}async execute(e,t){const[s,i]=this.chooseWorkerNode(),o={name:t,data:e??{},id:r.randomUUID()},n=new Promise(((e,t)=>{this.promiseResponseMap.set(o.id,{resolve:e,reject:t,worker:i.worker})}));return!0===this.opts.enableTasksQueue&&(this.busy||this.workerNodes[s].tasksUsage.running>=this.opts.tasksQueueOptions.concurrency)?this.enqueueTask(s,o):this.executeTask(s,o),this.checkAndEmitEvents(),n}async destroy(){await Promise.all(this.workerNodes.map((async(e,t)=>{this.flushTasksQueue(t),await this.destroyWorker(e.worker)})))}setupHook(){}beforeTaskExecutionHook(e){++this.workerNodes[e].tasksUsage.running}afterTaskExecutionHook(e,t){const r=this.getWorkerNodeKey(e),s=this.workerNodes[r].tasksUsage;--s.running,++s.run,null!=t.error&&++s.error,this.workerChoiceStrategyContext.getRequiredStatistics().runTime&&(s.runTime+=t.runTime??0,this.workerChoiceStrategyContext.getRequiredStatistics().avgRunTime&&0!==s.run&&(s.avgRunTime=s.runTime/s.run),this.workerChoiceStrategyContext.getRequiredStatistics().medRunTime&&(s.runTimeHistory.push(t.runTime??0),s.medRunTime=(e=>{if(Array.isArray(e)&&1===e.length)return e[0];const t=e.slice().sort(((e,t)=>e-t)),r=Math.floor(t.length/2);return t.length%2==0?t[r]:(t[r-1]+t[r])/2})(s.runTimeHistory))),this.workerChoiceStrategyContext.update(r)}chooseWorkerNode(){let e;if(this.type===c.DYNAMIC&&!this.full&&this.internalBusy()){const t=this.createAndSetupWorker();this.registerWorkerMessageListener(t,(e=>{const r=this.getWorkerNodeKey(t);var s;s=g.HARD,(e.kill===s||null!=e.kill&&0===this.workerNodes[r].tasksUsage.running)&&(this.flushTasksQueue(r),this.destroyWorker(t))})),e=this.getWorkerNodeKey(t)}else e=this.workerChoiceStrategyContext.execute();return[e,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===c.DYNAMIC&&this.full&&this.emitter?.emit(l.full))}setWorkerNodeTasksUsage(e,t){e.tasksUsage=t}pushWorkerNode(e){return this.workerNodes.push({worker:e,tasksUsage:{run:0,running:0,runTime:0,runTimeHistory:new w,avgRunTime:0,medRunTime:0,error:0},tasksQueue:new T})}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.enqueue(t)}dequeueTask(e){return this.workerNodes[e].tasksQueue.dequeue()}tasksQueueSize(e){return this.workerNodes[e].tasksQueue.size}flushTasksQueue(e){if(this.tasksQueueSize(e)>0)for(let t=0;t<this.tasksQueueSize(e);t++)this.executeTask(e,this.dequeueTask(e))}flushTasksQueues(){for(const[e]of this.workerNodes.entries())this.flushTasksQueue(e)}}class v extends x{opts;constructor(e,t,r={}){super(e,t,r),this.opts=r}setupHook(){t.setupPrimary({...this.opts.settings,exec:this.filePath})}isMain(){return t.isPrimary}destroyWorker(e){this.sendToWorker(e,{kill:1}),e.kill()}sendToWorker(e,t){e.send(t)}registerWorkerMessageListener(e,t){e.on("message",t)}createWorker(){return t.fork(this.opts.env)}afterWorkerSetup(e){this.registerWorkerMessageListener(e,super.workerListener())}get type(){return c.FIXED}get size(){return this.numberOfWorkers}get full(){return this.workerNodes.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}class I extends v{max;constructor(e,t,r,s={}){super(e,r,s),this.max=t}get type(){return c.DYNAMIC}get size(){return this.max}get full(){return this.workerNodes.length===this.max}get busy(){return this.full&&this.internalBusy()}}class C extends x{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 c.FIXED}get size(){return this.numberOfWorkers}get full(){return this.workerNodes.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}class E extends C{max;constructor(e,t,r,s={}){super(e,r,s),this.max=t}get type(){return c.DYNAMIC}get full(){return this.workerNodes.length===this.max}get size(){return this.max}get busy(){return this.full&&this.internalBusy()}}const F="default",M=6e4,Q=g.SOFT;class z extends u{isMain;mainWorker;opts;taskFunctions;lastTaskTimestamp;aliveInterval;constructor(e,t,r,s,i={killBehavior:Q,maxInactiveTime:M}){super(e),this.isMain=t,this.mainWorker=s,this.opts=i,this.checkWorkerOptions(this.opts),this.checkTaskFunctions(r),this.isMain||(this.lastTaskTimestamp=performance.now(),this.aliveInterval=setInterval(this.checkAlive.bind(this),(this.opts.maxInactiveTime??M)/2),this.checkAlive.bind(this)()),this.mainWorker?.on("message",this.messageListener.bind(this))}checkWorkerOptions(e){this.opts.killBehavior=e.killBehavior??Q,this.opts.maxInactiveTime=e.maxInactiveTime??M,this.opts.async=e.async??!1}checkTaskFunctions(e){if(null==e)throw new Error("taskFunctions parameter is mandatory");if(this.taskFunctions=new Map,"function"==typeof e)this.taskFunctions.set(F,e.bind(this));else{if(!m(e))throw new TypeError("taskFunctions parameter is not a function or a plain object");{let t=!0;for(const[r,s]of Object.entries(e)){if("function"!=typeof s)throw new TypeError("A taskFunctions parameter object value is not a function");this.taskFunctions.set(r,s.bind(this)),t&&(this.taskFunctions.set(F,s.bind(this)),t=!1)}if(t)throw new Error("taskFunctions parameter object is empty")}}}messageListener(e){if(null!=e.id&&null!=e.data){const t=this.getTaskFunction(e.name);"AsyncFunction"===t?.constructor.name?this.runInAsyncScope(this.runAsync.bind(this),this,t,e):this.runInAsyncScope(this.runSync.bind(this),this,t,e)}else null!=e.parent?this.mainWorker=e.parent:null!=e.kill&&(null!=this.aliveInterval&&clearInterval(this.aliveInterval),this.emitDestroy())}getMainWorker(){if(null==this.mainWorker)throw new Error("Main worker was not set");return this.mainWorker}checkAlive(){performance.now()-this.lastTaskTimestamp>(this.opts.maxInactiveTime??M)&&this.sendToMainWorker({kill:this.opts.killBehavior})}handleError(e){return e}runSync(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)}getTaskFunction(e){e=e??F;const t=this.taskFunctions.get(e);if(null==t)throw new Error(`Task function "${e}" not found`);return t}}class U extends z{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 A extends z{constructor(e,t={}){super("worker-thread-pool:poolifier",i,e,a,t)}sendToMainWorker(e){this.getMainWorker().postMessage(e)}}export{U as ClusterWorker,I as DynamicClusterPool,E as DynamicThreadPool,v as FixedClusterPool,C as FixedThreadPool,g as KillBehaviors,l as PoolEvents,A as ThreadWorker,f as WorkerChoiceStrategies};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MessageValue, PromiseResponseWrapper } from '../utility-types';
|
|
2
|
-
import {
|
|
2
|
+
import { type IPool, PoolEmitter, type PoolOptions, PoolType, type TasksQueueOptions } from './pool';
|
|
3
3
|
import type { IWorker, WorkerNode } from './worker';
|
|
4
4
|
import { type WorkerChoiceStrategy, type WorkerChoiceStrategyOptions } from './selection-strategies/selection-strategies-types';
|
|
5
5
|
import { WorkerChoiceStrategyContext } from './selection-strategies/worker-choice-strategy-context';
|
|
@@ -45,9 +45,12 @@ export declare abstract class AbstractPool<Worker extends IWorker, Data = unknow
|
|
|
45
45
|
private checkNumberOfWorkers;
|
|
46
46
|
private checkPoolOptions;
|
|
47
47
|
private checkValidWorkerChoiceStrategy;
|
|
48
|
+
private checkValidWorkerChoiceStrategyOptions;
|
|
48
49
|
private checkValidTasksQueueOptions;
|
|
49
50
|
/** @inheritDoc */
|
|
50
51
|
abstract get type(): PoolType;
|
|
52
|
+
/** @inheritDoc */
|
|
53
|
+
abstract get size(): number;
|
|
51
54
|
/**
|
|
52
55
|
* Number of tasks running in the pool.
|
|
53
56
|
*/
|
|
@@ -86,7 +89,7 @@ export declare abstract class AbstractPool<Worker extends IWorker, Data = unknow
|
|
|
86
89
|
protected abstract get busy(): boolean;
|
|
87
90
|
protected internalBusy(): boolean;
|
|
88
91
|
/** @inheritDoc */
|
|
89
|
-
execute(data?: Data): Promise<Response>;
|
|
92
|
+
execute(data?: Data, name?: string): Promise<Response>;
|
|
90
93
|
/** @inheritDoc */
|
|
91
94
|
destroy(): Promise<void>;
|
|
92
95
|
/**
|
|
@@ -175,14 +178,6 @@ export declare abstract class AbstractPool<Worker extends IWorker, Data = unknow
|
|
|
175
178
|
* @param tasksUsage - The worker node tasks usage.
|
|
176
179
|
*/
|
|
177
180
|
private setWorkerNodeTasksUsage;
|
|
178
|
-
/**
|
|
179
|
-
* Gets the given worker its tasks usage in the pool.
|
|
180
|
-
*
|
|
181
|
-
* @param worker - The worker.
|
|
182
|
-
* @throws Error if the worker is not found in the pool worker nodes.
|
|
183
|
-
* @returns The worker tasks usage.
|
|
184
|
-
*/
|
|
185
|
-
private getWorkerTasksUsage;
|
|
186
181
|
/**
|
|
187
182
|
* Pushes the given worker in the pool worker nodes.
|
|
188
183
|
*
|
|
@@ -210,6 +205,5 @@ export declare abstract class AbstractPool<Worker extends IWorker, Data = unknow
|
|
|
210
205
|
private dequeueTask;
|
|
211
206
|
private tasksQueueSize;
|
|
212
207
|
private flushTasksQueue;
|
|
213
|
-
private flushTasksQueueByWorker;
|
|
214
208
|
private flushTasksQueues;
|
|
215
209
|
}
|
|
@@ -26,6 +26,8 @@ export declare class DynamicClusterPool<Data = unknown, Response = unknown> exte
|
|
|
26
26
|
/** @inheritDoc */
|
|
27
27
|
get type(): PoolType;
|
|
28
28
|
/** @inheritDoc */
|
|
29
|
+
get size(): number;
|
|
30
|
+
/** @inheritDoc */
|
|
29
31
|
protected get full(): boolean;
|
|
30
32
|
/** @inheritDoc */
|
|
31
33
|
protected get busy(): boolean;
|
|
@@ -60,6 +60,8 @@ export declare class FixedClusterPool<Data = unknown, Response = unknown> extend
|
|
|
60
60
|
/** @inheritDoc */
|
|
61
61
|
get type(): PoolType;
|
|
62
62
|
/** @inheritDoc */
|
|
63
|
+
get size(): number;
|
|
64
|
+
/** @inheritDoc */
|
|
63
65
|
protected get full(): boolean;
|
|
64
66
|
/** @inheritDoc */
|
|
65
67
|
protected get busy(): boolean;
|
package/lib/pools/pool.d.ts
CHANGED
|
@@ -108,6 +108,10 @@ export interface IPool<Worker extends IWorker, Data = unknown, Response = unknow
|
|
|
108
108
|
* If it is `'dynamic'`, it provides the `max` property.
|
|
109
109
|
*/
|
|
110
110
|
readonly type: PoolType;
|
|
111
|
+
/**
|
|
112
|
+
* Pool maximum size.
|
|
113
|
+
*/
|
|
114
|
+
readonly size: number;
|
|
111
115
|
/**
|
|
112
116
|
* Pool worker nodes.
|
|
113
117
|
*/
|
|
@@ -125,9 +129,10 @@ export interface IPool<Worker extends IWorker, Data = unknown, Response = unknow
|
|
|
125
129
|
* Executes the function specified in the worker constructor with the task data input parameter.
|
|
126
130
|
*
|
|
127
131
|
* @param data - The task input data for the specified worker function. This can only be serializable data.
|
|
132
|
+
* @param name - The name of the worker function to execute. If not specified, the default worker function will be executed.
|
|
128
133
|
* @returns Promise that will be fulfilled when the task is completed.
|
|
129
134
|
*/
|
|
130
|
-
execute: (data?: Data) => Promise<Response>;
|
|
135
|
+
execute: (data?: Data, name?: string) => Promise<Response>;
|
|
131
136
|
/**
|
|
132
137
|
* Shutdowns every current worker in this pool.
|
|
133
138
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { IPool } from '../pool';
|
|
2
2
|
import type { IWorker } from '../worker';
|
|
3
3
|
import type { IWorkerChoiceStrategy, RequiredStatistics, WorkerChoiceStrategyOptions } from './selection-strategies-types';
|
|
4
4
|
/**
|
|
@@ -16,8 +16,6 @@ export declare abstract class AbstractWorkerChoiceStrategy<Worker extends IWorke
|
|
|
16
16
|
*/
|
|
17
17
|
private toggleFindLastFreeWorkerNodeKey;
|
|
18
18
|
/** @inheritDoc */
|
|
19
|
-
protected readonly isDynamicPool: boolean;
|
|
20
|
-
/** @inheritDoc */
|
|
21
19
|
readonly requiredStatistics: RequiredStatistics;
|
|
22
20
|
/**
|
|
23
21
|
* Constructs a worker choice strategy bound to the pool.
|
|
@@ -30,6 +28,8 @@ export declare abstract class AbstractWorkerChoiceStrategy<Worker extends IWorke
|
|
|
30
28
|
/** @inheritDoc */
|
|
31
29
|
abstract reset(): boolean;
|
|
32
30
|
/** @inheritDoc */
|
|
31
|
+
abstract update(workerNodeKey: number): boolean;
|
|
32
|
+
/** @inheritDoc */
|
|
33
33
|
abstract choose(): number;
|
|
34
34
|
/** @inheritDoc */
|
|
35
35
|
abstract remove(workerNodeKey: number): boolean;
|
|
@@ -14,21 +14,23 @@ export declare class FairShareWorkerChoiceStrategy<Worker extends IWorker, Data
|
|
|
14
14
|
/** @inheritDoc */
|
|
15
15
|
readonly requiredStatistics: RequiredStatistics;
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
17
|
+
* Workers' virtual task execution timestamp.
|
|
18
18
|
*/
|
|
19
|
-
private
|
|
19
|
+
private workersVirtualTaskTimestamp;
|
|
20
20
|
/** @inheritDoc */
|
|
21
21
|
constructor(pool: IPool<Worker, Data, Response>, opts?: WorkerChoiceStrategyOptions);
|
|
22
22
|
/** @inheritDoc */
|
|
23
23
|
reset(): boolean;
|
|
24
24
|
/** @inheritDoc */
|
|
25
|
+
update(workerNodeKey: number): boolean;
|
|
26
|
+
/** @inheritDoc */
|
|
25
27
|
choose(): number;
|
|
26
28
|
/** @inheritDoc */
|
|
27
29
|
remove(workerNodeKey: number): boolean;
|
|
28
30
|
/**
|
|
29
|
-
* Computes worker
|
|
31
|
+
* Computes worker virtual task timestamp.
|
|
30
32
|
*
|
|
31
33
|
* @param workerNodeKey - The worker node key.
|
|
32
34
|
*/
|
|
33
|
-
private
|
|
35
|
+
private computeWorkerVirtualTaskTimestamp;
|
|
34
36
|
}
|
|
@@ -17,7 +17,9 @@ export declare class LessBusyWorkerChoiceStrategy<Worker extends IWorker, Data =
|
|
|
17
17
|
/** @inheritDoc */
|
|
18
18
|
reset(): boolean;
|
|
19
19
|
/** @inheritDoc */
|
|
20
|
+
update(): boolean;
|
|
21
|
+
/** @inheritDoc */
|
|
20
22
|
choose(): number;
|
|
21
23
|
/** @inheritDoc */
|
|
22
|
-
remove(
|
|
24
|
+
remove(): boolean;
|
|
23
25
|
}
|
|
@@ -15,7 +15,9 @@ export declare class LessUsedWorkerChoiceStrategy<Worker extends IWorker, Data =
|
|
|
15
15
|
/** @inheritDoc */
|
|
16
16
|
reset(): boolean;
|
|
17
17
|
/** @inheritDoc */
|
|
18
|
+
update(): boolean;
|
|
19
|
+
/** @inheritDoc */
|
|
18
20
|
choose(): number;
|
|
19
21
|
/** @inheritDoc */
|
|
20
|
-
remove(
|
|
22
|
+
remove(): boolean;
|
|
21
23
|
}
|
|
@@ -19,6 +19,8 @@ export declare class RoundRobinWorkerChoiceStrategy<Worker extends IWorker, Data
|
|
|
19
19
|
/** @inheritDoc */
|
|
20
20
|
reset(): boolean;
|
|
21
21
|
/** @inheritDoc */
|
|
22
|
+
update(): boolean;
|
|
23
|
+
/** @inheritDoc */
|
|
22
24
|
choose(): number;
|
|
23
25
|
/** @inheritDoc */
|
|
24
26
|
remove(workerNodeKey: number): boolean;
|
|
@@ -37,6 +37,13 @@ export interface WorkerChoiceStrategyOptions {
|
|
|
37
37
|
* @defaultValue false
|
|
38
38
|
*/
|
|
39
39
|
medRunTime?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Worker weights to use for weighted round robin worker selection strategy.
|
|
42
|
+
* Weight is the tasks maximum average or median runtime in milliseconds.
|
|
43
|
+
*
|
|
44
|
+
* @defaultValue Computed worker weights automatically given the CPU performance.
|
|
45
|
+
*/
|
|
46
|
+
weights?: Record<number, number>;
|
|
40
47
|
}
|
|
41
48
|
/**
|
|
42
49
|
* Pool worker tasks usage statistics requirements.
|
|
@@ -66,17 +73,28 @@ export interface IWorkerChoiceStrategy {
|
|
|
66
73
|
*/
|
|
67
74
|
readonly requiredStatistics: RequiredStatistics;
|
|
68
75
|
/**
|
|
69
|
-
* Resets strategy internals
|
|
76
|
+
* Resets strategy internals.
|
|
77
|
+
*
|
|
78
|
+
* @returns `true` if the reset is successful, `false` otherwise.
|
|
70
79
|
*/
|
|
71
80
|
reset: () => boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Updates the worker node key strategy internals.
|
|
83
|
+
*
|
|
84
|
+
* @returns `true` if the update is successful, `false` otherwise.
|
|
85
|
+
*/
|
|
86
|
+
update: (workerNodeKey: number) => boolean;
|
|
72
87
|
/**
|
|
73
88
|
* Chooses a worker node in the pool and returns its key.
|
|
89
|
+
*
|
|
90
|
+
* @returns The worker node key.
|
|
74
91
|
*/
|
|
75
92
|
choose: () => number;
|
|
76
93
|
/**
|
|
77
|
-
* Removes
|
|
94
|
+
* Removes the worker node key from strategy internals.
|
|
78
95
|
*
|
|
79
96
|
* @param workerNodeKey - The worker node key.
|
|
97
|
+
* @returns `true` if the worker node key is removed, `false` otherwise.
|
|
80
98
|
*/
|
|
81
99
|
remove: (workerNodeKey: number) => boolean;
|
|
82
100
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { IWorker } from '../worker';
|
|
2
|
+
import type { IPool } from '../pool';
|
|
2
3
|
import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy';
|
|
3
4
|
import type { IWorkerChoiceStrategy, RequiredStatistics, WorkerChoiceStrategyOptions } from './selection-strategies-types';
|
|
4
|
-
import type { IPool } from '../pool';
|
|
5
5
|
/**
|
|
6
6
|
* Selects the next worker with a weighted round robin scheduling algorithm.
|
|
7
7
|
* Loosely modeled after the weighted round robin queueing algorithm: https://en.wikipedia.org/wiki/Weighted_round_robin.
|
|
@@ -22,20 +22,19 @@ export declare class WeightedRoundRobinWorkerChoiceStrategy<Worker extends IWork
|
|
|
22
22
|
*/
|
|
23
23
|
private readonly defaultWorkerWeight;
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* Worker virtual task runtime.
|
|
26
26
|
*/
|
|
27
|
-
private
|
|
27
|
+
private workerVirtualTaskRunTime;
|
|
28
28
|
/** @inheritDoc */
|
|
29
29
|
constructor(pool: IPool<Worker, Data, Response>, opts?: WorkerChoiceStrategyOptions);
|
|
30
30
|
/** @inheritDoc */
|
|
31
31
|
reset(): boolean;
|
|
32
32
|
/** @inheritDoc */
|
|
33
|
+
update(): boolean;
|
|
34
|
+
/** @inheritDoc */
|
|
33
35
|
choose(): number;
|
|
34
36
|
/** @inheritDoc */
|
|
35
37
|
remove(workerNodeKey: number): boolean;
|
|
36
|
-
private initWorkersTaskRunTime;
|
|
37
|
-
private initWorkerTaskRunTime;
|
|
38
|
-
private setWorkerTaskRunTime;
|
|
39
38
|
private getWorkerVirtualTaskRunTime;
|
|
40
|
-
private
|
|
39
|
+
private computeDefaultWorkerWeight;
|
|
41
40
|
}
|
|
@@ -31,6 +31,12 @@ export declare class WorkerChoiceStrategyContext<Worker extends IWorker, Data =
|
|
|
31
31
|
* @param workerChoiceStrategy - The worker choice strategy to set.
|
|
32
32
|
*/
|
|
33
33
|
setWorkerChoiceStrategy(workerChoiceStrategy: WorkerChoiceStrategy): void;
|
|
34
|
+
/**
|
|
35
|
+
* Updates the worker node key in the worker choice strategy internals in the context.
|
|
36
|
+
*
|
|
37
|
+
* @returns `true` if the update is successful, `false` otherwise.
|
|
38
|
+
*/
|
|
39
|
+
update(workerNodeKey: number): boolean;
|
|
34
40
|
/**
|
|
35
41
|
* Executes the worker choice strategy algorithm in the context.
|
|
36
42
|
*
|
|
@@ -38,7 +44,7 @@ export declare class WorkerChoiceStrategyContext<Worker extends IWorker, Data =
|
|
|
38
44
|
*/
|
|
39
45
|
execute(): number;
|
|
40
46
|
/**
|
|
41
|
-
* Removes
|
|
47
|
+
* Removes the worker node key from the worker choice strategy in the context.
|
|
42
48
|
*
|
|
43
49
|
* @param workerNodeKey - The key of the worker node.
|
|
44
50
|
* @returns `true` if the removal is successful, `false` otherwise.
|
|
@@ -44,6 +44,8 @@ export declare class FixedThreadPool<Data = unknown, Response = unknown> extends
|
|
|
44
44
|
/** @inheritDoc */
|
|
45
45
|
get type(): PoolType;
|
|
46
46
|
/** @inheritDoc */
|
|
47
|
+
get size(): number;
|
|
48
|
+
/** @inheritDoc */
|
|
47
49
|
protected get full(): boolean;
|
|
48
50
|
/** @inheritDoc */
|
|
49
51
|
protected get busy(): boolean;
|
package/lib/pools/worker.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CircularArray } from '../circular-array';
|
|
2
|
+
import type { Queue } from '../queue';
|
|
2
3
|
/**
|
|
3
4
|
* Callback invoked if the worker has received a message.
|
|
4
5
|
*/
|
|
@@ -22,12 +23,16 @@ export type ExitHandler<Worker extends IWorker> = (this: Worker, code: number) =
|
|
|
22
23
|
* @internal
|
|
23
24
|
*/
|
|
24
25
|
export interface Task<Data = unknown> {
|
|
26
|
+
/**
|
|
27
|
+
* Task name.
|
|
28
|
+
*/
|
|
29
|
+
readonly name?: string;
|
|
25
30
|
/**
|
|
26
31
|
* Task input data that will be passed to the worker.
|
|
27
32
|
*/
|
|
28
33
|
readonly data?: Data;
|
|
29
34
|
/**
|
|
30
|
-
* UUID
|
|
35
|
+
* Message UUID.
|
|
31
36
|
*/
|
|
32
37
|
readonly id?: string;
|
|
33
38
|
}
|
|
@@ -104,5 +109,5 @@ export interface WorkerNode<Worker extends IWorker, Data = unknown> {
|
|
|
104
109
|
/**
|
|
105
110
|
* Worker node tasks queue.
|
|
106
111
|
*/
|
|
107
|
-
readonly tasksQueue:
|
|
112
|
+
readonly tasksQueue: Queue<Task<Data>>;
|
|
108
113
|
}
|
package/lib/queue.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue
|
|
3
|
+
*
|
|
4
|
+
* @typeParam T - Type of queue items.
|
|
5
|
+
*/
|
|
6
|
+
export declare class Queue<T> {
|
|
7
|
+
private items;
|
|
8
|
+
private head;
|
|
9
|
+
private tail;
|
|
10
|
+
constructor();
|
|
11
|
+
/**
|
|
12
|
+
* Get the size of the queue.
|
|
13
|
+
*
|
|
14
|
+
* @returns The size of the queue.
|
|
15
|
+
* @readonly
|
|
16
|
+
*/
|
|
17
|
+
get size(): number;
|
|
18
|
+
/**
|
|
19
|
+
* Enqueue an item.
|
|
20
|
+
*
|
|
21
|
+
* @param item - Item to enqueue.
|
|
22
|
+
* @returns The new size of the queue.
|
|
23
|
+
*/
|
|
24
|
+
enqueue(item: T): number;
|
|
25
|
+
/**
|
|
26
|
+
* Dequeue an item.
|
|
27
|
+
*
|
|
28
|
+
* @returns The dequeued item or `undefined` if the queue is empty.
|
|
29
|
+
*/
|
|
30
|
+
dequeue(): T | undefined;
|
|
31
|
+
}
|
package/lib/utility-types.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export type Draft<T> = {
|
|
|
19
19
|
* @typeParam MainWorker - Type of main worker.
|
|
20
20
|
* @internal
|
|
21
21
|
*/
|
|
22
|
-
export interface MessageValue<Data = unknown, MainWorker extends ClusterWorker | MessagePort
|
|
22
|
+
export interface MessageValue<Data = unknown, MainWorker extends ClusterWorker | MessagePort = ClusterWorker | MessagePort> extends Task<Data> {
|
|
23
23
|
/**
|
|
24
24
|
* Kill code.
|
|
25
25
|
*/
|
|
@@ -60,6 +60,16 @@ export type WorkerAsyncFunction<Data = unknown, Response = unknown> = (data?: Da
|
|
|
60
60
|
* @typeParam Response - Type of execution response. This can only be serializable data.
|
|
61
61
|
*/
|
|
62
62
|
export type WorkerFunction<Data = unknown, Response = unknown> = WorkerSyncFunction<Data, Response> | WorkerAsyncFunction<Data, Response>;
|
|
63
|
+
/**
|
|
64
|
+
* Worker functions that can be executed.
|
|
65
|
+
* This object can contain synchronous or asynchronous functions.
|
|
66
|
+
* The key is the name of the function.
|
|
67
|
+
* The value is the function itself.
|
|
68
|
+
*
|
|
69
|
+
* @typeParam Data - Type of data sent to the worker. This can only be serializable data.
|
|
70
|
+
* @typeParam Response - Type of execution response. This can only be serializable data.
|
|
71
|
+
*/
|
|
72
|
+
export type TaskFunctions<Data = unknown, Response = unknown> = Record<string, WorkerFunction<Data, Response>>;
|
|
63
73
|
/**
|
|
64
74
|
* An object holding the execution response promise resolve/reject callbacks.
|
|
65
75
|
*
|
package/lib/utils.d.ts
CHANGED
|
@@ -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, WorkerAsyncFunction, WorkerFunction, WorkerSyncFunction } from '../utility-types';
|
|
8
|
+
import type { MessageValue, TaskFunctions, 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.
|
|
@@ -18,6 +18,10 @@ export declare abstract class AbstractWorker<MainWorker extends Worker | Message
|
|
|
18
18
|
protected readonly isMain: boolean;
|
|
19
19
|
protected mainWorker: MainWorker | undefined | null;
|
|
20
20
|
protected readonly opts: WorkerOptions;
|
|
21
|
+
/**
|
|
22
|
+
* Task function(s) processed by the worker when the pool's `execution` function is invoked.
|
|
23
|
+
*/
|
|
24
|
+
protected taskFunctions: Map<string, WorkerFunction<Data, Response>>;
|
|
21
25
|
/**
|
|
22
26
|
* Timestamp of the last task processed by this worker.
|
|
23
27
|
*/
|
|
@@ -31,25 +35,24 @@ export declare abstract class AbstractWorker<MainWorker extends Worker | Message
|
|
|
31
35
|
*
|
|
32
36
|
* @param type - The type of async event.
|
|
33
37
|
* @param isMain - Whether this is the main worker or not.
|
|
34
|
-
* @param
|
|
38
|
+
* @param taskFunctions - Task function(s) processed by the worker when the pool's `execution` function is invoked. The first function is the default function.
|
|
35
39
|
* @param mainWorker - Reference to main worker.
|
|
36
40
|
* @param opts - Options for the worker.
|
|
37
41
|
*/
|
|
38
|
-
constructor(type: string, isMain: boolean,
|
|
42
|
+
constructor(type: string, isMain: boolean, taskFunctions: WorkerFunction<Data, Response> | TaskFunctions<Data, Response>, mainWorker: MainWorker | undefined | null, opts?: WorkerOptions);
|
|
39
43
|
private checkWorkerOptions;
|
|
40
44
|
/**
|
|
41
|
-
* Checks if the `
|
|
45
|
+
* Checks if the `taskFunctions` parameter is passed to the constructor.
|
|
42
46
|
*
|
|
43
|
-
* @param
|
|
47
|
+
* @param taskFunctions - The task function(s) parameter that should be checked.
|
|
44
48
|
*/
|
|
45
|
-
private
|
|
49
|
+
private checkTaskFunctions;
|
|
46
50
|
/**
|
|
47
51
|
* Worker message listener.
|
|
48
52
|
*
|
|
49
53
|
* @param message - Message received.
|
|
50
|
-
* @param fn - Function processed by the worker when the pool's `execution` function is invoked.
|
|
51
54
|
*/
|
|
52
|
-
protected messageListener(message: MessageValue<Data, MainWorker
|
|
55
|
+
protected messageListener(message: MessageValue<Data, MainWorker>): void;
|
|
53
56
|
/**
|
|
54
57
|
* Returns the main worker.
|
|
55
58
|
*
|
|
@@ -79,7 +82,7 @@ export declare abstract class AbstractWorker<MainWorker extends Worker | Message
|
|
|
79
82
|
* @param fn - Function that will be executed.
|
|
80
83
|
* @param message - Input data for the given function.
|
|
81
84
|
*/
|
|
82
|
-
protected
|
|
85
|
+
protected runSync(fn: WorkerSyncFunction<Data, Response>, message: MessageValue<Data>): void;
|
|
83
86
|
/**
|
|
84
87
|
* Runs the given function asynchronously.
|
|
85
88
|
*
|
|
@@ -87,4 +90,10 @@ export declare abstract class AbstractWorker<MainWorker extends Worker | Message
|
|
|
87
90
|
* @param message - Input data for the given function.
|
|
88
91
|
*/
|
|
89
92
|
protected runAsync(fn: WorkerAsyncFunction<Data, Response>, message: MessageValue<Data>): void;
|
|
93
|
+
/**
|
|
94
|
+
* Gets the task function in the given scope.
|
|
95
|
+
*
|
|
96
|
+
* @param name - Name of the function that will be returned.
|
|
97
|
+
*/
|
|
98
|
+
private getTaskFunction;
|
|
90
99
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import type { Worker } from 'node:cluster';
|
|
3
|
-
import type { MessageValue, WorkerFunction } from '../utility-types';
|
|
3
|
+
import type { MessageValue, TaskFunctions, WorkerFunction } from '../utility-types';
|
|
4
4
|
import { AbstractWorker } from './abstract-worker';
|
|
5
5
|
import type { WorkerOptions } from './worker-options';
|
|
6
6
|
/**
|
|
@@ -21,10 +21,10 @@ export declare class ClusterWorker<Data = unknown, Response = unknown> extends A
|
|
|
21
21
|
/**
|
|
22
22
|
* Constructs a new poolifier cluster worker.
|
|
23
23
|
*
|
|
24
|
-
* @param
|
|
24
|
+
* @param taskFunctions - Task function(s) processed by the worker when the pool's `execution` function is invoked.
|
|
25
25
|
* @param opts - Options for the worker.
|
|
26
26
|
*/
|
|
27
|
-
constructor(
|
|
27
|
+
constructor(taskFunctions: WorkerFunction<Data, Response> | TaskFunctions<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, WorkerFunction } from '../utility-types';
|
|
3
|
+
import type { MessageValue, TaskFunctions, WorkerFunction } from '../utility-types';
|
|
4
4
|
import { AbstractWorker } from './abstract-worker';
|
|
5
5
|
import type { WorkerOptions } from './worker-options';
|
|
6
6
|
/**
|
|
@@ -21,10 +21,10 @@ export declare class ThreadWorker<Data = unknown, Response = unknown> extends Ab
|
|
|
21
21
|
/**
|
|
22
22
|
* Constructs a new poolifier thread worker.
|
|
23
23
|
*
|
|
24
|
-
* @param
|
|
24
|
+
* @param taskFunctions - Task function(s) processed by the worker when the pool's `execution` function is invoked.
|
|
25
25
|
* @param opts - Options for the worker.
|
|
26
26
|
*/
|
|
27
|
-
constructor(
|
|
27
|
+
constructor(taskFunctions: WorkerFunction<Data, Response> | TaskFunctions<Data, Response>, opts?: WorkerOptions);
|
|
28
28
|
/** @inheritDoc */
|
|
29
29
|
protected sendToMainWorker(message: MessageValue<Response>): void;
|
|
30
30
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "poolifier",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.12",
|
|
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",
|
|
@@ -22,10 +22,11 @@
|
|
|
22
22
|
},
|
|
23
23
|
"engines": {
|
|
24
24
|
"node": ">=16.0.0",
|
|
25
|
-
"pnpm": ">=
|
|
25
|
+
"pnpm": ">=8.0.0"
|
|
26
26
|
},
|
|
27
27
|
"volta": {
|
|
28
|
-
"node": "20.
|
|
28
|
+
"node": "20.1.0",
|
|
29
|
+
"pnpm": "8.4.0"
|
|
29
30
|
},
|
|
30
31
|
"repository": {
|
|
31
32
|
"type": "git",
|
|
@@ -77,45 +78,44 @@
|
|
|
77
78
|
"lib"
|
|
78
79
|
],
|
|
79
80
|
"devDependencies": {
|
|
80
|
-
"@commitlint/cli": "^17.6.
|
|
81
|
-
"@commitlint/config-conventional": "^17.6.
|
|
81
|
+
"@commitlint/cli": "^17.6.3",
|
|
82
|
+
"@commitlint/config-conventional": "^17.6.3",
|
|
82
83
|
"@release-it/bumper": "^4.0.2",
|
|
83
84
|
"@release-it/keep-a-changelog": "^3.1.0",
|
|
84
85
|
"@rollup/plugin-terser": "^0.4.1",
|
|
85
86
|
"@rollup/plugin-typescript": "^11.1.0",
|
|
86
|
-
"@types/node": "^
|
|
87
|
-
"@typescript-eslint/eslint-plugin": "^5.59.
|
|
88
|
-
"@typescript-eslint/parser": "^5.59.
|
|
87
|
+
"@types/node": "^20.1.0",
|
|
88
|
+
"@typescript-eslint/eslint-plugin": "^5.59.2",
|
|
89
|
+
"@typescript-eslint/parser": "^5.59.2",
|
|
89
90
|
"benny": "^3.7.1",
|
|
90
91
|
"c8": "^7.13.0",
|
|
91
|
-
"eslint": "^8.
|
|
92
|
+
"eslint": "^8.40.0",
|
|
92
93
|
"eslint-config-standard": "^17.0.0",
|
|
93
94
|
"eslint-config-standard-with-typescript": "^34.0.1",
|
|
94
|
-
"eslint-define-config": "^1.
|
|
95
|
+
"eslint-define-config": "^1.20.0",
|
|
95
96
|
"eslint-import-resolver-typescript": "^3.5.5",
|
|
96
97
|
"eslint-plugin-import": "^2.27.5",
|
|
97
|
-
"eslint-plugin-jsdoc": "^43.0
|
|
98
|
+
"eslint-plugin-jsdoc": "^43.2.0",
|
|
98
99
|
"eslint-plugin-n": "^15.7.0",
|
|
99
100
|
"eslint-plugin-promise": "^6.1.1",
|
|
100
101
|
"eslint-plugin-spellcheck": "^0.0.20",
|
|
101
102
|
"eslint-plugin-tsdoc": "^0.2.17",
|
|
102
103
|
"expect": "^29.5.0",
|
|
103
104
|
"husky": "^8.0.3",
|
|
104
|
-
"lint-staged": "^13.2.
|
|
105
|
+
"lint-staged": "^13.2.2",
|
|
105
106
|
"microtime": "^3.1.1",
|
|
106
107
|
"mocha": "^10.2.0",
|
|
107
108
|
"mochawesome": "^7.1.3",
|
|
108
109
|
"prettier": "^2.8.8",
|
|
109
|
-
"
|
|
110
|
-
"
|
|
111
|
-
"rollup": "^3.20.7",
|
|
110
|
+
"release-it": "^15.10.3",
|
|
111
|
+
"rollup": "^3.21.5",
|
|
112
112
|
"rollup-plugin-analyzer": "^4.0.0",
|
|
113
113
|
"rollup-plugin-command": "^1.1.3",
|
|
114
114
|
"rollup-plugin-delete": "^2.0.0",
|
|
115
115
|
"sinon": "^15.0.4",
|
|
116
116
|
"source-map-support": "^0.5.21",
|
|
117
117
|
"ts-standard": "^12.0.2",
|
|
118
|
-
"typedoc": "^0.24.
|
|
118
|
+
"typedoc": "^0.24.6",
|
|
119
119
|
"typescript": "^5.0.4"
|
|
120
120
|
},
|
|
121
121
|
"scripts": {
|