poolifier 2.4.5 → 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/lib/index.js +1 -1
- package/lib/index.mjs +1 -1
- package/lib/pools/abstract-pool.d.ts +13 -7
- package/package.json +2 -2
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 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.isDynamicPool=this.pool.type===e.DYNAMIC,this.choose.bind(this)}checkOptions(){this.requiredStatistics.avgRunTime&&!0===this.opts.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.internalExecute.bind(this),this.checkAndEmitEvents.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 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,r]of this.workerNodes.entries())this.setWorkerNode(e,r.worker,{run:0,running:0,runTime:0,runTimeHistory:new W,avgRunTime:0,medRunTime:0,error:0},r.tasksQueue);this.workerChoiceStrategyContext.setWorkerChoiceStrategy(e)}internalBusy(){return this.numberOfRunningTasks>=this.numberOfWorkers&&-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=this.internalExecute(r,s,i);let n=i;return!0===this.opts.enableTasksQueue&&(this.busy||this.tasksQueueSize(r)>0)&&(n=this.enqueueDequeueTask(r,i)),this.sendToWorker(s.worker,n),this.checkAndEmitEvents(),o}async destroy(){await Promise.all(this.workerNodes.map((async e=>{this.flushTasksQueueByWorker(e.worker),await this.destroyWorker(e.worker)})))}setupHook(){}beforePromiseResponseHook(e){++this.workerNodes[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),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||-1!==this.findFreeWorkerNodeKey())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.flushTasksQueueByWorker(e),this.destroyWorker(e))})),r=this.getWorkerNodeKey(e)}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.afterPromiseResponseHook(r.worker,e),this.promiseResponseMap.delete(e.id);const t=this.getWorkerNodeKey(r.worker);!0===this.opts.enableTasksQueue&&this.tasksQueueSize(t)>0&&this.sendToWorker(r.worker,this.dequeueTask(t))}}}}async internalExecute(e,r,t){return this.beforePromiseResponseHook(e),await new Promise(((e,s)=>{this.promiseResponseMap.set(t.id,{resolve:e,reject:s,worker:r.worker})}))}checkAndEmitEvents(){!0===this.opts.enableEvents&&(this.busy&&this.emitter?.emit(k.busy),this.type===e.DYNAMIC&&this.full&&this.emitter?.emit(k.full))}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)}enqueueDequeueTask(e,r){return this.enqueueTask(e,r),this.dequeueTask(e)}enqueueTask(e,r){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.sendToWorker(this.workerNodes[e].worker,r);this.workerNodes[e].tasksQueue=[]}}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&&-1===this.findFreeWorkerNodeKey()}},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&&-1===this.findFreeWorkerNodeKey()}},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;
|
|
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 h,parentPort as a}from"node:worker_threads";import{AsyncResource as u}from"node:async_hooks";var k;!function(e){e.FIXED="fixed",e.DYNAMIC="dynamic"}(k||(k={}));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.isDynamicPool=this.pool.type===k.DYNAMIC,this.choose.bind(this)}checkOptions(){this.requiredStatistics.avgRunTime&&!0===this.opts.medRunTime&&(this.requiredStatistics.medRunTime=!0)}}class w 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 T 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 T(e,t)],[p.FAIR_SHARE,new w(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.internalExecute.bind(this),this.checkAndEmitEvents.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 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,r]of this.workerNodes.entries())this.setWorkerNode(e,r.worker,{run:0,running:0,runTime:0,runTimeHistory:new S,avgRunTime:0,medRunTime:0,error:0},r.tasksQueue);this.workerChoiceStrategyContext.setWorkerChoiceStrategy(e)}internalBusy(){return this.numberOfRunningTasks>=this.numberOfWorkers&&-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=this.internalExecute(t,s,i);let n=i;return!0===this.opts.enableTasksQueue&&(this.busy||this.tasksQueueSize(t)>0)&&(n=this.enqueueDequeueTask(t,i)),this.sendToWorker(s.worker,n),this.checkAndEmitEvents(),o}async destroy(){await Promise.all(this.workerNodes.map((async e=>{this.flushTasksQueueByWorker(e.worker),await this.destroyWorker(e.worker)})))}setupHook(){}beforePromiseResponseHook(e){++this.workerNodes[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),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||-1!==this.findFreeWorkerNodeKey())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.flushTasksQueueByWorker(r),this.destroyWorker(r))})),e=this.getWorkerNodeKey(r)}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.afterPromiseResponseHook(r.worker,e),this.promiseResponseMap.delete(e.id);const t=this.getWorkerNodeKey(r.worker);!0===this.opts.enableTasksQueue&&this.tasksQueueSize(t)>0&&this.sendToWorker(r.worker,this.dequeueTask(t))}}}}async internalExecute(e,r,t){return this.beforePromiseResponseHook(e),await new Promise(((e,s)=>{this.promiseResponseMap.set(t.id,{resolve:e,reject:s,worker:r.worker})}))}checkAndEmitEvents(){!0===this.opts.enableEvents&&(this.busy&&this.emitter?.emit(m.busy),this.type===k.DYNAMIC&&this.full&&this.emitter?.emit(m.full))}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)}enqueueDequeueTask(e,r){return this.enqueueTask(e,r),this.dequeueTask(e)}enqueueTask(e,r){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.sendToWorker(this.workerNodes[e].worker,r);this.workerNodes[e].tasksQueue=[]}}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 v 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&&-1===this.findFreeWorkerNodeKey()}}class x 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 h;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 x{max;constructor(e,r,t,s={}){super(e,t,s),this.max=r}get type(){return k.DYNAMIC}get full(){return this.workerNodes.length===this.max}get busy(){return this.full&&-1===this.findFreeWorkerNodeKey()}}const b=6e4,E=l.SOFT;class M extends u{isMain;mainWorker;opts;lastTaskTimestamp;aliveInterval;constructor(e,r,t,s,i={killBehavior:E,maxInactiveTime:b}){super(e),this.isMain=r,this.mainWorker=s,this.opts=i,this.checkFunctionInput(t),this.checkWorkerOptions(this.opts),this.isMain||(this.lastTaskTimestamp=performance.now(),this.aliveInterval=setInterval(this.checkAlive.bind(this),(this.opts.maxInactiveTime??b)/2),this.checkAlive.bind(this)()),this.mainWorker?.on("message",(e=>{this.messageListener(e,t)}))}messageListener(e,r){null!=e.data&&null!=e.id?!0===this.opts.async?this.runInAsyncScope(this.runAsync.bind(this),this,r,e):this.runInAsyncScope(this.run.bind(this),this,r,e):null!=e.parent?this.mainWorker=e.parent:null!=e.kill&&(null!=this.aliveInterval&&clearInterval(this.aliveInterval),this.emitDestroy())}checkWorkerOptions(e){this.opts.killBehavior=e.killBehavior??E,this.opts.maxInactiveTime=e.maxInactiveTime??b,this.opts.async=e.async??!1}checkFunctionInput(e){if(null==e)throw new Error("fn parameter is mandatory");if("function"!=typeof e)throw new TypeError("fn parameter is not a function")}getMainWorker(){if(null==this.mainWorker)throw new Error("Main worker was not set");return this.mainWorker}checkAlive(){performance.now()-this.lastTaskTimestamp>(this.opts.maxInactiveTime??b)&&this.sendToMainWorker({kill:this.opts.killBehavior})}handleError(e){return e}run(e,r){try{const t=performance.now(),s=e(r.data),i=performance.now()-t;this.sendToMainWorker({data:s,id:r.id,runTime:i})}catch(e){const t=this.handleError(e);this.sendToMainWorker({error:t,id:r.id})}finally{!this.isMain&&(this.lastTaskTimestamp=performance.now())}}runAsync(e,r){const t=performance.now();e(r.data).then((e=>{const s=performance.now()-t;return this.sendToMainWorker({data:e,id:r.id,runTime:s}),null})).catch((e=>{const t=this.handleError(e);this.sendToMainWorker({error:t,id:r.id})})).finally((()=>{!this.isMain&&(this.lastTaskTimestamp=performance.now())})).catch(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 D extends M{constructor(e,r={}){super("worker-thread-pool:poolifier",i,e,a,r)}sendToMainWorker(e){this.getMainWorker().postMessage(e)}}export{O as ClusterWorker,v as DynamicClusterPool,C as DynamicThreadPool,I as FixedClusterPool,x as FixedThreadPool,l as KillBehaviors,m as PoolEvents,D as ThreadWorker,p 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};
|
|
@@ -85,7 +85,7 @@ export declare abstract class AbstractPool<Worker extends IWorker, Data = unknow
|
|
|
85
85
|
*/
|
|
86
86
|
protected abstract destroyWorker(worker: Worker): void | Promise<void>;
|
|
87
87
|
/**
|
|
88
|
-
* Setup hook to
|
|
88
|
+
* Setup hook to execute code before worker node are created in the abstract constructor.
|
|
89
89
|
* Can be overridden
|
|
90
90
|
*
|
|
91
91
|
* @virtual
|
|
@@ -96,20 +96,20 @@ export declare abstract class AbstractPool<Worker extends IWorker, Data = unknow
|
|
|
96
96
|
*/
|
|
97
97
|
protected abstract isMain(): boolean;
|
|
98
98
|
/**
|
|
99
|
-
* Hook executed before the worker task
|
|
99
|
+
* Hook executed before the worker task execution.
|
|
100
100
|
* Can be overridden.
|
|
101
101
|
*
|
|
102
102
|
* @param workerNodeKey - The worker node key.
|
|
103
103
|
*/
|
|
104
|
-
protected
|
|
104
|
+
protected beforeTaskExecutionHook(workerNodeKey: number): void;
|
|
105
105
|
/**
|
|
106
|
-
* Hook executed after the worker task
|
|
106
|
+
* Hook executed after the worker task execution.
|
|
107
107
|
* Can be overridden.
|
|
108
108
|
*
|
|
109
109
|
* @param worker - The worker.
|
|
110
110
|
* @param message - The received message.
|
|
111
111
|
*/
|
|
112
|
-
protected
|
|
112
|
+
protected afterTaskExecutionHook(worker: Worker, message: MessageValue<Response>): void;
|
|
113
113
|
/**
|
|
114
114
|
* Chooses a worker node for the next task.
|
|
115
115
|
*
|
|
@@ -156,8 +156,14 @@ export declare abstract class AbstractPool<Worker extends IWorker, Data = unknow
|
|
|
156
156
|
* @returns The listener function to execute when a message is received from a worker.
|
|
157
157
|
*/
|
|
158
158
|
protected workerListener(): (message: MessageValue<Response>) => void;
|
|
159
|
-
private internalExecute;
|
|
160
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;
|
|
161
167
|
/**
|
|
162
168
|
* Gets the given worker its tasks usage in the pool.
|
|
163
169
|
*
|
|
@@ -187,7 +193,7 @@ export declare abstract class AbstractPool<Worker extends IWorker, Data = unknow
|
|
|
187
193
|
* @param worker - The worker.
|
|
188
194
|
*/
|
|
189
195
|
private removeWorkerNode;
|
|
190
|
-
private
|
|
196
|
+
private executeTask;
|
|
191
197
|
private enqueueTask;
|
|
192
198
|
private dequeueTask;
|
|
193
199
|
private tasksQueueSize;
|
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,7 +77,7 @@
|
|
|
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",
|