poolifier 2.4.4 → 2.4.6
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 +7 -0
- package/lib/circular-array.d.ts +15 -0
- package/lib/index.d.ts +2 -2
- package/lib/index.js +1 -1
- package/lib/index.mjs +1 -1
- package/lib/pools/abstract-pool.d.ts +56 -42
- package/lib/pools/pool-internal.d.ts +8 -27
- package/lib/pools/pool.d.ts +13 -2
- package/lib/pools/selection-strategies/abstract-worker-choice-strategy.d.ts +9 -6
- package/lib/pools/selection-strategies/fair-share-worker-choice-strategy.d.ts +5 -5
- package/lib/pools/selection-strategies/less-busy-worker-choice-strategy.d.ts +3 -3
- package/lib/pools/selection-strategies/less-used-worker-choice-strategy.d.ts +3 -3
- package/lib/pools/selection-strategies/round-robin-worker-choice-strategy.d.ts +5 -5
- package/lib/pools/selection-strategies/selection-strategies-types.d.ts +15 -5
- package/lib/pools/selection-strategies/weighted-round-robin-worker-choice-strategy.d.ts +9 -8
- package/lib/pools/selection-strategies/worker-choice-strategy-context.d.ts +10 -10
- package/lib/pools/{pool-worker.d.ts → worker.d.ts} +30 -2
- package/lib/utility-types.d.ts +3 -3
- package/lib/utils.d.ts +7 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -174,7 +174,14 @@ Node versions >= 16.x are supported.
|
|
|
174
174
|
`WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN` and `WorkerChoiceStrategies.FAIR_SHARE` strategies are targeted to heavy and long tasks
|
|
175
175
|
Default: `WorkerChoiceStrategies.ROUND_ROBIN`
|
|
176
176
|
|
|
177
|
+
- `workerChoiceStrategyOptions` (optional) - The worker choice strategy options object to use in this pool.
|
|
178
|
+
Properties:
|
|
179
|
+
|
|
180
|
+
- `medRunTime` (optional) - Use the tasks median run time instead of the tasks average run time in worker choice strategies.
|
|
181
|
+
Default: { medRunTime: false }
|
|
182
|
+
|
|
177
183
|
- `enableEvents` (optional) - Events emission enablement in this pool. Default: true
|
|
184
|
+
- `enableTasksQueue` (optional, experimental) - Tasks queue per worker enablement in this pool. Default: false
|
|
178
185
|
|
|
179
186
|
### `pool = new DynamicThreadPool/DynamicClusterPool(min, max, filePath, opts)`
|
|
180
187
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Array with a maximum length shifting items when full.
|
|
3
|
+
*/
|
|
4
|
+
export declare class CircularArray<T> extends Array<T> {
|
|
5
|
+
size: number;
|
|
6
|
+
constructor(size?: number, ...items: T[]);
|
|
7
|
+
push(...items: T[]): number;
|
|
8
|
+
unshift(...items: T[]): number;
|
|
9
|
+
concat(...items: Array<T | ConcatArray<T>>): CircularArray<T>;
|
|
10
|
+
splice(start: number, deleteCount?: number, ...items: T[]): T[];
|
|
11
|
+
resize(size: number): void;
|
|
12
|
+
empty(): boolean;
|
|
13
|
+
full(): boolean;
|
|
14
|
+
private checkSize;
|
|
15
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -3,9 +3,9 @@ export { FixedClusterPool } from './pools/cluster/fixed';
|
|
|
3
3
|
export type { ClusterPoolOptions } from './pools/cluster/fixed';
|
|
4
4
|
export { PoolEvents } from './pools/pool';
|
|
5
5
|
export type { IPool, PoolEmitter, PoolOptions, PoolEvent } from './pools/pool';
|
|
6
|
-
export type { ErrorHandler, ExitHandler, MessageHandler, OnlineHandler } from './pools/
|
|
6
|
+
export type { ErrorHandler, ExitHandler, MessageHandler, OnlineHandler } from './pools/worker';
|
|
7
7
|
export { WorkerChoiceStrategies } from './pools/selection-strategies/selection-strategies-types';
|
|
8
|
-
export type { WorkerChoiceStrategy } from './pools/selection-strategies/selection-strategies-types';
|
|
8
|
+
export type { WorkerChoiceStrategy, WorkerChoiceStrategyOptions } from './pools/selection-strategies/selection-strategies-types';
|
|
9
9
|
export { DynamicThreadPool } from './pools/thread/dynamic';
|
|
10
10
|
export { FixedThreadPool } from './pools/thread/fixed';
|
|
11
11
|
export type { ThreadWorkerWithMessageChannel } from './pools/thread/fixed';
|
package/lib/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var e,r=require("node:cluster"),t=require("node:crypto"),s=require("node:events"),i=require("node:os"),o=require("node:worker_threads"),n=require("node:async_hooks");!function(e){e.FIXED="fixed",e.DYNAMIC="dynamic"}(e||(e={}));const a=Object.freeze((()=>{})),h=Object.freeze({SOFT:"SOFT",HARD:"HARD"});class k extends s{}const u=Object.freeze({full:"full",busy:"busy"}),c=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 l{pool;isDynamicPool;requiredStatistics={runTime:!1,avgRunTime:!1};constructor(r){this.pool=r,this.isDynamicPool=this.pool.type===e.DYNAMIC,this.choose.bind(this)}}class p extends l{requiredStatistics={runTime:!0,avgRunTime:!0};workerLastVirtualTaskTimestamp=new Map;reset(){return this.workerLastVirtualTaskTimestamp.clear(),!0}choose(){let e,r=1/0;for(const[t]of this.pool.workers.entries()){this.computeWorkerLastVirtualTaskTimestamp(t);const s=this.workerLastVirtualTaskTimestamp.get(t)?.end??0;s<r&&(r=s,e=t)}return e}remove(e){const r=this.workerLastVirtualTaskTimestamp.delete(e);for(const[r,t]of this.workerLastVirtualTaskTimestamp.entries())r>e&&this.workerLastVirtualTaskTimestamp.set(r-1,t);return r}computeWorkerLastVirtualTaskTimestamp(e){const r=Math.max(Date.now(),this.workerLastVirtualTaskTimestamp.get(e)?.end??-1/0);this.workerLastVirtualTaskTimestamp.set(e,{start:r,end:r+(this.pool.workers[e].tasksUsage.avgRunTime??0)})}}class m extends l{requiredStatistics={runTime:!0,avgRunTime:!1};reset(){return!0}choose(){const e=this.pool.findFreeWorkerKey();if(-1!==e)return e;let r,t=1/0;for(const[e,s]of this.pool.workers.entries()){const i=s.tasksUsage.runTime;if(0===i)return e;i<t&&(t=i,r=e)}return r}remove(e){return!0}}class d extends l{reset(){return!0}choose(){const e=this.pool.findFreeWorkerKey();if(-1!==e)return e;let r,t=1/0;for(const[e,s]of this.pool.workers.entries()){const i=s.tasksUsage,o=i.run+i.running;if(0===o)return e;o<t&&(t=o,r=e)}return r}remove(e){return!0}}class g extends l{nextWorkerId=0;reset(){return this.nextWorkerId=0,!0}choose(){const e=this.nextWorkerId;return this.nextWorkerId=this.nextWorkerId===this.pool.workers.length-1?0:this.nextWorkerId+1,e}remove(e){return this.nextWorkerId===e&&(0===this.pool.workers.length?this.nextWorkerId=0:this.nextWorkerId=this.nextWorkerId>this.pool.workers.length-1?this.pool.workers.length-1:this.nextWorkerId),!0}}class w extends l{requiredStatistics={runTime:!0,avgRunTime:!0};currentWorkerId=0;defaultWorkerWeight;workersTaskRunTime=new Map;constructor(e){super(e),this.defaultWorkerWeight=this.computeWorkerWeight(),this.initWorkersTaskRunTime()}reset(){return this.currentWorkerId=0,this.workersTaskRunTime.clear(),this.initWorkersTaskRunTime(),!0}choose(){const e=this.currentWorkerId;this.isDynamicPool&&!this.workersTaskRunTime.has(e)&&this.initWorkerTaskRunTime(e);const r=this.workersTaskRunTime.get(e)?.runTime??0,t=this.workersTaskRunTime.get(e)?.weight??this.defaultWorkerWeight;return r<t?this.setWorkerTaskRunTime(e,t,r+(this.getWorkerVirtualTaskRunTime(e)??0)):(this.currentWorkerId=this.currentWorkerId===this.pool.workers.length-1?0:this.currentWorkerId+1,this.setWorkerTaskRunTime(this.currentWorkerId,t,0)),e}remove(e){this.currentWorkerId===e&&(0===this.pool.workers.length?this.currentWorkerId=0:this.currentWorkerId=this.currentWorkerId>this.pool.workers.length-1?this.pool.workers.length-1:this.currentWorkerId);const r=this.workersTaskRunTime.delete(e);for(const[r,t]of this.workersTaskRunTime)r>e&&this.workersTaskRunTime.set(r-1,t);return r}initWorkersTaskRunTime(){for(const[e]of this.pool.workers.entries())this.initWorkerTaskRunTime(e)}initWorkerTaskRunTime(e){this.setWorkerTaskRunTime(e,this.defaultWorkerWeight,0)}setWorkerTaskRunTime(e,r,t){this.workersTaskRunTime.set(e,{weight:r,runTime:t})}getWorkerVirtualTaskRunTime(e){return this.pool.workers[e].tasksUsage.avgRunTime}computeWorkerWeight(){let e=0;for(const r of i.cpus()){const t=r.speed.toString().length-1;e+=1/(r.speed/Math.pow(10,t))*Math.pow(10,t)}return Math.round(e/i.cpus().length)}}class T{workerChoiceStrategyType;workerChoiceStrategies;constructor(e,r=c.ROUND_ROBIN){this.workerChoiceStrategyType=r,this.execute.bind(this),this.workerChoiceStrategies=new Map([[c.ROUND_ROBIN,new g(e)],[c.LESS_USED,new d(e)],[c.LESS_BUSY,new m(e)],[c.FAIR_SHARE,new p(e)],[c.WEIGHTED_ROUND_ROBIN,new w(e)]])}getRequiredStatistics(){return this.workerChoiceStrategies.get(this.workerChoiceStrategyType).requiredStatistics}setWorkerChoiceStrategy(e){this.workerChoiceStrategyType!==e&&(this.workerChoiceStrategyType=e),this.workerChoiceStrategies.get(this.workerChoiceStrategyType)?.reset()}execute(){return this.workerChoiceStrategies.get(this.workerChoiceStrategyType).choose()}remove(e){return this.workerChoiceStrategies.get(this.workerChoiceStrategyType).remove(e)}}class W{numberOfWorkers;filePath;opts;workers=[];emitter;promiseResponseMap=new Map;workerChoiceStrategyContext;constructor(e,r,t){if(this.numberOfWorkers=e,this.filePath=r,this.opts=t,!this.isMain())throw new Error("Cannot start a pool from a worker!");this.checkNumberOfWorkers(this.numberOfWorkers),this.checkFilePath(this.filePath),this.checkPoolOptions(this.opts),this.chooseWorker.bind(this),this.internalExecute.bind(this),this.checkAndEmitFull.bind(this),this.checkAndEmitBusy.bind(this),this.sendToWorker.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 T(this,this.opts.workerChoiceStrategy)}checkFilePath(e){if(null==e||"string"==typeof e&&0===e.trim().length)throw new Error("Please specify a file with a worker implementation")}checkNumberOfWorkers(r){if(null==r)throw new Error("Cannot instantiate a pool without specifying the number of workers");if(!Number.isSafeInteger(r))throw new TypeError("Cannot instantiate a pool with a non integer number of workers");if(r<0)throw new RangeError("Cannot instantiate a pool with a negative number of workers");if(this.type===e.FIXED&&0===r)throw new Error("Cannot instantiate a fixed pool with no worker")}checkPoolOptions(e){this.opts.workerChoiceStrategy=e.workerChoiceStrategy??c.ROUND_ROBIN,this.checkValidWorkerChoiceStrategy(this.opts.workerChoiceStrategy),this.opts.enableEvents=e.enableEvents??!0}checkValidWorkerChoiceStrategy(e){if(!Object.values(c).includes(e))throw new Error(`Invalid worker choice strategy '${e}'`)}get numberOfRunningTasks(){return this.promiseResponseMap.size}getWorkerKey(e){return this.workers.findIndex((r=>r.worker===e))}setWorkerChoiceStrategy(e){this.checkValidWorkerChoiceStrategy(e),this.opts.workerChoiceStrategy=e;for(const[e,r]of this.workers.entries())this.setWorker(e,r.worker,{run:0,running:0,runTime:0,avgRunTime:0,error:0});this.workerChoiceStrategyContext.setWorkerChoiceStrategy(e)}internalBusy(){return this.numberOfRunningTasks>=this.numberOfWorkers&&-1===this.findFreeWorkerKey()}findFreeWorkerKey(){return this.workers.findIndex((e=>0===e.tasksUsage.running))}async execute(e){const[r,s]=this.chooseWorker(),i=t.randomUUID(),o=this.internalExecute(r,s,i);return this.checkAndEmitFull(),this.checkAndEmitBusy(),this.sendToWorker(s,{data:e??{},id:i}),o}async destroy(){await Promise.all(this.workers.map((async e=>{await this.destroyWorker(e.worker)})))}setupHook(){}beforePromiseResponseHook(e){++this.workers[e].tasksUsage.running}afterPromiseResponseHook(e,r){const t=this.getWorkerTasksUsage(e);--t.running,++t.run,null!=r.error&&++t.error,this.workerChoiceStrategyContext.getRequiredStatistics().runTime&&(t.runTime+=r.runTime??0,this.workerChoiceStrategyContext.getRequiredStatistics().avgRunTime&&0!==t.run&&(t.avgRunTime=t.runTime/t.run))}chooseWorker(){let r;if(this.type!==e.DYNAMIC||this.full||-1!==this.findFreeWorkerKey())r=this.workerChoiceStrategyContext.execute();else{const e=this.createAndSetupWorker();this.registerWorkerMessageListener(e,(r=>{var t;t=h.HARD,(r.kill===t||null!=r.kill&&0===this.getWorkerTasksUsage(e)?.running)&&this.destroyWorker(e)})),r=this.getWorkerKey(e)}return[r,this.workers[r].worker]}createAndSetupWorker(){const e=this.createWorker();return e.on("message",this.opts.messageHandler??a),e.on("error",this.opts.errorHandler??a),e.on("online",this.opts.onlineHandler??a),e.on("exit",this.opts.exitHandler??a),e.once("exit",(()=>{this.removeWorker(e)})),this.pushWorker(e,{run:0,running:0,runTime:0,avgRunTime:0,error:0}),this.afterWorkerSetup(e),e}workerListener(){return e=>{if(null!=e.id){const r=this.promiseResponseMap.get(e.id);null!=r&&(null!=e.error?r.reject(e.error):r.resolve(e.data),this.afterPromiseResponseHook(r.worker,e),this.promiseResponseMap.delete(e.id))}}}async internalExecute(e,r,t){return this.beforePromiseResponseHook(e),await new Promise(((e,s)=>{this.promiseResponseMap.set(t,{resolve:e,reject:s,worker:r})}))}checkAndEmitBusy(){!0===this.opts.enableEvents&&this.busy&&this.emitter?.emit(u.busy)}checkAndEmitFull(){this.type===e.DYNAMIC&&!0===this.opts.enableEvents&&this.full&&this.emitter?.emit(u.full)}getWorkerTasksUsage(e){const r=this.getWorkerKey(e);if(-1!==r)return this.workers[r].tasksUsage;throw new Error("Worker could not be found in the pool")}pushWorker(e,r){this.workers.push({worker:e,tasksUsage:r})}setWorker(e,r,t){this.workers[e]={worker:r,tasksUsage:t}}removeWorker(e){const r=this.getWorkerKey(e);this.workers.splice(r,1),this.workerChoiceStrategyContext.remove(r)}}class f extends W{opts;constructor(e,r,t={}){super(e,r,t),this.opts=t}setupHook(){r.setupPrimary({...this.opts.settings,exec:this.filePath})}isMain(){return r.isPrimary}destroyWorker(e){this.sendToWorker(e,{kill:1}),e.kill()}sendToWorker(e,r){e.send(r)}registerWorkerMessageListener(e,r){e.on("message",r)}createWorker(){return r.fork(this.opts.env)}afterWorkerSetup(e){this.registerWorkerMessageListener(e,super.workerListener())}get type(){return e.FIXED}get full(){return this.workers.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}class y extends W{constructor(e,r,t={}){super(e,r,t)}isMain(){return o.isMainThread}async destroyWorker(e){this.sendToWorker(e,{kill:1}),await e.terminate()}sendToWorker(e,r){e.postMessage(r)}registerWorkerMessageListener(e,r){e.port2?.on("message",r)}createWorker(){return new o.Worker(this.filePath,{env:o.SHARE_ENV})}afterWorkerSetup(e){const{port1:r,port2:t}=new o.MessageChannel;e.postMessage({parent:r},[r]),e.port1=r,e.port2=t,this.registerWorkerMessageListener(e,super.workerListener())}get type(){return e.FIXED}get full(){return this.workers.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}const S=6e4,R=h.SOFT;class x extends n.AsyncResource{isMain;mainWorker;opts;lastTaskTimestamp;aliveInterval;constructor(e,r,t,s,i={killBehavior:R,maxInactiveTime:S}){super(e),this.isMain=r,this.mainWorker=s,this.opts=i,this.checkFunctionInput(t),this.checkWorkerOptions(this.opts),this.isMain||(this.lastTaskTimestamp=Date.now(),this.aliveInterval=setInterval(this.checkAlive.bind(this),(this.opts.maxInactiveTime??S)/2),this.checkAlive.bind(this)()),this.mainWorker?.on("message",(e=>{this.messageListener(e,t)}))}messageListener(e,r){null!=e.data&&null!=e.id?!0===this.opts.async?this.runInAsyncScope(this.runAsync.bind(this),this,r,e):this.runInAsyncScope(this.run.bind(this),this,r,e):null!=e.parent?this.mainWorker=e.parent:null!=e.kill&&(null!=this.aliveInterval&&clearInterval(this.aliveInterval),this.emitDestroy())}checkWorkerOptions(e){this.opts.killBehavior=e.killBehavior??R,this.opts.maxInactiveTime=e.maxInactiveTime??S,this.opts.async=e.async??!1}checkFunctionInput(e){if(null==e)throw new Error("fn parameter is mandatory");if("function"!=typeof e)throw new TypeError("fn parameter is not a function")}getMainWorker(){if(null==this.mainWorker)throw new Error("Main worker was not set");return this.mainWorker}checkAlive(){Date.now()-this.lastTaskTimestamp>(this.opts.maxInactiveTime??S)&&this.sendToMainWorker({kill:this.opts.killBehavior})}handleError(e){return e}run(e,r){try{const t=Date.now(),s=e(r.data),i=Date.now()-t;this.sendToMainWorker({data:s,id:r.id,runTime:i})}catch(e){const t=this.handleError(e);this.sendToMainWorker({error:t,id:r.id})}finally{!this.isMain&&(this.lastTaskTimestamp=Date.now())}}runAsync(e,r){const t=Date.now();e(r.data).then((e=>{const s=Date.now()-t;return this.sendToMainWorker({data:e,id:r.id,runTime:s}),null})).catch((e=>{const t=this.handleError(e);this.sendToMainWorker({error:t,id:r.id})})).finally((()=>{!this.isMain&&(this.lastTaskTimestamp=Date.now())})).catch(a)}}exports.ClusterWorker=class extends x{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 f{max;constructor(e,r,t,s={}){super(e,t,s),this.max=r}get type(){return e.DYNAMIC}get full(){return this.workers.length===this.max}get busy(){return this.full&&-1===this.findFreeWorkerKey()}},exports.DynamicThreadPool=class extends y{max;constructor(e,r,t,s={}){super(e,t,s),this.max=r}get type(){return e.DYNAMIC}get full(){return this.workers.length===this.max}get busy(){return this.full&&-1===this.findFreeWorkerKey()}},exports.FixedClusterPool=f,exports.FixedThreadPool=y,exports.KillBehaviors=h,exports.PoolEvents=u,exports.ThreadWorker=class extends x{constructor(e,r={}){super("worker-thread-pool:poolifier",o.isMainThread,e,o.parentPort,r)}sendToMainWorker(e){this.getMainWorker().postMessage(e)}},exports.WorkerChoiceStrategies=c;
|
|
1
|
+
"use strict";var e,r=require("node:cluster"),t=require("node:crypto"),s=require("node:events"),i=require("node:os"),o=require("node:worker_threads"),n=require("node:async_hooks");!function(e){e.FIXED="fixed",e.DYNAMIC="dynamic"}(e||(e={}));const a=Object.freeze((()=>{})),h=Object.freeze({SOFT:"SOFT",HARD:"HARD"});class u extends s{}const k=Object.freeze({full:"full",busy:"busy"}),c=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 l{pool;opts;isDynamicPool;requiredStatistics={runTime:!1,avgRunTime:!1,medRunTime:!1};constructor(r,t={medRunTime:!1}){this.pool=r,this.opts=t,this.checkOptions(this.opts),this.isDynamicPool=this.pool.type===e.DYNAMIC,this.choose.bind(this)}checkOptions(e){this.requiredStatistics.avgRunTime&&!0===e.medRunTime&&(this.requiredStatistics.medRunTime=!0)}}class d extends l{requiredStatistics={runTime:!0,avgRunTime:!0,medRunTime:!1};workerLastVirtualTaskTimestamp=new Map;reset(){return this.workerLastVirtualTaskTimestamp.clear(),!0}choose(){let e,r=1/0;for(const[t]of this.pool.workerNodes.entries()){this.computeWorkerLastVirtualTaskTimestamp(t);const s=this.workerLastVirtualTaskTimestamp.get(t)?.end??0;s<r&&(r=s,e=t)}return e}remove(e){const r=this.workerLastVirtualTaskTimestamp.delete(e);for(const[r,t]of this.workerLastVirtualTaskTimestamp.entries())r>e&&this.workerLastVirtualTaskTimestamp.set(r-1,t);return r}computeWorkerLastVirtualTaskTimestamp(e){const r=Math.max(performance.now(),this.workerLastVirtualTaskTimestamp.get(e)?.end??-1/0),t=this.requiredStatistics.medRunTime?this.pool.workerNodes[e].tasksUsage.medRunTime:this.pool.workerNodes[e].tasksUsage.avgRunTime;this.workerLastVirtualTaskTimestamp.set(e,{start:r,end:r+(t??0)})}}class p extends l{requiredStatistics={runTime:!0,avgRunTime:!1,medRunTime:!1};reset(){return!0}choose(){const e=this.pool.findFreeWorkerNodeKey();if(-1!==e)return e;let r,t=1/0;for(const[e,s]of this.pool.workerNodes.entries()){const i=s.tasksUsage.runTime;if(0===i)return e;i<t&&(t=i,r=e)}return r}remove(e){return!0}}class m extends l{reset(){return!0}choose(){const e=this.pool.findFreeWorkerNodeKey();if(-1!==e)return e;let r,t=1/0;for(const[e,s]of this.pool.workerNodes.entries()){const i=s.tasksUsage,o=i.run+i.running;if(0===o)return e;o<t&&(t=o,r=e)}return r}remove(e){return!0}}class g extends l{nextWorkerNodeId=0;reset(){return this.nextWorkerNodeId=0,!0}choose(){const e=this.nextWorkerNodeId;return this.nextWorkerNodeId=this.nextWorkerNodeId===this.pool.workerNodes.length-1?0:this.nextWorkerNodeId+1,e}remove(e){return this.nextWorkerNodeId===e&&(0===this.pool.workerNodes.length?this.nextWorkerNodeId=0:this.nextWorkerNodeId=this.nextWorkerNodeId>this.pool.workerNodes.length-1?this.pool.workerNodes.length-1:this.nextWorkerNodeId),!0}}class T extends l{requiredStatistics={runTime:!0,avgRunTime:!0,medRunTime:!1};currentWorkerNodeId=0;defaultWorkerWeight;workersTaskRunTime=new Map;constructor(e,r){super(e,r),this.defaultWorkerWeight=this.computeWorkerWeight(),this.initWorkersTaskRunTime()}reset(){return this.currentWorkerNodeId=0,this.workersTaskRunTime.clear(),this.initWorkersTaskRunTime(),!0}choose(){const e=this.currentWorkerNodeId;this.isDynamicPool&&!this.workersTaskRunTime.has(e)&&this.initWorkerTaskRunTime(e);const r=this.workersTaskRunTime.get(e)?.runTime??0,t=this.workersTaskRunTime.get(e)?.weight??this.defaultWorkerWeight;return r<t?this.setWorkerTaskRunTime(e,t,r+(this.getWorkerVirtualTaskRunTime(e)??0)):(this.currentWorkerNodeId=this.currentWorkerNodeId===this.pool.workerNodes.length-1?0:this.currentWorkerNodeId+1,this.setWorkerTaskRunTime(this.currentWorkerNodeId,t,0)),e}remove(e){this.currentWorkerNodeId===e&&(0===this.pool.workerNodes.length?this.currentWorkerNodeId=0:this.currentWorkerNodeId=this.currentWorkerNodeId>this.pool.workerNodes.length-1?this.pool.workerNodes.length-1:this.currentWorkerNodeId);const r=this.workersTaskRunTime.delete(e);for(const[r,t]of this.workersTaskRunTime)r>e&&this.workersTaskRunTime.set(r-1,t);return r}initWorkersTaskRunTime(){for(const[e]of this.pool.workerNodes.entries())this.initWorkerTaskRunTime(e)}initWorkerTaskRunTime(e){this.setWorkerTaskRunTime(e,this.defaultWorkerWeight,0)}setWorkerTaskRunTime(e,r,t){this.workersTaskRunTime.set(e,{weight:r,runTime:t})}getWorkerVirtualTaskRunTime(e){return this.requiredStatistics.medRunTime?this.pool.workerNodes[e].tasksUsage.medRunTime:this.pool.workerNodes[e].tasksUsage.avgRunTime}computeWorkerWeight(){let e=0;for(const r of i.cpus()){const t=r.speed.toString().length-1;e+=1/(r.speed/Math.pow(10,t))*Math.pow(10,t)}return Math.round(e/i.cpus().length)}}class w{workerChoiceStrategyType;workerChoiceStrategies;constructor(e,r=c.ROUND_ROBIN,t={medRunTime:!1}){this.workerChoiceStrategyType=r,this.execute.bind(this),this.workerChoiceStrategies=new Map([[c.ROUND_ROBIN,new g(e,t)],[c.LESS_USED,new m(e,t)],[c.LESS_BUSY,new p(e,t)],[c.FAIR_SHARE,new d(e,t)],[c.WEIGHTED_ROUND_ROBIN,new T(e,t)]])}getRequiredStatistics(){return this.workerChoiceStrategies.get(this.workerChoiceStrategyType).requiredStatistics}setWorkerChoiceStrategy(e){this.workerChoiceStrategyType!==e&&(this.workerChoiceStrategyType=e),this.workerChoiceStrategies.get(this.workerChoiceStrategyType)?.reset()}execute(){return this.workerChoiceStrategies.get(this.workerChoiceStrategyType).choose()}remove(e){return this.workerChoiceStrategies.get(this.workerChoiceStrategyType).remove(e)}}class W extends Array{size;constructor(e=1024,...r){super(),this.checkSize(e),this.size=e,arguments.length>1&&this.push(...r)}push(...e){const r=super.push(...e);return r>this.size&&super.splice(0,r-this.size),this.length}unshift(...e){return super.unshift(...e)>this.size&&super.splice(this.size,e.length),this.length}concat(...e){const r=super.concat(e);return r.size=this.size,r.length>r.size&&r.splice(0,r.length-r.size),r}splice(e,r,...t){let s;return arguments.length>=3&&void 0!==r?(s=super.splice(e,r),this.push(...t)):s=2===arguments.length?super.splice(e,r):super.splice(e),s}resize(e){if(this.checkSize(e),0===e)this.length=0;else if(e<this.size)for(let r=e;r<this.size;r++)super.pop();this.size=e}empty(){return 0===this.length}full(){return this.length===this.size}checkSize(e){if(!Number.isSafeInteger(e))throw new TypeError(`Invalid circular array size: ${e} is not a safe integer`);if(e<0)throw new RangeError(`Invalid circular array size: ${e} < 0`)}}class f{numberOfWorkers;filePath;opts;workerNodes=[];emitter;promiseResponseMap=new Map;workerChoiceStrategyContext;constructor(e,r,t){if(this.numberOfWorkers=e,this.filePath=r,this.opts=t,!this.isMain())throw new Error("Cannot start a pool from a worker!");this.checkNumberOfWorkers(this.numberOfWorkers),this.checkFilePath(this.filePath),this.checkPoolOptions(this.opts),this.chooseWorkerNode.bind(this),this.executeTask.bind(this),this.enqueueTask.bind(this),this.checkAndEmitEvents.bind(this),this.setupHook();for(let e=1;e<=this.numberOfWorkers;e++)this.createAndSetupWorker();!0===this.opts.enableEvents&&(this.emitter=new u),this.workerChoiceStrategyContext=new w(this,this.opts.workerChoiceStrategy,this.opts.workerChoiceStrategyOptions)}checkFilePath(e){if(null==e||"string"==typeof e&&0===e.trim().length)throw new Error("Please specify a file with a worker implementation")}checkNumberOfWorkers(r){if(null==r)throw new Error("Cannot instantiate a pool without specifying the number of workers");if(!Number.isSafeInteger(r))throw new TypeError("Cannot instantiate a pool with a non integer number of workers");if(r<0)throw new RangeError("Cannot instantiate a pool with a negative number of workers");if(this.type===e.FIXED&&0===r)throw new Error("Cannot instantiate a fixed pool with no worker")}checkPoolOptions(e){this.opts.workerChoiceStrategy=e.workerChoiceStrategy??c.ROUND_ROBIN,this.checkValidWorkerChoiceStrategy(this.opts.workerChoiceStrategy),this.opts.workerChoiceStrategyOptions=e.workerChoiceStrategyOptions??{medRunTime:!1},this.opts.enableEvents=e.enableEvents??!0,this.opts.enableTasksQueue=e.enableTasksQueue??!1}checkValidWorkerChoiceStrategy(e){if(!Object.values(c).includes(e))throw new Error(`Invalid worker choice strategy '${e}'`)}get numberOfRunningTasks(){return this.workerNodes.reduce(((e,r)=>e+r.tasksUsage.running),0)}get numberOfQueuedTasks(){return!1===this.opts.enableTasksQueue?0:this.workerNodes.reduce(((e,r)=>e+r.tasksQueue.length),0)}getWorkerNodeKey(e){return this.workerNodes.findIndex((r=>r.worker===e))}setWorkerChoiceStrategy(e){this.checkValidWorkerChoiceStrategy(e),this.opts.workerChoiceStrategy=e;for(const e of this.workerNodes)this.setWorkerNodeTasksUsage(e,{run:0,running:0,runTime:0,runTimeHistory:new W,avgRunTime:0,medRunTime:0,error:0});this.workerChoiceStrategyContext.setWorkerChoiceStrategy(e)}internalBusy(){return-1===this.findFreeWorkerNodeKey()}findFreeWorkerNodeKey(){return this.workerNodes.findIndex((e=>0===e.tasksUsage?.running))}async execute(e){const[r,s]=this.chooseWorkerNode(),i={data:e??{},id:t.randomUUID()},o=new Promise(((e,r)=>{this.promiseResponseMap.set(i.id,{resolve:e,reject:r,worker:s.worker})}));return!0===this.opts.enableTasksQueue&&(this.busy||this.workerNodes[r].tasksUsage.running>0)?this.enqueueTask(r,i):this.executeTask(r,i),this.checkAndEmitEvents(),o}async destroy(){await Promise.all(this.workerNodes.map((async(e,r)=>{this.flushTasksQueue(r),await this.destroyWorker(e.worker)})))}setupHook(){}beforeTaskExecutionHook(e){++this.workerNodes[e].tasksUsage.running}afterTaskExecutionHook(e,r){const t=this.getWorkerTasksUsage(e);--t.running,++t.run,null!=r.error&&++t.error,this.workerChoiceStrategyContext.getRequiredStatistics().runTime&&(t.runTime+=r.runTime??0,this.workerChoiceStrategyContext.getRequiredStatistics().avgRunTime&&0!==t.run&&(t.avgRunTime=t.runTime/t.run),this.workerChoiceStrategyContext.getRequiredStatistics().medRunTime&&(t.runTimeHistory.push(r.runTime??0),t.medRunTime=(e=>{if(Array.isArray(e)&&1===e.length)return e[0];const r=e.slice().sort(((e,r)=>e-r)),t=Math.floor(r.length/2);return r.length%2==0?r[t/2]:(r[t-1]+r[t])/2})(t.runTimeHistory)))}chooseWorkerNode(){let r;if(this.type===e.DYNAMIC&&!this.full&&this.internalBusy()){const e=this.createAndSetupWorker();this.registerWorkerMessageListener(e,(r=>{var t;t=h.HARD,(r.kill===t||null!=r.kill&&0===this.getWorkerTasksUsage(e)?.running)&&(this.flushTasksQueueByWorker(e),this.destroyWorker(e))})),r=this.getWorkerNodeKey(e)}else r=this.workerChoiceStrategyContext.execute();return[r,this.workerNodes[r]]}createAndSetupWorker(){const e=this.createWorker();return e.on("message",this.opts.messageHandler??a),e.on("error",this.opts.errorHandler??a),e.on("online",this.opts.onlineHandler??a),e.on("exit",this.opts.exitHandler??a),e.once("exit",(()=>{this.removeWorkerNode(e)})),this.pushWorkerNode(e),this.afterWorkerSetup(e),e}workerListener(){return e=>{if(null!=e.id){const r=this.promiseResponseMap.get(e.id);if(null!=r){null!=e.error?r.reject(e.error):r.resolve(e.data),this.afterTaskExecutionHook(r.worker,e),this.promiseResponseMap.delete(e.id);const t=this.getWorkerNodeKey(r.worker);!0===this.opts.enableTasksQueue&&this.tasksQueueSize(t)>0&&this.executeTask(t,this.dequeueTask(t))}}}}checkAndEmitEvents(){!0===this.opts.enableEvents&&(this.busy&&this.emitter?.emit(k.busy),this.type===e.DYNAMIC&&this.full&&this.emitter?.emit(k.full))}setWorkerNodeTasksUsage(e,r){e.tasksUsage=r}getWorkerTasksUsage(e){const r=this.getWorkerNodeKey(e);if(-1!==r)return this.workerNodes[r].tasksUsage;throw new Error("Worker could not be found in the pool worker nodes")}pushWorkerNode(e){return this.workerNodes.push({worker:e,tasksUsage:{run:0,running:0,runTime:0,runTimeHistory:new W,avgRunTime:0,medRunTime:0,error:0},tasksQueue:[]})}setWorkerNode(e,r,t,s){this.workerNodes[e]={worker:r,tasksUsage:t,tasksQueue:s}}removeWorkerNode(e){const r=this.getWorkerNodeKey(e);this.workerNodes.splice(r,1),this.workerChoiceStrategyContext.remove(r)}executeTask(e,r){this.beforeTaskExecutionHook(e),this.sendToWorker(this.workerNodes[e].worker,r)}enqueueTask(e,r){return this.workerNodes[e].tasksQueue.push(r)}dequeueTask(e){return this.workerNodes[e].tasksQueue.shift()}tasksQueueSize(e){return this.workerNodes[e].tasksQueue.length}flushTasksQueue(e){if(this.tasksQueueSize(e)>0)for(const r of this.workerNodes[e].tasksQueue)this.executeTask(e,r)}flushTasksQueueByWorker(e){const r=this.getWorkerNodeKey(e);this.flushTasksQueue(r)}}class y extends f{opts;constructor(e,r,t={}){super(e,r,t),this.opts=t}setupHook(){r.setupPrimary({...this.opts.settings,exec:this.filePath})}isMain(){return r.isPrimary}destroyWorker(e){this.sendToWorker(e,{kill:1}),e.kill()}sendToWorker(e,r){e.send(r)}registerWorkerMessageListener(e,r){e.on("message",r)}createWorker(){return r.fork(this.opts.env)}afterWorkerSetup(e){this.registerWorkerMessageListener(e,super.workerListener())}get type(){return e.FIXED}get full(){return this.workerNodes.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}class N extends f{constructor(e,r,t={}){super(e,r,t)}isMain(){return o.isMainThread}async destroyWorker(e){this.sendToWorker(e,{kill:1}),await e.terminate()}sendToWorker(e,r){e.postMessage(r)}registerWorkerMessageListener(e,r){e.port2?.on("message",r)}createWorker(){return new o.Worker(this.filePath,{env:o.SHARE_ENV})}afterWorkerSetup(e){const{port1:r,port2:t}=new o.MessageChannel;e.postMessage({parent:r},[r]),e.port1=r,e.port2=t,this.registerWorkerMessageListener(e,super.workerListener())}get type(){return e.FIXED}get full(){return this.workerNodes.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}const S=6e4,R=h.SOFT;class x extends n.AsyncResource{isMain;mainWorker;opts;lastTaskTimestamp;aliveInterval;constructor(e,r,t,s,i={killBehavior:R,maxInactiveTime:S}){super(e),this.isMain=r,this.mainWorker=s,this.opts=i,this.checkFunctionInput(t),this.checkWorkerOptions(this.opts),this.isMain||(this.lastTaskTimestamp=performance.now(),this.aliveInterval=setInterval(this.checkAlive.bind(this),(this.opts.maxInactiveTime??S)/2),this.checkAlive.bind(this)()),this.mainWorker?.on("message",(e=>{this.messageListener(e,t)}))}messageListener(e,r){null!=e.data&&null!=e.id?!0===this.opts.async?this.runInAsyncScope(this.runAsync.bind(this),this,r,e):this.runInAsyncScope(this.run.bind(this),this,r,e):null!=e.parent?this.mainWorker=e.parent:null!=e.kill&&(null!=this.aliveInterval&&clearInterval(this.aliveInterval),this.emitDestroy())}checkWorkerOptions(e){this.opts.killBehavior=e.killBehavior??R,this.opts.maxInactiveTime=e.maxInactiveTime??S,this.opts.async=e.async??!1}checkFunctionInput(e){if(null==e)throw new Error("fn parameter is mandatory");if("function"!=typeof e)throw new TypeError("fn parameter is not a function")}getMainWorker(){if(null==this.mainWorker)throw new Error("Main worker was not set");return this.mainWorker}checkAlive(){performance.now()-this.lastTaskTimestamp>(this.opts.maxInactiveTime??S)&&this.sendToMainWorker({kill:this.opts.killBehavior})}handleError(e){return e}run(e,r){try{const t=performance.now(),s=e(r.data),i=performance.now()-t;this.sendToMainWorker({data:s,id:r.id,runTime:i})}catch(e){const t=this.handleError(e);this.sendToMainWorker({error:t,id:r.id})}finally{!this.isMain&&(this.lastTaskTimestamp=performance.now())}}runAsync(e,r){const t=performance.now();e(r.data).then((e=>{const s=performance.now()-t;return this.sendToMainWorker({data:e,id:r.id,runTime:s}),null})).catch((e=>{const t=this.handleError(e);this.sendToMainWorker({error:t,id:r.id})})).finally((()=>{!this.isMain&&(this.lastTaskTimestamp=performance.now())})).catch(a)}}exports.ClusterWorker=class extends x{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 y{max;constructor(e,r,t,s={}){super(e,t,s),this.max=r}get type(){return e.DYNAMIC}get full(){return this.workerNodes.length===this.max}get busy(){return this.full&&this.internalBusy()}},exports.DynamicThreadPool=class extends N{max;constructor(e,r,t,s={}){super(e,t,s),this.max=r}get type(){return e.DYNAMIC}get full(){return this.workerNodes.length===this.max}get busy(){return this.full&&this.internalBusy()}},exports.FixedClusterPool=y,exports.FixedThreadPool=N,exports.KillBehaviors=h,exports.PoolEvents=k,exports.ThreadWorker=class extends x{constructor(e,r={}){super("worker-thread-pool:poolifier",o.isMainThread,e,o.parentPort,r)}sendToMainWorker(e){this.getMainWorker().postMessage(e)}},exports.WorkerChoiceStrategies=c;
|
package/lib/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"node:cluster";import r from"node:crypto";import t from"node:events";import{cpus as s}from"node:os";import{isMainThread as i,Worker as o,SHARE_ENV as n,MessageChannel as a,parentPort as h}from"node:worker_threads";import{AsyncResource as k}from"node:async_hooks";var u;!function(e){e.FIXED="fixed",e.DYNAMIC="dynamic"}(u||(u={}));const c=Object.freeze((()=>{})),l=Object.freeze({SOFT:"SOFT",HARD:"HARD"});class m extends t{}const p=Object.freeze({full:"full",busy:"busy"}),d=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 w{pool;isDynamicPool;requiredStatistics={runTime:!1,avgRunTime:!1};constructor(e){this.pool=e,this.isDynamicPool=this.pool.type===u.DYNAMIC,this.choose.bind(this)}}class g extends w{requiredStatistics={runTime:!0,avgRunTime:!0};workerLastVirtualTaskTimestamp=new Map;reset(){return this.workerLastVirtualTaskTimestamp.clear(),!0}choose(){let e,r=1/0;for(const[t]of this.pool.workers.entries()){this.computeWorkerLastVirtualTaskTimestamp(t);const s=this.workerLastVirtualTaskTimestamp.get(t)?.end??0;s<r&&(r=s,e=t)}return e}remove(e){const r=this.workerLastVirtualTaskTimestamp.delete(e);for(const[r,t]of this.workerLastVirtualTaskTimestamp.entries())r>e&&this.workerLastVirtualTaskTimestamp.set(r-1,t);return r}computeWorkerLastVirtualTaskTimestamp(e){const r=Math.max(Date.now(),this.workerLastVirtualTaskTimestamp.get(e)?.end??-1/0);this.workerLastVirtualTaskTimestamp.set(e,{start:r,end:r+(this.pool.workers[e].tasksUsage.avgRunTime??0)})}}class T extends w{requiredStatistics={runTime:!0,avgRunTime:!1};reset(){return!0}choose(){const e=this.pool.findFreeWorkerKey();if(-1!==e)return e;let r,t=1/0;for(const[e,s]of this.pool.workers.entries()){const i=s.tasksUsage.runTime;if(0===i)return e;i<t&&(t=i,r=e)}return r}remove(e){return!0}}class W extends w{reset(){return!0}choose(){const e=this.pool.findFreeWorkerKey();if(-1!==e)return e;let r,t=1/0;for(const[e,s]of this.pool.workers.entries()){const i=s.tasksUsage,o=i.run+i.running;if(0===o)return e;o<t&&(t=o,r=e)}return r}remove(e){return!0}}class f extends w{nextWorkerId=0;reset(){return this.nextWorkerId=0,!0}choose(){const e=this.nextWorkerId;return this.nextWorkerId=this.nextWorkerId===this.pool.workers.length-1?0:this.nextWorkerId+1,e}remove(e){return this.nextWorkerId===e&&(0===this.pool.workers.length?this.nextWorkerId=0:this.nextWorkerId=this.nextWorkerId>this.pool.workers.length-1?this.pool.workers.length-1:this.nextWorkerId),!0}}class y extends w{requiredStatistics={runTime:!0,avgRunTime:!0};currentWorkerId=0;defaultWorkerWeight;workersTaskRunTime=new Map;constructor(e){super(e),this.defaultWorkerWeight=this.computeWorkerWeight(),this.initWorkersTaskRunTime()}reset(){return this.currentWorkerId=0,this.workersTaskRunTime.clear(),this.initWorkersTaskRunTime(),!0}choose(){const e=this.currentWorkerId;this.isDynamicPool&&!this.workersTaskRunTime.has(e)&&this.initWorkerTaskRunTime(e);const r=this.workersTaskRunTime.get(e)?.runTime??0,t=this.workersTaskRunTime.get(e)?.weight??this.defaultWorkerWeight;return r<t?this.setWorkerTaskRunTime(e,t,r+(this.getWorkerVirtualTaskRunTime(e)??0)):(this.currentWorkerId=this.currentWorkerId===this.pool.workers.length-1?0:this.currentWorkerId+1,this.setWorkerTaskRunTime(this.currentWorkerId,t,0)),e}remove(e){this.currentWorkerId===e&&(0===this.pool.workers.length?this.currentWorkerId=0:this.currentWorkerId=this.currentWorkerId>this.pool.workers.length-1?this.pool.workers.length-1:this.currentWorkerId);const r=this.workersTaskRunTime.delete(e);for(const[r,t]of this.workersTaskRunTime)r>e&&this.workersTaskRunTime.set(r-1,t);return r}initWorkersTaskRunTime(){for(const[e]of this.pool.workers.entries())this.initWorkerTaskRunTime(e)}initWorkerTaskRunTime(e){this.setWorkerTaskRunTime(e,this.defaultWorkerWeight,0)}setWorkerTaskRunTime(e,r,t){this.workersTaskRunTime.set(e,{weight:r,runTime:t})}getWorkerVirtualTaskRunTime(e){return this.pool.workers[e].tasksUsage.avgRunTime}computeWorkerWeight(){let e=0;for(const r of s()){const t=r.speed.toString().length-1;e+=1/(r.speed/Math.pow(10,t))*Math.pow(10,t)}return Math.round(e/s().length)}}class S{workerChoiceStrategyType;workerChoiceStrategies;constructor(e,r=d.ROUND_ROBIN){this.workerChoiceStrategyType=r,this.execute.bind(this),this.workerChoiceStrategies=new Map([[d.ROUND_ROBIN,new f(e)],[d.LESS_USED,new W(e)],[d.LESS_BUSY,new T(e)],[d.FAIR_SHARE,new g(e)],[d.WEIGHTED_ROUND_ROBIN,new y(e)]])}getRequiredStatistics(){return this.workerChoiceStrategies.get(this.workerChoiceStrategyType).requiredStatistics}setWorkerChoiceStrategy(e){this.workerChoiceStrategyType!==e&&(this.workerChoiceStrategyType=e),this.workerChoiceStrategies.get(this.workerChoiceStrategyType)?.reset()}execute(){return this.workerChoiceStrategies.get(this.workerChoiceStrategyType).choose()}remove(e){return this.workerChoiceStrategies.get(this.workerChoiceStrategyType).remove(e)}}class R{numberOfWorkers;filePath;opts;workers=[];emitter;promiseResponseMap=new Map;workerChoiceStrategyContext;constructor(e,r,t){if(this.numberOfWorkers=e,this.filePath=r,this.opts=t,!this.isMain())throw new Error("Cannot start a pool from a worker!");this.checkNumberOfWorkers(this.numberOfWorkers),this.checkFilePath(this.filePath),this.checkPoolOptions(this.opts),this.chooseWorker.bind(this),this.internalExecute.bind(this),this.checkAndEmitFull.bind(this),this.checkAndEmitBusy.bind(this),this.sendToWorker.bind(this),this.setupHook();for(let e=1;e<=this.numberOfWorkers;e++)this.createAndSetupWorker();!0===this.opts.enableEvents&&(this.emitter=new m),this.workerChoiceStrategyContext=new S(this,this.opts.workerChoiceStrategy)}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===u.FIXED&&0===e)throw new Error("Cannot instantiate a fixed pool with no worker")}checkPoolOptions(e){this.opts.workerChoiceStrategy=e.workerChoiceStrategy??d.ROUND_ROBIN,this.checkValidWorkerChoiceStrategy(this.opts.workerChoiceStrategy),this.opts.enableEvents=e.enableEvents??!0}checkValidWorkerChoiceStrategy(e){if(!Object.values(d).includes(e))throw new Error(`Invalid worker choice strategy '${e}'`)}get numberOfRunningTasks(){return this.promiseResponseMap.size}getWorkerKey(e){return this.workers.findIndex((r=>r.worker===e))}setWorkerChoiceStrategy(e){this.checkValidWorkerChoiceStrategy(e),this.opts.workerChoiceStrategy=e;for(const[e,r]of this.workers.entries())this.setWorker(e,r.worker,{run:0,running:0,runTime:0,avgRunTime:0,error:0});this.workerChoiceStrategyContext.setWorkerChoiceStrategy(e)}internalBusy(){return this.numberOfRunningTasks>=this.numberOfWorkers&&-1===this.findFreeWorkerKey()}findFreeWorkerKey(){return this.workers.findIndex((e=>0===e.tasksUsage.running))}async execute(e){const[t,s]=this.chooseWorker(),i=r.randomUUID(),o=this.internalExecute(t,s,i);return this.checkAndEmitFull(),this.checkAndEmitBusy(),this.sendToWorker(s,{data:e??{},id:i}),o}async destroy(){await Promise.all(this.workers.map((async e=>{await this.destroyWorker(e.worker)})))}setupHook(){}beforePromiseResponseHook(e){++this.workers[e].tasksUsage.running}afterPromiseResponseHook(e,r){const t=this.getWorkerTasksUsage(e);--t.running,++t.run,null!=r.error&&++t.error,this.workerChoiceStrategyContext.getRequiredStatistics().runTime&&(t.runTime+=r.runTime??0,this.workerChoiceStrategyContext.getRequiredStatistics().avgRunTime&&0!==t.run&&(t.avgRunTime=t.runTime/t.run))}chooseWorker(){let e;if(this.type!==u.DYNAMIC||this.full||-1!==this.findFreeWorkerKey())e=this.workerChoiceStrategyContext.execute();else{const r=this.createAndSetupWorker();this.registerWorkerMessageListener(r,(e=>{var t;t=l.HARD,(e.kill===t||null!=e.kill&&0===this.getWorkerTasksUsage(r)?.running)&&this.destroyWorker(r)})),e=this.getWorkerKey(r)}return[e,this.workers[e].worker]}createAndSetupWorker(){const e=this.createWorker();return e.on("message",this.opts.messageHandler??c),e.on("error",this.opts.errorHandler??c),e.on("online",this.opts.onlineHandler??c),e.on("exit",this.opts.exitHandler??c),e.once("exit",(()=>{this.removeWorker(e)})),this.pushWorker(e,{run:0,running:0,runTime:0,avgRunTime:0,error:0}),this.afterWorkerSetup(e),e}workerListener(){return e=>{if(null!=e.id){const r=this.promiseResponseMap.get(e.id);null!=r&&(null!=e.error?r.reject(e.error):r.resolve(e.data),this.afterPromiseResponseHook(r.worker,e),this.promiseResponseMap.delete(e.id))}}}async internalExecute(e,r,t){return this.beforePromiseResponseHook(e),await new Promise(((e,s)=>{this.promiseResponseMap.set(t,{resolve:e,reject:s,worker:r})}))}checkAndEmitBusy(){!0===this.opts.enableEvents&&this.busy&&this.emitter?.emit(p.busy)}checkAndEmitFull(){this.type===u.DYNAMIC&&!0===this.opts.enableEvents&&this.full&&this.emitter?.emit(p.full)}getWorkerTasksUsage(e){const r=this.getWorkerKey(e);if(-1!==r)return this.workers[r].tasksUsage;throw new Error("Worker could not be found in the pool")}pushWorker(e,r){this.workers.push({worker:e,tasksUsage:r})}setWorker(e,r,t){this.workers[e]={worker:r,tasksUsage:t}}removeWorker(e){const r=this.getWorkerKey(e);this.workers.splice(r,1),this.workerChoiceStrategyContext.remove(r)}}class I extends R{opts;constructor(e,r,t={}){super(e,r,t),this.opts=t}setupHook(){e.setupPrimary({...this.opts.settings,exec:this.filePath})}isMain(){return e.isPrimary}destroyWorker(e){this.sendToWorker(e,{kill:1}),e.kill()}sendToWorker(e,r){e.send(r)}registerWorkerMessageListener(e,r){e.on("message",r)}createWorker(){return e.fork(this.opts.env)}afterWorkerSetup(e){this.registerWorkerMessageListener(e,super.workerListener())}get type(){return u.FIXED}get full(){return this.workers.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}class x extends I{max;constructor(e,r,t,s={}){super(e,t,s),this.max=r}get type(){return u.DYNAMIC}get full(){return this.workers.length===this.max}get busy(){return this.full&&-1===this.findFreeWorkerKey()}}class v extends R{constructor(e,r,t={}){super(e,r,t)}isMain(){return i}async destroyWorker(e){this.sendToWorker(e,{kill:1}),await e.terminate()}sendToWorker(e,r){e.postMessage(r)}registerWorkerMessageListener(e,r){e.port2?.on("message",r)}createWorker(){return new o(this.filePath,{env:n})}afterWorkerSetup(e){const{port1:r,port2:t}=new a;e.postMessage({parent:r},[r]),e.port1=r,e.port2=t,this.registerWorkerMessageListener(e,super.workerListener())}get type(){return u.FIXED}get full(){return this.workers.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}class E extends v{max;constructor(e,r,t,s={}){super(e,t,s),this.max=r}get type(){return u.DYNAMIC}get full(){return this.workers.length===this.max}get busy(){return this.full&&-1===this.findFreeWorkerKey()}}const C=6e4,b=l.SOFT;class M extends k{isMain;mainWorker;opts;lastTaskTimestamp;aliveInterval;constructor(e,r,t,s,i={killBehavior:b,maxInactiveTime:C}){super(e),this.isMain=r,this.mainWorker=s,this.opts=i,this.checkFunctionInput(t),this.checkWorkerOptions(this.opts),this.isMain||(this.lastTaskTimestamp=Date.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,t)}))}messageListener(e,r){null!=e.data&&null!=e.id?!0===this.opts.async?this.runInAsyncScope(this.runAsync.bind(this),this,r,e):this.runInAsyncScope(this.run.bind(this),this,r,e):null!=e.parent?this.mainWorker=e.parent:null!=e.kill&&(null!=this.aliveInterval&&clearInterval(this.aliveInterval),this.emitDestroy())}checkWorkerOptions(e){this.opts.killBehavior=e.killBehavior??b,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")}getMainWorker(){if(null==this.mainWorker)throw new Error("Main worker was not set");return this.mainWorker}checkAlive(){Date.now()-this.lastTaskTimestamp>(this.opts.maxInactiveTime??C)&&this.sendToMainWorker({kill:this.opts.killBehavior})}handleError(e){return e}run(e,r){try{const t=Date.now(),s=e(r.data),i=Date.now()-t;this.sendToMainWorker({data:s,id:r.id,runTime:i})}catch(e){const t=this.handleError(e);this.sendToMainWorker({error:t,id:r.id})}finally{!this.isMain&&(this.lastTaskTimestamp=Date.now())}}runAsync(e,r){const t=Date.now();e(r.data).then((e=>{const s=Date.now()-t;return this.sendToMainWorker({data:e,id:r.id,runTime:s}),null})).catch((e=>{const t=this.handleError(e);this.sendToMainWorker({error:t,id:r.id})})).finally((()=>{!this.isMain&&(this.lastTaskTimestamp=Date.now())})).catch(c)}}class D extends M{constructor(r,t={}){super("worker-cluster-pool:poolifier",e.isPrimary,r,e.worker,t)}sendToMainWorker(e){this.getMainWorker().send(e)}handleError(e){return e instanceof Error?e.message:e}}class O extends M{constructor(e,r={}){super("worker-thread-pool:poolifier",i,e,h,r)}sendToMainWorker(e){this.getMainWorker().postMessage(e)}}export{D as ClusterWorker,x as DynamicClusterPool,E as DynamicThreadPool,I as FixedClusterPool,v as FixedThreadPool,l as KillBehaviors,p as PoolEvents,O as ThreadWorker,d as WorkerChoiceStrategies};
|
|
1
|
+
import e from"node:cluster";import r from"node:crypto";import t from"node:events";import{cpus as s}from"node:os";import{isMainThread as i,Worker as o,SHARE_ENV as n,MessageChannel as a,parentPort as h}from"node:worker_threads";import{AsyncResource as u}from"node:async_hooks";var k;!function(e){e.FIXED="fixed",e.DYNAMIC="dynamic"}(k||(k={}));const c=Object.freeze((()=>{})),l=Object.freeze({SOFT:"SOFT",HARD:"HARD"});class d extends t{}const m=Object.freeze({full:"full",busy:"busy"}),p=Object.freeze({ROUND_ROBIN:"ROUND_ROBIN",LESS_USED:"LESS_USED",LESS_BUSY:"LESS_BUSY",FAIR_SHARE:"FAIR_SHARE",WEIGHTED_ROUND_ROBIN:"WEIGHTED_ROUND_ROBIN"});class g{pool;opts;isDynamicPool;requiredStatistics={runTime:!1,avgRunTime:!1,medRunTime:!1};constructor(e,r={medRunTime:!1}){this.pool=e,this.opts=r,this.checkOptions(this.opts),this.isDynamicPool=this.pool.type===k.DYNAMIC,this.choose.bind(this)}checkOptions(e){this.requiredStatistics.avgRunTime&&!0===e.medRunTime&&(this.requiredStatistics.medRunTime=!0)}}class T extends g{requiredStatistics={runTime:!0,avgRunTime:!0,medRunTime:!1};workerLastVirtualTaskTimestamp=new Map;reset(){return this.workerLastVirtualTaskTimestamp.clear(),!0}choose(){let e,r=1/0;for(const[t]of this.pool.workerNodes.entries()){this.computeWorkerLastVirtualTaskTimestamp(t);const s=this.workerLastVirtualTaskTimestamp.get(t)?.end??0;s<r&&(r=s,e=t)}return e}remove(e){const r=this.workerLastVirtualTaskTimestamp.delete(e);for(const[r,t]of this.workerLastVirtualTaskTimestamp.entries())r>e&&this.workerLastVirtualTaskTimestamp.set(r-1,t);return r}computeWorkerLastVirtualTaskTimestamp(e){const r=Math.max(performance.now(),this.workerLastVirtualTaskTimestamp.get(e)?.end??-1/0),t=this.requiredStatistics.medRunTime?this.pool.workerNodes[e].tasksUsage.medRunTime:this.pool.workerNodes[e].tasksUsage.avgRunTime;this.workerLastVirtualTaskTimestamp.set(e,{start:r,end:r+(t??0)})}}class w extends g{requiredStatistics={runTime:!0,avgRunTime:!1,medRunTime:!1};reset(){return!0}choose(){const e=this.pool.findFreeWorkerNodeKey();if(-1!==e)return e;let r,t=1/0;for(const[e,s]of this.pool.workerNodes.entries()){const i=s.tasksUsage.runTime;if(0===i)return e;i<t&&(t=i,r=e)}return r}remove(e){return!0}}class f extends g{reset(){return!0}choose(){const e=this.pool.findFreeWorkerNodeKey();if(-1!==e)return e;let r,t=1/0;for(const[e,s]of this.pool.workerNodes.entries()){const i=s.tasksUsage,o=i.run+i.running;if(0===o)return e;o<t&&(t=o,r=e)}return r}remove(e){return!0}}class W extends g{nextWorkerNodeId=0;reset(){return this.nextWorkerNodeId=0,!0}choose(){const e=this.nextWorkerNodeId;return this.nextWorkerNodeId=this.nextWorkerNodeId===this.pool.workerNodes.length-1?0:this.nextWorkerNodeId+1,e}remove(e){return this.nextWorkerNodeId===e&&(0===this.pool.workerNodes.length?this.nextWorkerNodeId=0:this.nextWorkerNodeId=this.nextWorkerNodeId>this.pool.workerNodes.length-1?this.pool.workerNodes.length-1:this.nextWorkerNodeId),!0}}class y extends g{requiredStatistics={runTime:!0,avgRunTime:!0,medRunTime:!1};currentWorkerNodeId=0;defaultWorkerWeight;workersTaskRunTime=new Map;constructor(e,r){super(e,r),this.defaultWorkerWeight=this.computeWorkerWeight(),this.initWorkersTaskRunTime()}reset(){return this.currentWorkerNodeId=0,this.workersTaskRunTime.clear(),this.initWorkersTaskRunTime(),!0}choose(){const e=this.currentWorkerNodeId;this.isDynamicPool&&!this.workersTaskRunTime.has(e)&&this.initWorkerTaskRunTime(e);const r=this.workersTaskRunTime.get(e)?.runTime??0,t=this.workersTaskRunTime.get(e)?.weight??this.defaultWorkerWeight;return r<t?this.setWorkerTaskRunTime(e,t,r+(this.getWorkerVirtualTaskRunTime(e)??0)):(this.currentWorkerNodeId=this.currentWorkerNodeId===this.pool.workerNodes.length-1?0:this.currentWorkerNodeId+1,this.setWorkerTaskRunTime(this.currentWorkerNodeId,t,0)),e}remove(e){this.currentWorkerNodeId===e&&(0===this.pool.workerNodes.length?this.currentWorkerNodeId=0:this.currentWorkerNodeId=this.currentWorkerNodeId>this.pool.workerNodes.length-1?this.pool.workerNodes.length-1:this.currentWorkerNodeId);const r=this.workersTaskRunTime.delete(e);for(const[r,t]of this.workersTaskRunTime)r>e&&this.workersTaskRunTime.set(r-1,t);return r}initWorkersTaskRunTime(){for(const[e]of this.pool.workerNodes.entries())this.initWorkerTaskRunTime(e)}initWorkerTaskRunTime(e){this.setWorkerTaskRunTime(e,this.defaultWorkerWeight,0)}setWorkerTaskRunTime(e,r,t){this.workersTaskRunTime.set(e,{weight:r,runTime:t})}getWorkerVirtualTaskRunTime(e){return this.requiredStatistics.medRunTime?this.pool.workerNodes[e].tasksUsage.medRunTime:this.pool.workerNodes[e].tasksUsage.avgRunTime}computeWorkerWeight(){let e=0;for(const r of s()){const t=r.speed.toString().length-1;e+=1/(r.speed/Math.pow(10,t))*Math.pow(10,t)}return Math.round(e/s().length)}}class N{workerChoiceStrategyType;workerChoiceStrategies;constructor(e,r=p.ROUND_ROBIN,t={medRunTime:!1}){this.workerChoiceStrategyType=r,this.execute.bind(this),this.workerChoiceStrategies=new Map([[p.ROUND_ROBIN,new W(e,t)],[p.LESS_USED,new f(e,t)],[p.LESS_BUSY,new w(e,t)],[p.FAIR_SHARE,new T(e,t)],[p.WEIGHTED_ROUND_ROBIN,new y(e,t)]])}getRequiredStatistics(){return this.workerChoiceStrategies.get(this.workerChoiceStrategyType).requiredStatistics}setWorkerChoiceStrategy(e){this.workerChoiceStrategyType!==e&&(this.workerChoiceStrategyType=e),this.workerChoiceStrategies.get(this.workerChoiceStrategyType)?.reset()}execute(){return this.workerChoiceStrategies.get(this.workerChoiceStrategyType).choose()}remove(e){return this.workerChoiceStrategies.get(this.workerChoiceStrategyType).remove(e)}}class S extends Array{size;constructor(e=1024,...r){super(),this.checkSize(e),this.size=e,arguments.length>1&&this.push(...r)}push(...e){const r=super.push(...e);return r>this.size&&super.splice(0,r-this.size),this.length}unshift(...e){return super.unshift(...e)>this.size&&super.splice(this.size,e.length),this.length}concat(...e){const r=super.concat(e);return r.size=this.size,r.length>r.size&&r.splice(0,r.length-r.size),r}splice(e,r,...t){let s;return arguments.length>=3&&void 0!==r?(s=super.splice(e,r),this.push(...t)):s=2===arguments.length?super.splice(e,r):super.splice(e),s}resize(e){if(this.checkSize(e),0===e)this.length=0;else if(e<this.size)for(let r=e;r<this.size;r++)super.pop();this.size=e}empty(){return 0===this.length}full(){return this.length===this.size}checkSize(e){if(!Number.isSafeInteger(e))throw new TypeError(`Invalid circular array size: ${e} is not a safe integer`);if(e<0)throw new RangeError(`Invalid circular array size: ${e} < 0`)}}class R{numberOfWorkers;filePath;opts;workerNodes=[];emitter;promiseResponseMap=new Map;workerChoiceStrategyContext;constructor(e,r,t){if(this.numberOfWorkers=e,this.filePath=r,this.opts=t,!this.isMain())throw new Error("Cannot start a pool from a worker!");this.checkNumberOfWorkers(this.numberOfWorkers),this.checkFilePath(this.filePath),this.checkPoolOptions(this.opts),this.chooseWorkerNode.bind(this),this.executeTask.bind(this),this.enqueueTask.bind(this),this.checkAndEmitEvents.bind(this),this.setupHook();for(let e=1;e<=this.numberOfWorkers;e++)this.createAndSetupWorker();!0===this.opts.enableEvents&&(this.emitter=new d),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(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??p.ROUND_ROBIN,this.checkValidWorkerChoiceStrategy(this.opts.workerChoiceStrategy),this.opts.workerChoiceStrategyOptions=e.workerChoiceStrategyOptions??{medRunTime:!1},this.opts.enableEvents=e.enableEvents??!0,this.opts.enableTasksQueue=e.enableTasksQueue??!1}checkValidWorkerChoiceStrategy(e){if(!Object.values(p).includes(e))throw new Error(`Invalid worker choice strategy '${e}'`)}get numberOfRunningTasks(){return this.workerNodes.reduce(((e,r)=>e+r.tasksUsage.running),0)}get numberOfQueuedTasks(){return!1===this.opts.enableTasksQueue?0:this.workerNodes.reduce(((e,r)=>e+r.tasksQueue.length),0)}getWorkerNodeKey(e){return this.workerNodes.findIndex((r=>r.worker===e))}setWorkerChoiceStrategy(e){this.checkValidWorkerChoiceStrategy(e),this.opts.workerChoiceStrategy=e;for(const e of this.workerNodes)this.setWorkerNodeTasksUsage(e,{run:0,running:0,runTime:0,runTimeHistory:new S,avgRunTime:0,medRunTime:0,error:0});this.workerChoiceStrategyContext.setWorkerChoiceStrategy(e)}internalBusy(){return-1===this.findFreeWorkerNodeKey()}findFreeWorkerNodeKey(){return this.workerNodes.findIndex((e=>0===e.tasksUsage?.running))}async execute(e){const[t,s]=this.chooseWorkerNode(),i={data:e??{},id:r.randomUUID()},o=new Promise(((e,r)=>{this.promiseResponseMap.set(i.id,{resolve:e,reject:r,worker:s.worker})}));return!0===this.opts.enableTasksQueue&&(this.busy||this.workerNodes[t].tasksUsage.running>0)?this.enqueueTask(t,i):this.executeTask(t,i),this.checkAndEmitEvents(),o}async destroy(){await Promise.all(this.workerNodes.map((async(e,r)=>{this.flushTasksQueue(r),await this.destroyWorker(e.worker)})))}setupHook(){}beforeTaskExecutionHook(e){++this.workerNodes[e].tasksUsage.running}afterTaskExecutionHook(e,r){const t=this.getWorkerTasksUsage(e);--t.running,++t.run,null!=r.error&&++t.error,this.workerChoiceStrategyContext.getRequiredStatistics().runTime&&(t.runTime+=r.runTime??0,this.workerChoiceStrategyContext.getRequiredStatistics().avgRunTime&&0!==t.run&&(t.avgRunTime=t.runTime/t.run),this.workerChoiceStrategyContext.getRequiredStatistics().medRunTime&&(t.runTimeHistory.push(r.runTime??0),t.medRunTime=(e=>{if(Array.isArray(e)&&1===e.length)return e[0];const r=e.slice().sort(((e,r)=>e-r)),t=Math.floor(r.length/2);return r.length%2==0?r[t/2]:(r[t-1]+r[t])/2})(t.runTimeHistory)))}chooseWorkerNode(){let e;if(this.type===k.DYNAMIC&&!this.full&&this.internalBusy()){const r=this.createAndSetupWorker();this.registerWorkerMessageListener(r,(e=>{var t;t=l.HARD,(e.kill===t||null!=e.kill&&0===this.getWorkerTasksUsage(r)?.running)&&(this.flushTasksQueueByWorker(r),this.destroyWorker(r))})),e=this.getWorkerNodeKey(r)}else e=this.workerChoiceStrategyContext.execute();return[e,this.workerNodes[e]]}createAndSetupWorker(){const e=this.createWorker();return e.on("message",this.opts.messageHandler??c),e.on("error",this.opts.errorHandler??c),e.on("online",this.opts.onlineHandler??c),e.on("exit",this.opts.exitHandler??c),e.once("exit",(()=>{this.removeWorkerNode(e)})),this.pushWorkerNode(e),this.afterWorkerSetup(e),e}workerListener(){return e=>{if(null!=e.id){const r=this.promiseResponseMap.get(e.id);if(null!=r){null!=e.error?r.reject(e.error):r.resolve(e.data),this.afterTaskExecutionHook(r.worker,e),this.promiseResponseMap.delete(e.id);const t=this.getWorkerNodeKey(r.worker);!0===this.opts.enableTasksQueue&&this.tasksQueueSize(t)>0&&this.executeTask(t,this.dequeueTask(t))}}}}checkAndEmitEvents(){!0===this.opts.enableEvents&&(this.busy&&this.emitter?.emit(m.busy),this.type===k.DYNAMIC&&this.full&&this.emitter?.emit(m.full))}setWorkerNodeTasksUsage(e,r){e.tasksUsage=r}getWorkerTasksUsage(e){const r=this.getWorkerNodeKey(e);if(-1!==r)return this.workerNodes[r].tasksUsage;throw new Error("Worker could not be found in the pool worker nodes")}pushWorkerNode(e){return this.workerNodes.push({worker:e,tasksUsage:{run:0,running:0,runTime:0,runTimeHistory:new S,avgRunTime:0,medRunTime:0,error:0},tasksQueue:[]})}setWorkerNode(e,r,t,s){this.workerNodes[e]={worker:r,tasksUsage:t,tasksQueue:s}}removeWorkerNode(e){const r=this.getWorkerNodeKey(e);this.workerNodes.splice(r,1),this.workerChoiceStrategyContext.remove(r)}executeTask(e,r){this.beforeTaskExecutionHook(e),this.sendToWorker(this.workerNodes[e].worker,r)}enqueueTask(e,r){return this.workerNodes[e].tasksQueue.push(r)}dequeueTask(e){return this.workerNodes[e].tasksQueue.shift()}tasksQueueSize(e){return this.workerNodes[e].tasksQueue.length}flushTasksQueue(e){if(this.tasksQueueSize(e)>0)for(const r of this.workerNodes[e].tasksQueue)this.executeTask(e,r)}flushTasksQueueByWorker(e){const r=this.getWorkerNodeKey(e);this.flushTasksQueue(r)}}class I extends R{opts;constructor(e,r,t={}){super(e,r,t),this.opts=t}setupHook(){e.setupPrimary({...this.opts.settings,exec:this.filePath})}isMain(){return e.isPrimary}destroyWorker(e){this.sendToWorker(e,{kill:1}),e.kill()}sendToWorker(e,r){e.send(r)}registerWorkerMessageListener(e,r){e.on("message",r)}createWorker(){return e.fork(this.opts.env)}afterWorkerSetup(e){this.registerWorkerMessageListener(e,super.workerListener())}get type(){return k.FIXED}get full(){return this.workerNodes.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}class x extends I{max;constructor(e,r,t,s={}){super(e,t,s),this.max=r}get type(){return k.DYNAMIC}get full(){return this.workerNodes.length===this.max}get busy(){return this.full&&this.internalBusy()}}class v extends R{constructor(e,r,t={}){super(e,r,t)}isMain(){return i}async destroyWorker(e){this.sendToWorker(e,{kill:1}),await e.terminate()}sendToWorker(e,r){e.postMessage(r)}registerWorkerMessageListener(e,r){e.port2?.on("message",r)}createWorker(){return new o(this.filePath,{env:n})}afterWorkerSetup(e){const{port1:r,port2:t}=new a;e.postMessage({parent:r},[r]),e.port1=r,e.port2=t,this.registerWorkerMessageListener(e,super.workerListener())}get type(){return k.FIXED}get full(){return this.workerNodes.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}class C extends v{max;constructor(e,r,t,s={}){super(e,t,s),this.max=r}get type(){return k.DYNAMIC}get full(){return this.workerNodes.length===this.max}get busy(){return this.full&&this.internalBusy()}}const E=6e4,b=l.SOFT;class M extends u{isMain;mainWorker;opts;lastTaskTimestamp;aliveInterval;constructor(e,r,t,s,i={killBehavior:b,maxInactiveTime:E}){super(e),this.isMain=r,this.mainWorker=s,this.opts=i,this.checkFunctionInput(t),this.checkWorkerOptions(this.opts),this.isMain||(this.lastTaskTimestamp=performance.now(),this.aliveInterval=setInterval(this.checkAlive.bind(this),(this.opts.maxInactiveTime??E)/2),this.checkAlive.bind(this)()),this.mainWorker?.on("message",(e=>{this.messageListener(e,t)}))}messageListener(e,r){null!=e.data&&null!=e.id?!0===this.opts.async?this.runInAsyncScope(this.runAsync.bind(this),this,r,e):this.runInAsyncScope(this.run.bind(this),this,r,e):null!=e.parent?this.mainWorker=e.parent:null!=e.kill&&(null!=this.aliveInterval&&clearInterval(this.aliveInterval),this.emitDestroy())}checkWorkerOptions(e){this.opts.killBehavior=e.killBehavior??b,this.opts.maxInactiveTime=e.maxInactiveTime??E,this.opts.async=e.async??!1}checkFunctionInput(e){if(null==e)throw new Error("fn parameter is mandatory");if("function"!=typeof e)throw new TypeError("fn parameter is not a function")}getMainWorker(){if(null==this.mainWorker)throw new Error("Main worker was not set");return this.mainWorker}checkAlive(){performance.now()-this.lastTaskTimestamp>(this.opts.maxInactiveTime??E)&&this.sendToMainWorker({kill:this.opts.killBehavior})}handleError(e){return e}run(e,r){try{const t=performance.now(),s=e(r.data),i=performance.now()-t;this.sendToMainWorker({data:s,id:r.id,runTime:i})}catch(e){const t=this.handleError(e);this.sendToMainWorker({error:t,id:r.id})}finally{!this.isMain&&(this.lastTaskTimestamp=performance.now())}}runAsync(e,r){const t=performance.now();e(r.data).then((e=>{const s=performance.now()-t;return this.sendToMainWorker({data:e,id:r.id,runTime:s}),null})).catch((e=>{const t=this.handleError(e);this.sendToMainWorker({error:t,id:r.id})})).finally((()=>{!this.isMain&&(this.lastTaskTimestamp=performance.now())})).catch(c)}}class O extends M{constructor(r,t={}){super("worker-cluster-pool:poolifier",e.isPrimary,r,e.worker,t)}sendToMainWorker(e){this.getMainWorker().send(e)}handleError(e){return e instanceof Error?e.message:e}}class U extends M{constructor(e,r={}){super("worker-thread-pool:poolifier",i,e,h,r)}sendToMainWorker(e){this.getMainWorker().postMessage(e)}}export{O as ClusterWorker,x as DynamicClusterPool,C as DynamicThreadPool,I as FixedClusterPool,v as FixedThreadPool,l as KillBehaviors,m as PoolEvents,U as ThreadWorker,p as WorkerChoiceStrategies};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { MessageValue, PromiseResponseWrapper } from '../utility-types';
|
|
2
2
|
import { type PoolOptions } from './pool';
|
|
3
3
|
import { PoolEmitter } from './pool';
|
|
4
|
-
import type { IPoolInternal
|
|
4
|
+
import type { IPoolInternal } from './pool-internal';
|
|
5
5
|
import { PoolType } from './pool-internal';
|
|
6
|
-
import type {
|
|
6
|
+
import type { IWorker, WorkerNode } from './worker';
|
|
7
7
|
import { type WorkerChoiceStrategy } from './selection-strategies/selection-strategies-types';
|
|
8
8
|
import { WorkerChoiceStrategyContext } from './selection-strategies/worker-choice-strategy-context';
|
|
9
9
|
/**
|
|
@@ -13,21 +13,21 @@ import { WorkerChoiceStrategyContext } from './selection-strategies/worker-choic
|
|
|
13
13
|
* @typeParam Data - Type of data sent to the worker. This can only be serializable data.
|
|
14
14
|
* @typeParam Response - Type of response of execution. This can only be serializable data.
|
|
15
15
|
*/
|
|
16
|
-
export declare abstract class AbstractPool<Worker extends
|
|
16
|
+
export declare abstract class AbstractPool<Worker extends IWorker, Data = unknown, Response = unknown> implements IPoolInternal<Worker, Data, Response> {
|
|
17
17
|
readonly numberOfWorkers: number;
|
|
18
18
|
readonly filePath: string;
|
|
19
19
|
readonly opts: PoolOptions<Worker>;
|
|
20
20
|
/** @inheritDoc */
|
|
21
|
-
readonly
|
|
21
|
+
readonly workerNodes: Array<WorkerNode<Worker, Data>>;
|
|
22
22
|
/** @inheritDoc */
|
|
23
23
|
readonly emitter?: PoolEmitter;
|
|
24
24
|
/**
|
|
25
|
-
* The
|
|
25
|
+
* The execution response promise map.
|
|
26
26
|
*
|
|
27
27
|
* - `key`: The message id of each submitted task.
|
|
28
|
-
* - `value`: An object that contains the worker, the promise resolve and reject callbacks.
|
|
28
|
+
* - `value`: An object that contains the worker, the execution response promise resolve and reject callbacks.
|
|
29
29
|
*
|
|
30
|
-
* When we receive a message from the worker we get a map entry with the promise resolve/reject bound to the message.
|
|
30
|
+
* When we receive a message from the worker, we get a map entry with the promise resolve/reject bound to the message id.
|
|
31
31
|
*/
|
|
32
32
|
protected promiseResponseMap: Map<string, PromiseResponseWrapper<Worker, Response>>;
|
|
33
33
|
/**
|
|
@@ -51,16 +51,20 @@ export declare abstract class AbstractPool<Worker extends IPoolWorker, Data = un
|
|
|
51
51
|
/** @inheritDoc */
|
|
52
52
|
abstract get type(): PoolType;
|
|
53
53
|
/**
|
|
54
|
-
* Number of tasks
|
|
54
|
+
* Number of tasks running in the pool.
|
|
55
55
|
*/
|
|
56
56
|
private get numberOfRunningTasks();
|
|
57
57
|
/**
|
|
58
|
-
*
|
|
58
|
+
* Number of tasks queued in the pool.
|
|
59
|
+
*/
|
|
60
|
+
private get numberOfQueuedTasks();
|
|
61
|
+
/**
|
|
62
|
+
* Gets the given worker its worker node key.
|
|
59
63
|
*
|
|
60
64
|
* @param worker - The worker.
|
|
61
|
-
* @returns The worker key if the worker is found in the pool, `-1` otherwise.
|
|
65
|
+
* @returns The worker node key if the worker is found in the pool worker nodes, `-1` otherwise.
|
|
62
66
|
*/
|
|
63
|
-
private
|
|
67
|
+
private getWorkerNodeKey;
|
|
64
68
|
/** @inheritDoc */
|
|
65
69
|
setWorkerChoiceStrategy(workerChoiceStrategy: WorkerChoiceStrategy): void;
|
|
66
70
|
/** @inheritDoc */
|
|
@@ -69,20 +73,19 @@ export declare abstract class AbstractPool<Worker extends IPoolWorker, Data = un
|
|
|
69
73
|
abstract get busy(): boolean;
|
|
70
74
|
protected internalBusy(): boolean;
|
|
71
75
|
/** @inheritDoc */
|
|
72
|
-
|
|
76
|
+
findFreeWorkerNodeKey(): number;
|
|
73
77
|
/** @inheritDoc */
|
|
74
78
|
execute(data: Data): Promise<Response>;
|
|
75
79
|
/** @inheritDoc */
|
|
76
80
|
destroy(): Promise<void>;
|
|
77
81
|
/**
|
|
78
|
-
* Shutdowns given worker
|
|
82
|
+
* Shutdowns the given worker.
|
|
79
83
|
*
|
|
80
|
-
* @param worker - A worker within `
|
|
84
|
+
* @param worker - A worker within `workerNodes`.
|
|
81
85
|
*/
|
|
82
86
|
protected abstract destroyWorker(worker: Worker): void | Promise<void>;
|
|
83
87
|
/**
|
|
84
|
-
* Setup hook
|
|
85
|
-
* to run code before workers are created in the abstract constructor.
|
|
88
|
+
* Setup hook to execute code before worker node are created in the abstract constructor.
|
|
86
89
|
* Can be overridden
|
|
87
90
|
*
|
|
88
91
|
* @virtual
|
|
@@ -93,28 +96,28 @@ export declare abstract class AbstractPool<Worker extends IPoolWorker, Data = un
|
|
|
93
96
|
*/
|
|
94
97
|
protected abstract isMain(): boolean;
|
|
95
98
|
/**
|
|
96
|
-
* Hook executed before the worker task
|
|
99
|
+
* Hook executed before the worker task execution.
|
|
97
100
|
* Can be overridden.
|
|
98
101
|
*
|
|
99
|
-
* @param
|
|
102
|
+
* @param workerNodeKey - The worker node key.
|
|
100
103
|
*/
|
|
101
|
-
protected
|
|
104
|
+
protected beforeTaskExecutionHook(workerNodeKey: number): void;
|
|
102
105
|
/**
|
|
103
|
-
* Hook executed after the worker task
|
|
106
|
+
* Hook executed after the worker task execution.
|
|
104
107
|
* Can be overridden.
|
|
105
108
|
*
|
|
106
109
|
* @param worker - The worker.
|
|
107
110
|
* @param message - The received message.
|
|
108
111
|
*/
|
|
109
|
-
protected
|
|
112
|
+
protected afterTaskExecutionHook(worker: Worker, message: MessageValue<Response>): void;
|
|
110
113
|
/**
|
|
111
|
-
* Chooses a worker for the next task.
|
|
114
|
+
* Chooses a worker node for the next task.
|
|
112
115
|
*
|
|
113
116
|
* The default uses a round robin algorithm to distribute the load.
|
|
114
117
|
*
|
|
115
|
-
* @returns [worker key, worker].
|
|
118
|
+
* @returns [worker node key, worker node].
|
|
116
119
|
*/
|
|
117
|
-
protected
|
|
120
|
+
protected chooseWorkerNode(): [number, WorkerNode<Worker, Data>];
|
|
118
121
|
/**
|
|
119
122
|
* Sends a message to the given worker.
|
|
120
123
|
*
|
|
@@ -123,7 +126,7 @@ export declare abstract class AbstractPool<Worker extends IPoolWorker, Data = un
|
|
|
123
126
|
*/
|
|
124
127
|
protected abstract sendToWorker(worker: Worker, message: MessageValue<Data>): void;
|
|
125
128
|
/**
|
|
126
|
-
* Registers a listener callback on
|
|
129
|
+
* Registers a listener callback on the given worker.
|
|
127
130
|
*
|
|
128
131
|
* @param worker - The worker which should register a listener.
|
|
129
132
|
* @param listener - The message listener callback.
|
|
@@ -134,55 +137,66 @@ export declare abstract class AbstractPool<Worker extends IPoolWorker, Data = un
|
|
|
134
137
|
*/
|
|
135
138
|
protected abstract createWorker(): Worker;
|
|
136
139
|
/**
|
|
137
|
-
* Function that can be hooked up when a worker has been newly created and moved to the
|
|
140
|
+
* Function that can be hooked up when a worker has been newly created and moved to the pool worker nodes.
|
|
138
141
|
*
|
|
139
142
|
* Can be used to update the `maxListeners` or binding the `main-worker`\<-\>`worker` connection if not bind by default.
|
|
140
143
|
*
|
|
141
144
|
* @param worker - The newly created worker.
|
|
142
|
-
* @virtual
|
|
143
145
|
*/
|
|
144
146
|
protected abstract afterWorkerSetup(worker: Worker): void;
|
|
145
147
|
/**
|
|
146
|
-
* Creates a new worker
|
|
148
|
+
* Creates a new worker and sets it up completely in the pool worker nodes.
|
|
147
149
|
*
|
|
148
150
|
* @returns New, completely set up worker.
|
|
149
151
|
*/
|
|
150
152
|
protected createAndSetupWorker(): Worker;
|
|
151
153
|
/**
|
|
152
|
-
* This function is the listener registered for each worker.
|
|
154
|
+
* This function is the listener registered for each worker message.
|
|
153
155
|
*
|
|
154
156
|
* @returns The listener function to execute when a message is received from a worker.
|
|
155
157
|
*/
|
|
156
158
|
protected workerListener(): (message: MessageValue<Response>) => void;
|
|
157
|
-
private
|
|
158
|
-
|
|
159
|
-
|
|
159
|
+
private checkAndEmitEvents;
|
|
160
|
+
/**
|
|
161
|
+
* Sets the given worker node its tasks usage in the pool.
|
|
162
|
+
*
|
|
163
|
+
* @param workerNode - The worker node.
|
|
164
|
+
* @param tasksUsage - The worker node tasks usage.
|
|
165
|
+
*/
|
|
166
|
+
private setWorkerNodeTasksUsage;
|
|
160
167
|
/**
|
|
161
|
-
* Gets the given worker tasks usage in the pool.
|
|
168
|
+
* Gets the given worker its tasks usage in the pool.
|
|
162
169
|
*
|
|
163
170
|
* @param worker - The worker.
|
|
164
171
|
* @returns The worker tasks usage.
|
|
165
172
|
*/
|
|
166
173
|
private getWorkerTasksUsage;
|
|
167
174
|
/**
|
|
168
|
-
* Pushes the given worker in the pool.
|
|
175
|
+
* Pushes the given worker in the pool worker nodes.
|
|
169
176
|
*
|
|
170
177
|
* @param worker - The worker.
|
|
171
|
-
* @
|
|
178
|
+
* @returns The worker nodes length.
|
|
172
179
|
*/
|
|
173
|
-
private
|
|
180
|
+
private pushWorkerNode;
|
|
174
181
|
/**
|
|
175
|
-
* Sets the given worker in the pool.
|
|
182
|
+
* Sets the given worker in the pool worker nodes.
|
|
176
183
|
*
|
|
177
|
-
* @param
|
|
184
|
+
* @param workerNodeKey - The worker node key.
|
|
178
185
|
* @param worker - The worker.
|
|
179
186
|
* @param tasksUsage - The worker tasks usage.
|
|
187
|
+
* @param tasksQueue - The worker task queue.
|
|
180
188
|
*/
|
|
181
|
-
private
|
|
189
|
+
private setWorkerNode;
|
|
182
190
|
/**
|
|
183
|
-
* Removes the given worker from the pool.
|
|
191
|
+
* Removes the given worker from the pool worker nodes.
|
|
184
192
|
*
|
|
185
|
-
* @param worker - The worker
|
|
193
|
+
* @param worker - The worker.
|
|
186
194
|
*/
|
|
187
|
-
|
|
195
|
+
private removeWorkerNode;
|
|
196
|
+
private executeTask;
|
|
197
|
+
private enqueueTask;
|
|
198
|
+
private dequeueTask;
|
|
199
|
+
private tasksQueueSize;
|
|
200
|
+
private flushTasksQueue;
|
|
201
|
+
private flushTasksQueueByWorker;
|
|
188
202
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IPool } from './pool';
|
|
2
|
-
import type {
|
|
2
|
+
import type { IWorker, WorkerNode } from './worker';
|
|
3
3
|
/**
|
|
4
4
|
* Internal pool types.
|
|
5
5
|
*
|
|
@@ -9,25 +9,6 @@ export declare enum PoolType {
|
|
|
9
9
|
FIXED = "fixed",
|
|
10
10
|
DYNAMIC = "dynamic"
|
|
11
11
|
}
|
|
12
|
-
/**
|
|
13
|
-
* Internal tasks usage statistics.
|
|
14
|
-
*/
|
|
15
|
-
export interface TasksUsage {
|
|
16
|
-
run: number;
|
|
17
|
-
running: number;
|
|
18
|
-
runTime: number;
|
|
19
|
-
avgRunTime: number;
|
|
20
|
-
error: number;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Internal worker type.
|
|
24
|
-
*
|
|
25
|
-
* @typeParam Worker - Type of worker type items which manages this pool.
|
|
26
|
-
*/
|
|
27
|
-
export interface WorkerType<Worker extends IPoolWorker> {
|
|
28
|
-
worker: Worker;
|
|
29
|
-
tasksUsage: TasksUsage;
|
|
30
|
-
}
|
|
31
12
|
/**
|
|
32
13
|
* Internal contract definition for a poolifier pool.
|
|
33
14
|
*
|
|
@@ -35,11 +16,11 @@ export interface WorkerType<Worker extends IPoolWorker> {
|
|
|
35
16
|
* @typeParam Data - Type of data sent to the worker. This can only be serializable data.
|
|
36
17
|
* @typeParam Response - Type of response of execution. This can only be serializable data.
|
|
37
18
|
*/
|
|
38
|
-
export interface IPoolInternal<Worker extends
|
|
19
|
+
export interface IPoolInternal<Worker extends IWorker, Data = unknown, Response = unknown> extends IPool<Data, Response> {
|
|
39
20
|
/**
|
|
40
|
-
* Pool worker
|
|
21
|
+
* Pool worker nodes.
|
|
41
22
|
*/
|
|
42
|
-
readonly
|
|
23
|
+
readonly workerNodes: Array<WorkerNode<Worker, Data>>;
|
|
43
24
|
/**
|
|
44
25
|
* Pool type.
|
|
45
26
|
*
|
|
@@ -59,13 +40,13 @@ export interface IPoolInternal<Worker extends IPoolWorker, Data = unknown, Respo
|
|
|
59
40
|
*/
|
|
60
41
|
readonly busy: boolean;
|
|
61
42
|
/**
|
|
62
|
-
* Finds a free worker key based on the number of tasks the worker has applied.
|
|
43
|
+
* Finds a free worker node key based on the number of tasks the worker has applied.
|
|
63
44
|
*
|
|
64
|
-
* If a worker is found with `0` running tasks, it is detected as free and its key is returned.
|
|
45
|
+
* If a worker is found with `0` running tasks, it is detected as free and its worker node key is returned.
|
|
65
46
|
*
|
|
66
47
|
* If no free worker is found, `-1` is returned.
|
|
67
48
|
*
|
|
68
|
-
* @returns A worker key if there is one, `-1` otherwise.
|
|
49
|
+
* @returns A worker node key if there is one, `-1` otherwise.
|
|
69
50
|
*/
|
|
70
|
-
|
|
51
|
+
findFreeWorkerNodeKey: () => number;
|
|
71
52
|
}
|
package/lib/pools/pool.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import EventEmitter from 'node:events';
|
|
3
|
-
import type { ErrorHandler, ExitHandler, MessageHandler, OnlineHandler } from './
|
|
4
|
-
import type { WorkerChoiceStrategy } from './selection-strategies/selection-strategies-types';
|
|
3
|
+
import type { ErrorHandler, ExitHandler, MessageHandler, OnlineHandler } from './worker';
|
|
4
|
+
import type { WorkerChoiceStrategy, WorkerChoiceStrategyOptions } from './selection-strategies/selection-strategies-types';
|
|
5
5
|
/**
|
|
6
6
|
* Pool events emitter.
|
|
7
7
|
*/
|
|
@@ -42,12 +42,23 @@ export interface PoolOptions<Worker> {
|
|
|
42
42
|
* The worker choice strategy to use in this pool.
|
|
43
43
|
*/
|
|
44
44
|
workerChoiceStrategy?: WorkerChoiceStrategy;
|
|
45
|
+
/**
|
|
46
|
+
* The worker choice strategy options.
|
|
47
|
+
*/
|
|
48
|
+
workerChoiceStrategyOptions?: WorkerChoiceStrategyOptions;
|
|
45
49
|
/**
|
|
46
50
|
* Pool events emission.
|
|
47
51
|
*
|
|
48
52
|
* @defaultValue true
|
|
49
53
|
*/
|
|
50
54
|
enableEvents?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Pool worker tasks queue.
|
|
57
|
+
*
|
|
58
|
+
* @experimental
|
|
59
|
+
* @defaultValue false
|
|
60
|
+
*/
|
|
61
|
+
enableTasksQueue?: boolean;
|
|
51
62
|
}
|
|
52
63
|
/**
|
|
53
64
|
* Contract definition for a poolifier pool.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { IPoolInternal } from '../pool-internal';
|
|
2
|
-
import type {
|
|
3
|
-
import type { IWorkerChoiceStrategy, RequiredStatistics } from './selection-strategies-types';
|
|
2
|
+
import type { IWorker } from '../worker';
|
|
3
|
+
import type { IWorkerChoiceStrategy, RequiredStatistics, WorkerChoiceStrategyOptions } from './selection-strategies-types';
|
|
4
4
|
/**
|
|
5
5
|
* Worker choice strategy abstract base class.
|
|
6
6
|
*
|
|
@@ -8,22 +8,25 @@ import type { IWorkerChoiceStrategy, RequiredStatistics } from './selection-stra
|
|
|
8
8
|
* @typeParam Data - Type of data sent to the worker. This can only be serializable data.
|
|
9
9
|
* @typeParam Response - Type of response of execution. This can only be serializable data.
|
|
10
10
|
*/
|
|
11
|
-
export declare abstract class AbstractWorkerChoiceStrategy<Worker extends
|
|
11
|
+
export declare abstract class AbstractWorkerChoiceStrategy<Worker extends IWorker, Data = unknown, Response = unknown> implements IWorkerChoiceStrategy {
|
|
12
12
|
protected readonly pool: IPoolInternal<Worker, Data, Response>;
|
|
13
|
+
protected readonly opts: WorkerChoiceStrategyOptions;
|
|
13
14
|
/** @inheritDoc */
|
|
14
15
|
protected readonly isDynamicPool: boolean;
|
|
15
16
|
/** @inheritDoc */
|
|
16
|
-
requiredStatistics: RequiredStatistics;
|
|
17
|
+
readonly requiredStatistics: RequiredStatistics;
|
|
17
18
|
/**
|
|
18
19
|
* Constructs a worker choice strategy bound to the pool.
|
|
19
20
|
*
|
|
20
21
|
* @param pool - The pool instance.
|
|
22
|
+
* @param opts - The worker choice strategy options.
|
|
21
23
|
*/
|
|
22
|
-
constructor(pool: IPoolInternal<Worker, Data, Response
|
|
24
|
+
constructor(pool: IPoolInternal<Worker, Data, Response>, opts?: WorkerChoiceStrategyOptions);
|
|
25
|
+
private checkOptions;
|
|
23
26
|
/** @inheritDoc */
|
|
24
27
|
abstract reset(): boolean;
|
|
25
28
|
/** @inheritDoc */
|
|
26
29
|
abstract choose(): number;
|
|
27
30
|
/** @inheritDoc */
|
|
28
|
-
abstract remove(
|
|
31
|
+
abstract remove(workerNodeKey: number): boolean;
|
|
29
32
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { IWorker } from '../worker';
|
|
2
2
|
import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy';
|
|
3
3
|
import type { IWorkerChoiceStrategy, RequiredStatistics } from './selection-strategies-types';
|
|
4
4
|
/**
|
|
@@ -9,11 +9,11 @@ import type { IWorkerChoiceStrategy, RequiredStatistics } from './selection-stra
|
|
|
9
9
|
* @typeParam Data - Type of data sent to the worker. This can only be serializable data.
|
|
10
10
|
* @typeParam Response - Type of response of execution. This can only be serializable data.
|
|
11
11
|
*/
|
|
12
|
-
export declare class FairShareWorkerChoiceStrategy<Worker extends
|
|
12
|
+
export declare class FairShareWorkerChoiceStrategy<Worker extends IWorker, Data = unknown, Response = unknown> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy {
|
|
13
13
|
/** @inheritDoc */
|
|
14
14
|
readonly requiredStatistics: RequiredStatistics;
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* Worker last virtual task execution timestamp.
|
|
17
17
|
*/
|
|
18
18
|
private readonly workerLastVirtualTaskTimestamp;
|
|
19
19
|
/** @inheritDoc */
|
|
@@ -21,11 +21,11 @@ export declare class FairShareWorkerChoiceStrategy<Worker extends IPoolWorker, D
|
|
|
21
21
|
/** @inheritDoc */
|
|
22
22
|
choose(): number;
|
|
23
23
|
/** @inheritDoc */
|
|
24
|
-
remove(
|
|
24
|
+
remove(workerNodeKey: number): boolean;
|
|
25
25
|
/**
|
|
26
26
|
* Computes worker last virtual task timestamp.
|
|
27
27
|
*
|
|
28
|
-
* @param
|
|
28
|
+
* @param workerNodeKey - The worker node key.
|
|
29
29
|
*/
|
|
30
30
|
private computeWorkerLastVirtualTaskTimestamp;
|
|
31
31
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { IWorker } from '../worker';
|
|
2
2
|
import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy';
|
|
3
3
|
import type { IWorkerChoiceStrategy, RequiredStatistics } from './selection-strategies-types';
|
|
4
4
|
/**
|
|
@@ -8,7 +8,7 @@ import type { IWorkerChoiceStrategy, RequiredStatistics } from './selection-stra
|
|
|
8
8
|
* @typeParam Data - Type of data sent to the worker. This can only be serializable data.
|
|
9
9
|
* @typeParam Response - Type of response of execution. This can only be serializable data.
|
|
10
10
|
*/
|
|
11
|
-
export declare class LessBusyWorkerChoiceStrategy<Worker extends
|
|
11
|
+
export declare class LessBusyWorkerChoiceStrategy<Worker extends IWorker, Data = unknown, Response = unknown> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy {
|
|
12
12
|
/** @inheritDoc */
|
|
13
13
|
readonly requiredStatistics: RequiredStatistics;
|
|
14
14
|
/** @inheritDoc */
|
|
@@ -16,5 +16,5 @@ export declare class LessBusyWorkerChoiceStrategy<Worker extends IPoolWorker, Da
|
|
|
16
16
|
/** @inheritDoc */
|
|
17
17
|
choose(): number;
|
|
18
18
|
/** @inheritDoc */
|
|
19
|
-
remove(
|
|
19
|
+
remove(workerNodeKey: number): boolean;
|
|
20
20
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { IWorker } from '../worker';
|
|
2
2
|
import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy';
|
|
3
3
|
import type { IWorkerChoiceStrategy } from './selection-strategies-types';
|
|
4
4
|
/**
|
|
@@ -8,11 +8,11 @@ import type { IWorkerChoiceStrategy } from './selection-strategies-types';
|
|
|
8
8
|
* @typeParam Data - Type of data sent to the worker. This can only be serializable data.
|
|
9
9
|
* @typeParam Response - Type of response of execution. This can only be serializable data.
|
|
10
10
|
*/
|
|
11
|
-
export declare class LessUsedWorkerChoiceStrategy<Worker extends
|
|
11
|
+
export declare class LessUsedWorkerChoiceStrategy<Worker extends IWorker, Data = unknown, Response = unknown> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy {
|
|
12
12
|
/** @inheritDoc */
|
|
13
13
|
reset(): boolean;
|
|
14
14
|
/** @inheritDoc */
|
|
15
15
|
choose(): number;
|
|
16
16
|
/** @inheritDoc */
|
|
17
|
-
remove(
|
|
17
|
+
remove(workerNodeKey: number): boolean;
|
|
18
18
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { IWorker } from '../worker';
|
|
2
2
|
import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy';
|
|
3
3
|
import type { IWorkerChoiceStrategy } from './selection-strategies-types';
|
|
4
4
|
/**
|
|
@@ -8,15 +8,15 @@ import type { IWorkerChoiceStrategy } from './selection-strategies-types';
|
|
|
8
8
|
* @typeParam Data - Type of data sent to the worker. This can only be serializable data.
|
|
9
9
|
* @typeParam Response - Type of response of execution. This can only be serializable data.
|
|
10
10
|
*/
|
|
11
|
-
export declare class RoundRobinWorkerChoiceStrategy<Worker extends
|
|
11
|
+
export declare class RoundRobinWorkerChoiceStrategy<Worker extends IWorker, Data = unknown, Response = unknown> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy {
|
|
12
12
|
/**
|
|
13
|
-
* Id of the next worker.
|
|
13
|
+
* Id of the next worker node.
|
|
14
14
|
*/
|
|
15
|
-
private
|
|
15
|
+
private nextWorkerNodeId;
|
|
16
16
|
/** @inheritDoc */
|
|
17
17
|
reset(): boolean;
|
|
18
18
|
/** @inheritDoc */
|
|
19
19
|
choose(): number;
|
|
20
20
|
/** @inheritDoc */
|
|
21
|
-
remove(
|
|
21
|
+
remove(workerNodeKey: number): boolean;
|
|
22
22
|
}
|
|
@@ -27,19 +27,29 @@ export declare const WorkerChoiceStrategies: Readonly<{
|
|
|
27
27
|
* Worker choice strategy.
|
|
28
28
|
*/
|
|
29
29
|
export type WorkerChoiceStrategy = keyof typeof WorkerChoiceStrategies;
|
|
30
|
+
/**
|
|
31
|
+
* Worker choice strategy options.
|
|
32
|
+
*/
|
|
33
|
+
export interface WorkerChoiceStrategyOptions {
|
|
34
|
+
/**
|
|
35
|
+
* Use tasks median run time instead of average run time.
|
|
36
|
+
*/
|
|
37
|
+
medRunTime?: boolean;
|
|
38
|
+
}
|
|
30
39
|
/**
|
|
31
40
|
* Pool worker tasks usage statistics requirements.
|
|
32
41
|
*/
|
|
33
42
|
export interface RequiredStatistics {
|
|
34
43
|
runTime: boolean;
|
|
35
44
|
avgRunTime: boolean;
|
|
45
|
+
medRunTime: boolean;
|
|
36
46
|
}
|
|
37
47
|
/**
|
|
38
48
|
* Worker choice strategy interface.
|
|
39
49
|
*/
|
|
40
50
|
export interface IWorkerChoiceStrategy {
|
|
41
51
|
/**
|
|
42
|
-
* Required
|
|
52
|
+
* Required tasks usage statistics.
|
|
43
53
|
*/
|
|
44
54
|
readonly requiredStatistics: RequiredStatistics;
|
|
45
55
|
/**
|
|
@@ -47,13 +57,13 @@ export interface IWorkerChoiceStrategy {
|
|
|
47
57
|
*/
|
|
48
58
|
reset: () => boolean;
|
|
49
59
|
/**
|
|
50
|
-
* Chooses a worker in the pool and returns its key.
|
|
60
|
+
* Chooses a worker node in the pool and returns its key.
|
|
51
61
|
*/
|
|
52
62
|
choose: () => number;
|
|
53
63
|
/**
|
|
54
|
-
* Removes a worker
|
|
64
|
+
* Removes a worker node key from strategy internals.
|
|
55
65
|
*
|
|
56
|
-
* @param
|
|
66
|
+
* @param workerNodeKey - The worker node key.
|
|
57
67
|
*/
|
|
58
|
-
remove: (
|
|
68
|
+
remove: (workerNodeKey: number) => boolean;
|
|
59
69
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { IPoolInternal } from '../pool-internal';
|
|
2
|
-
import type {
|
|
2
|
+
import type { IWorker } from '../worker';
|
|
3
3
|
import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy';
|
|
4
|
-
import type { IWorkerChoiceStrategy, RequiredStatistics } from './selection-strategies-types';
|
|
4
|
+
import type { IWorkerChoiceStrategy, RequiredStatistics, WorkerChoiceStrategyOptions } from './selection-strategies-types';
|
|
5
5
|
/**
|
|
6
6
|
* Selects the next worker with a weighted round robin scheduling algorithm.
|
|
7
7
|
* Loosely modeled after the weighted round robin queueing algorithm: https://en.wikipedia.org/wiki/Weighted_round_robin.
|
|
@@ -10,33 +10,34 @@ import type { IWorkerChoiceStrategy, RequiredStatistics } from './selection-stra
|
|
|
10
10
|
* @typeParam Data - Type of data sent to the worker. This can only be serializable data.
|
|
11
11
|
* @typeParam Response - Type of response of execution. This can only be serializable data.
|
|
12
12
|
*/
|
|
13
|
-
export declare class WeightedRoundRobinWorkerChoiceStrategy<Worker extends
|
|
13
|
+
export declare class WeightedRoundRobinWorkerChoiceStrategy<Worker extends IWorker, Data = unknown, Response = unknown> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy {
|
|
14
14
|
/** @inheritDoc */
|
|
15
15
|
readonly requiredStatistics: RequiredStatistics;
|
|
16
16
|
/**
|
|
17
|
-
* Worker id where the current task will be submitted.
|
|
17
|
+
* Worker node id where the current task will be submitted.
|
|
18
18
|
*/
|
|
19
|
-
private
|
|
19
|
+
private currentWorkerNodeId;
|
|
20
20
|
/**
|
|
21
21
|
* Default worker weight.
|
|
22
22
|
*/
|
|
23
23
|
private readonly defaultWorkerWeight;
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* Workers' virtual task runtime.
|
|
26
26
|
*/
|
|
27
27
|
private readonly workersTaskRunTime;
|
|
28
28
|
/**
|
|
29
29
|
* Constructs a worker choice strategy that selects with a weighted round robin scheduling algorithm.
|
|
30
30
|
*
|
|
31
31
|
* @param pool - The pool instance.
|
|
32
|
+
* @param opts - The worker choice strategy options.
|
|
32
33
|
*/
|
|
33
|
-
constructor(pool: IPoolInternal<Worker, Data, Response
|
|
34
|
+
constructor(pool: IPoolInternal<Worker, Data, Response>, opts?: WorkerChoiceStrategyOptions);
|
|
34
35
|
/** @inheritDoc */
|
|
35
36
|
reset(): boolean;
|
|
36
37
|
/** @inheritDoc */
|
|
37
38
|
choose(): number;
|
|
38
39
|
/** @inheritDoc */
|
|
39
|
-
remove(
|
|
40
|
+
remove(workerNodeKey: number): boolean;
|
|
40
41
|
private initWorkersTaskRunTime;
|
|
41
42
|
private initWorkerTaskRunTime;
|
|
42
43
|
private setWorkerTaskRunTime;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { IPoolInternal } from '../pool-internal';
|
|
2
|
-
import type {
|
|
3
|
-
import type { RequiredStatistics, WorkerChoiceStrategy } from './selection-strategies-types';
|
|
2
|
+
import type { IWorker } from '../worker';
|
|
3
|
+
import type { RequiredStatistics, WorkerChoiceStrategy, WorkerChoiceStrategyOptions } from './selection-strategies-types';
|
|
4
4
|
/**
|
|
5
5
|
* The worker choice strategy context.
|
|
6
6
|
*
|
|
@@ -8,17 +8,17 @@ import type { RequiredStatistics, WorkerChoiceStrategy } from './selection-strat
|
|
|
8
8
|
* @typeParam Data - Type of data sent to the worker. This can only be serializable data.
|
|
9
9
|
* @typeParam Response - Type of response of execution. This can only be serializable data.
|
|
10
10
|
*/
|
|
11
|
-
export declare class WorkerChoiceStrategyContext<Worker extends
|
|
11
|
+
export declare class WorkerChoiceStrategyContext<Worker extends IWorker, Data = unknown, Response = unknown> {
|
|
12
12
|
private workerChoiceStrategyType;
|
|
13
13
|
private readonly workerChoiceStrategies;
|
|
14
14
|
/**
|
|
15
15
|
* Worker choice strategy context constructor.
|
|
16
16
|
*
|
|
17
17
|
* @param pool - The pool instance.
|
|
18
|
-
* @param
|
|
19
|
-
* @param
|
|
18
|
+
* @param workerChoiceStrategyType - The worker choice strategy.
|
|
19
|
+
* @param opts - The worker choice strategy options.
|
|
20
20
|
*/
|
|
21
|
-
constructor(pool: IPoolInternal<Worker, Data, Response>, workerChoiceStrategyType?: WorkerChoiceStrategy);
|
|
21
|
+
constructor(pool: IPoolInternal<Worker, Data, Response>, workerChoiceStrategyType?: WorkerChoiceStrategy, opts?: WorkerChoiceStrategyOptions);
|
|
22
22
|
/**
|
|
23
23
|
* Gets the worker choice strategy in the context required statistics.
|
|
24
24
|
*
|
|
@@ -34,14 +34,14 @@ export declare class WorkerChoiceStrategyContext<Worker extends IPoolWorker, Dat
|
|
|
34
34
|
/**
|
|
35
35
|
* Executes the worker choice strategy algorithm in the context.
|
|
36
36
|
*
|
|
37
|
-
* @returns The key of the
|
|
37
|
+
* @returns The key of the worker node.
|
|
38
38
|
*/
|
|
39
39
|
execute(): number;
|
|
40
40
|
/**
|
|
41
|
-
* Removes a worker from the worker choice strategy in the context.
|
|
41
|
+
* Removes a worker node key from the worker choice strategy in the context.
|
|
42
42
|
*
|
|
43
|
-
* @param
|
|
43
|
+
* @param workerNodeKey - The key of the worker node.
|
|
44
44
|
* @returns `true` if the removal is successful, `false` otherwise.
|
|
45
45
|
*/
|
|
46
|
-
remove(
|
|
46
|
+
remove(workerNodeKey: number): boolean;
|
|
47
47
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CircularArray } from '../circular-array';
|
|
1
2
|
/**
|
|
2
3
|
* Callback invoked if the worker has received a message.
|
|
3
4
|
*/
|
|
@@ -15,9 +16,28 @@ export type OnlineHandler<Worker> = (this: Worker) => void;
|
|
|
15
16
|
*/
|
|
16
17
|
export type ExitHandler<Worker> = (this: Worker, code: number) => void;
|
|
17
18
|
/**
|
|
18
|
-
*
|
|
19
|
+
* Worker task interface.
|
|
19
20
|
*/
|
|
20
|
-
export interface
|
|
21
|
+
export interface Task<Data = unknown> {
|
|
22
|
+
data: Data;
|
|
23
|
+
id: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Worker tasks usage statistics.
|
|
27
|
+
*/
|
|
28
|
+
export interface TasksUsage {
|
|
29
|
+
run: number;
|
|
30
|
+
running: number;
|
|
31
|
+
runTime: number;
|
|
32
|
+
runTimeHistory: CircularArray<number>;
|
|
33
|
+
avgRunTime: number;
|
|
34
|
+
medRunTime: number;
|
|
35
|
+
error: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Worker interface.
|
|
39
|
+
*/
|
|
40
|
+
export interface IWorker {
|
|
21
41
|
/**
|
|
22
42
|
* Register an event listener.
|
|
23
43
|
*
|
|
@@ -33,3 +53,11 @@ export interface IPoolWorker {
|
|
|
33
53
|
*/
|
|
34
54
|
once: (event: 'exit', handler: ExitHandler<this>) => void;
|
|
35
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Worker node interface.
|
|
58
|
+
*/
|
|
59
|
+
export interface WorkerNode<Worker extends IWorker, Data = unknown> {
|
|
60
|
+
worker: Worker;
|
|
61
|
+
tasksUsage: TasksUsage;
|
|
62
|
+
tasksQueue: Array<Task<Data>>;
|
|
63
|
+
}
|
package/lib/utility-types.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { Worker as ClusterWorker } from 'node:cluster';
|
|
4
4
|
import type { MessagePort } from 'node:worker_threads';
|
|
5
5
|
import type { KillBehavior } from './worker/worker-options';
|
|
6
|
-
import type {
|
|
6
|
+
import type { IWorker } from './pools/worker';
|
|
7
7
|
/**
|
|
8
8
|
* Make all properties in T non-readonly.
|
|
9
9
|
*/
|
|
@@ -47,7 +47,7 @@ export interface MessageValue<Data = unknown, MainWorker extends ClusterWorker |
|
|
|
47
47
|
* @typeParam Worker - Type of worker.
|
|
48
48
|
* @typeParam Response - Type of execution response. This can only be serializable data.
|
|
49
49
|
*/
|
|
50
|
-
export interface PromiseResponseWrapper<Worker extends
|
|
50
|
+
export interface PromiseResponseWrapper<Worker extends IWorker, Response = unknown> {
|
|
51
51
|
/**
|
|
52
52
|
* Resolve callback to fulfill the promise.
|
|
53
53
|
*/
|
|
@@ -57,7 +57,7 @@ export interface PromiseResponseWrapper<Worker extends IPoolWorker, Response = u
|
|
|
57
57
|
*/
|
|
58
58
|
readonly reject: (reason?: string) => void;
|
|
59
59
|
/**
|
|
60
|
-
* The worker handling the
|
|
60
|
+
* The worker handling the execution.
|
|
61
61
|
*/
|
|
62
62
|
readonly worker: Worker;
|
|
63
63
|
}
|
package/lib/utils.d.ts
CHANGED
|
@@ -2,3 +2,10 @@
|
|
|
2
2
|
* An intentional empty function.
|
|
3
3
|
*/
|
|
4
4
|
export declare const EMPTY_FUNCTION: () => void;
|
|
5
|
+
/**
|
|
6
|
+
* Returns the median of the given data set.
|
|
7
|
+
*
|
|
8
|
+
* @param dataSet - Data set.
|
|
9
|
+
* @returns The median of the given data set.
|
|
10
|
+
*/
|
|
11
|
+
export declare const median: (dataSet: number[]) => number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "poolifier",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.6",
|
|
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",
|
|
@@ -77,20 +77,20 @@
|
|
|
77
77
|
"@commitlint/config-conventional": "^17.4.4",
|
|
78
78
|
"@release-it/bumper": "^4.0.2",
|
|
79
79
|
"@release-it/keep-a-changelog": "^3.1.0",
|
|
80
|
-
"@rollup/plugin-terser": "^0.4.
|
|
80
|
+
"@rollup/plugin-terser": "^0.4.1",
|
|
81
81
|
"@rollup/plugin-typescript": "^11.1.0",
|
|
82
82
|
"@types/node": "^18.15.11",
|
|
83
83
|
"@typescript-eslint/eslint-plugin": "^5.57.1",
|
|
84
84
|
"@typescript-eslint/parser": "^5.57.1",
|
|
85
85
|
"benny": "^3.7.1",
|
|
86
86
|
"c8": "^7.13.0",
|
|
87
|
-
"eslint": "^8.
|
|
87
|
+
"eslint": "^8.38.0",
|
|
88
88
|
"eslint-config-standard": "^17.0.0",
|
|
89
89
|
"eslint-config-standard-with-typescript": "^34.0.1",
|
|
90
90
|
"eslint-define-config": "^1.17.0",
|
|
91
91
|
"eslint-import-resolver-typescript": "^3.5.5",
|
|
92
92
|
"eslint-plugin-import": "^2.27.5",
|
|
93
|
-
"eslint-plugin-jsdoc": "^40.1.
|
|
93
|
+
"eslint-plugin-jsdoc": "^40.1.2",
|
|
94
94
|
"eslint-plugin-n": "^15.7.0",
|
|
95
95
|
"eslint-plugin-promise": "^6.1.1",
|
|
96
96
|
"eslint-plugin-spellcheck": "^0.0.20",
|