poolifier 2.3.1 → 2.3.3
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 +14 -12
- package/lib/index.d.ts +23 -17
- package/lib/index.js +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -39,16 +39,16 @@ Please consult our <a href="#general-guidance">general guidelines</a>
|
|
|
39
39
|
- Performance :racehorse: [benchmarks](./benchmarks/README.md)
|
|
40
40
|
- Security :bank: :cop: [](https://sonarcloud.io/dashboard?id=pioardi_poolifier) [](https://sonarcloud.io/dashboard?id=pioardi_poolifier)
|
|
41
41
|
- Easy to use :couple:
|
|
42
|
-
- Easy switch from a pool to another, easy to tune :
|
|
43
|
-
- Dynamic pool size :
|
|
44
|
-
- No runtime dependencies :
|
|
45
|
-
- Proper async integration with node async hooks :
|
|
46
|
-
- Support for worker threads and cluster node modules :
|
|
47
|
-
- Support sync and async tasks :
|
|
48
|
-
- General guidance on pools to use :
|
|
49
|
-
- Widely tested :
|
|
50
|
-
- Error handling out of the box :
|
|
51
|
-
- Active community :
|
|
42
|
+
- Easy switch from a pool to another, easy to tune :white_check_mark:
|
|
43
|
+
- Dynamic pool size :white_check_mark:
|
|
44
|
+
- No runtime dependencies :white_check_mark:
|
|
45
|
+
- Proper async integration with node async hooks :white_check_mark:
|
|
46
|
+
- Support for worker threads and cluster node modules :white_check_mark:
|
|
47
|
+
- Support sync and async tasks :white_check_mark:
|
|
48
|
+
- General guidance on pools to use :white_check_mark:
|
|
49
|
+
- Widely tested :white_check_mark:
|
|
50
|
+
- Error handling out of the box :white_check_mark:
|
|
51
|
+
- Active community :white_check_mark:
|
|
52
52
|
- Code quality :octocat: [](https://sonarcloud.io/dashboard?id=pioardi_poolifier)
|
|
53
53
|
[](https://sonarcloud.io/dashboard?id=pioardi_poolifier)
|
|
54
54
|
[](https://sonarcloud.io/dashboard?id=pioardi_poolifier)
|
|
@@ -146,10 +146,12 @@ Remember that workers can only send and receive serializable data.
|
|
|
146
146
|
|
|
147
147
|
## Node versions
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
Node versions >= 16.x are supported.
|
|
150
150
|
|
|
151
151
|
## API
|
|
152
152
|
|
|
153
|
+
### [Documentation](https://poolifier.github.io/poolifier/)
|
|
154
|
+
|
|
153
155
|
### `pool = new FixedThreadPool/FixedClusterPool(numberOfThreads/numberOfWorkers, filePath, opts)`
|
|
154
156
|
|
|
155
157
|
`numberOfThreads/numberOfWorkers` (mandatory) Number of workers for this pool
|
|
@@ -248,7 +250,7 @@ But in general, **always profile your application**
|
|
|
248
250
|
## Contribute
|
|
249
251
|
|
|
250
252
|
See guidelines [CONTRIBUTING](CONTRIBUTING.md)
|
|
251
|
-
Choose your task here [2.3.
|
|
253
|
+
Choose your task here [2.3.x](https://github.com/orgs/poolifier/projects/1), propose an idea, a fix, an improvement.
|
|
252
254
|
|
|
253
255
|
## Team
|
|
254
256
|
|
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import EventEmitter from "events";
|
|
3
|
-
import { Worker } from "cluster";
|
|
3
|
+
import { ClusterSettings, Worker } from "cluster";
|
|
4
4
|
import { Worker as ClusterWorker } from "cluster";
|
|
5
5
|
import { MessagePort, MessageChannel } from "worker_threads";
|
|
6
6
|
import { Worker as Worker$0 } from "worker_threads";
|
|
@@ -331,7 +331,7 @@ interface IPoolInternal<Worker extends IPoolWorker, Data = unknown, Response = u
|
|
|
331
331
|
*/
|
|
332
332
|
readonly numberOfRunningTasks: number;
|
|
333
333
|
/**
|
|
334
|
-
*
|
|
334
|
+
* Finds a free worker based on the number of tasks the worker has applied.
|
|
335
335
|
*
|
|
336
336
|
* If a worker is found with `0` running tasks, it is detected as free and returned.
|
|
337
337
|
*
|
|
@@ -341,21 +341,21 @@ interface IPoolInternal<Worker extends IPoolWorker, Data = unknown, Response = u
|
|
|
341
341
|
*/
|
|
342
342
|
findFreeWorker(): Worker | false;
|
|
343
343
|
/**
|
|
344
|
-
*
|
|
344
|
+
* Gets worker index.
|
|
345
345
|
*
|
|
346
346
|
* @param worker The worker.
|
|
347
347
|
* @returns The worker index.
|
|
348
348
|
*/
|
|
349
349
|
getWorkerIndex(worker: Worker): number;
|
|
350
350
|
/**
|
|
351
|
-
*
|
|
351
|
+
* Gets worker running tasks.
|
|
352
352
|
*
|
|
353
353
|
* @param worker The worker.
|
|
354
354
|
* @returns The number of tasks currently running on the worker.
|
|
355
355
|
*/
|
|
356
356
|
getWorkerRunningTasks(worker: Worker): number | undefined;
|
|
357
357
|
/**
|
|
358
|
-
*
|
|
358
|
+
* Gets worker average tasks runtime.
|
|
359
359
|
*
|
|
360
360
|
* @param worker The worker.
|
|
361
361
|
* @returns The average tasks runtime on the worker.
|
|
@@ -510,7 +510,7 @@ declare abstract class AbstractPool<Worker extends IPoolWorker, Data = unknown,
|
|
|
510
510
|
/**
|
|
511
511
|
* Removes the given worker from the pool.
|
|
512
512
|
*
|
|
513
|
-
* @param worker
|
|
513
|
+
* @param worker The worker that will be removed.
|
|
514
514
|
*/
|
|
515
515
|
protected removeWorker(worker: Worker): void;
|
|
516
516
|
/**
|
|
@@ -531,8 +531,8 @@ declare abstract class AbstractPool<Worker extends IPoolWorker, Data = unknown,
|
|
|
531
531
|
/**
|
|
532
532
|
* Registers a listener callback on a given worker.
|
|
533
533
|
*
|
|
534
|
-
* @param worker
|
|
535
|
-
* @param listener
|
|
534
|
+
* @param worker The worker which should register a listener.
|
|
535
|
+
* @param listener The message listener callback.
|
|
536
536
|
*/
|
|
537
537
|
protected abstract registerWorkerMessageListener<Message extends Data | Response>(worker: Worker, listener: (message: MessageValue<Message>) => void): void;
|
|
538
538
|
protected internalExecute(worker: Worker, messageId: number): Promise<Response>;
|
|
@@ -624,6 +624,12 @@ interface ClusterPoolOptions extends PoolOptions<Worker> {
|
|
|
624
624
|
*/
|
|
625
625
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
626
626
|
env?: any;
|
|
627
|
+
/**
|
|
628
|
+
* Cluster settings.
|
|
629
|
+
*
|
|
630
|
+
* @see https://nodejs.org/api/cluster.html#cluster_cluster_settings
|
|
631
|
+
*/
|
|
632
|
+
settings?: ClusterSettings;
|
|
627
633
|
}
|
|
628
634
|
/**
|
|
629
635
|
* A cluster pool with a fixed number of workers.
|
|
@@ -644,7 +650,7 @@ declare class FixedClusterPool<Data = unknown, Response = unknown> extends Abstr
|
|
|
644
650
|
*
|
|
645
651
|
* @param numberOfWorkers Number of workers for this pool.
|
|
646
652
|
* @param filePath Path to an implementation of a `ClusterWorker` file, which can be relative or absolute.
|
|
647
|
-
* @param
|
|
653
|
+
* @param opts Options for this fixed cluster pool.
|
|
648
654
|
*/
|
|
649
655
|
constructor(numberOfWorkers: number, filePath: string, opts?: ClusterPoolOptions);
|
|
650
656
|
/** @inheritDoc */
|
|
@@ -685,7 +691,7 @@ declare class DynamicClusterPool<Data = unknown, Response = unknown> extends Fix
|
|
|
685
691
|
* @param min Minimum number of workers which are always active.
|
|
686
692
|
* @param max Maximum number of workers that can be created by this pool.
|
|
687
693
|
* @param filePath Path to an implementation of a `ClusterWorker` file, which can be relative or absolute.
|
|
688
|
-
* @param
|
|
694
|
+
* @param opts Options for this dynamic cluster pool.
|
|
689
695
|
*/
|
|
690
696
|
constructor(min: number, max: number, filePath: string, opts?: ClusterPoolOptions);
|
|
691
697
|
/** @inheritDoc */
|
|
@@ -715,7 +721,7 @@ declare class FixedThreadPool<Data = unknown, Response = unknown> extends Abstra
|
|
|
715
721
|
*
|
|
716
722
|
* @param numberOfThreads Number of threads for this pool.
|
|
717
723
|
* @param filePath Path to an implementation of a `ThreadWorker` file, which can be relative or absolute.
|
|
718
|
-
* @param
|
|
724
|
+
* @param opts Options for this fixed thread pool.
|
|
719
725
|
*/
|
|
720
726
|
constructor(numberOfThreads: number, filePath: string, opts?: PoolOptions<ThreadWorkerWithMessageChannel>);
|
|
721
727
|
/** @inheritDoc */
|
|
@@ -754,7 +760,7 @@ declare class DynamicThreadPool<Data = unknown, Response = unknown> extends Fixe
|
|
|
754
760
|
* @param min Minimum number of threads which are always active.
|
|
755
761
|
* @param max Maximum number of threads that can be created by this pool.
|
|
756
762
|
* @param filePath Path to an implementation of a `ThreadWorker` file, which can be relative or absolute.
|
|
757
|
-
* @param
|
|
763
|
+
* @param opts Options for this dynamic thread pool.
|
|
758
764
|
*/
|
|
759
765
|
constructor(min: number, max: number, filePath: string, opts?: PoolOptions<ThreadWorkerWithMessageChannel>);
|
|
760
766
|
/** @inheritDoc */
|
|
@@ -796,7 +802,7 @@ declare abstract class AbstractWorker<MainWorker extends Worker | MessagePort, D
|
|
|
796
802
|
protected messageListener(value: MessageValue<Data, MainWorker>, fn: (data: Data) => Response): void;
|
|
797
803
|
private checkWorkerOptions;
|
|
798
804
|
/**
|
|
799
|
-
*
|
|
805
|
+
* Checks if the `fn` parameter is passed to the constructor.
|
|
800
806
|
*
|
|
801
807
|
* @param fn The function that should be defined.
|
|
802
808
|
*/
|
|
@@ -808,7 +814,7 @@ declare abstract class AbstractWorker<MainWorker extends Worker | MessagePort, D
|
|
|
808
814
|
*/
|
|
809
815
|
protected getMainWorker(): MainWorker;
|
|
810
816
|
/**
|
|
811
|
-
*
|
|
817
|
+
* Sends a message to the main worker.
|
|
812
818
|
*
|
|
813
819
|
* @param message The response message.
|
|
814
820
|
*/
|
|
@@ -818,21 +824,21 @@ declare abstract class AbstractWorker<MainWorker extends Worker | MessagePort, D
|
|
|
818
824
|
*/
|
|
819
825
|
protected checkAlive(): void;
|
|
820
826
|
/**
|
|
821
|
-
*
|
|
827
|
+
* Handles an error and convert it to a string so it can be sent back to the main worker.
|
|
822
828
|
*
|
|
823
829
|
* @param e The error raised by the worker.
|
|
824
830
|
* @returns Message of the error.
|
|
825
831
|
*/
|
|
826
832
|
protected handleError(e: Error | string): string;
|
|
827
833
|
/**
|
|
828
|
-
*
|
|
834
|
+
* Runs the given function synchronously.
|
|
829
835
|
*
|
|
830
836
|
* @param fn Function that will be executed.
|
|
831
837
|
* @param value Input data for the given function.
|
|
832
838
|
*/
|
|
833
839
|
protected run(fn: (data?: Data) => Response, value: MessageValue<Data>): void;
|
|
834
840
|
/**
|
|
835
|
-
*
|
|
841
|
+
* Runs the given function asynchronously.
|
|
836
842
|
*
|
|
837
843
|
* @param fn Function that will be executed.
|
|
838
844
|
* @param value Input data for the given function.
|
package/lib/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var e,r=require("events"),t=require("cluster"),s=require("os"),o=require("worker_threads"),i=require("async_hooks");!function(e){e.FIXED="fixed",e.DYNAMIC="dynamic"}(e||(e={}));class n extends r{}const a=()=>{},h=Object.freeze({SOFT:"SOFT",HARD:"HARD"});const k=Object.freeze({ROUND_ROBIN:"ROUND_ROBIN",LESS_RECENTLY_USED:"LESS_RECENTLY_USED",FAIR_SHARE:"FAIR_SHARE",WEIGHTED_ROUND_ROBIN:"WEIGHTED_ROUND_ROBIN"});class u{constructor(r){this.pool=r,this.isDynamicPool=this.pool.type===e.DYNAMIC,this.requiredStatistics={runTime:!1}}}class c extends u{constructor(){super(...arguments),this.requiredStatistics={runTime:!0},this.workerLastVirtualTaskTimestamp=new Map}reset(){return this.workerLastVirtualTaskTimestamp.clear(),!0}choose(){this.computeWorkerLastVirtualTaskTimestamp();let e,r=1/0;for(const t of this.pool.workers){const s=this.workerLastVirtualTaskTimestamp.get(t)?.end??0;s<r&&(r=s,e=t)}return e}computeWorkerLastVirtualTaskTimestamp(){for(const e of this.pool.workers){const r=Math.max(Date.now(),this.workerLastVirtualTaskTimestamp.get(e)?.end??-1/0),t=r+(this.pool.getWorkerAverageTasksRunTime(e)??0);this.workerLastVirtualTaskTimestamp.set(e,{start:r,end:t})}}}class l extends u{reset(){return!0}choose(){let e,r=1/0;for(const t of this.pool.workers){const s=this.pool.getWorkerRunningTasks(t);if(!1===this.isDynamicPool&&0===s)return t;s<r&&(e=t,r=s)}return e}}class p extends u{constructor(){super(...arguments),this.nextWorkerIndex=0}reset(){return this.nextWorkerIndex=0,!0}choose(){const e=this.pool.workers[this.nextWorkerIndex];return this.nextWorkerIndex=this.nextWorkerIndex===this.pool.workers.length-1?0:this.nextWorkerIndex+1,e}}class W extends u{constructor(e){super(e),this.requiredStatistics={runTime:!0},this.previousWorkerIndex=0,this.currentWorkerIndex=0,this.workersTaskRunTime=new Map,this.defaultWorkerWeight=this.computeWorkerWeight(),this.initWorkersTaskRunTime()}reset(){return this.previousWorkerIndex=0,this.currentWorkerIndex=0,this.workersTaskRunTime.clear(),this.initWorkersTaskRunTime(),!0}choose(){const e=this.pool.workers[this.currentWorkerIndex];!0===this.isDynamicPool&&!1===this.workersTaskRunTime.has(e)&&this.initWorkerTaskRunTime(e);const r=this.getWorkerVirtualTaskRunTime(e)??0,t=this.workersTaskRunTime.get(e)?.weight??this.defaultWorkerWeight;if(this.currentWorkerIndex===this.previousWorkerIndex){const s=(this.workersTaskRunTime.get(e)?.runTime??0)+r;this.setWorkerTaskRunTime(e,t,s)}else this.setWorkerTaskRunTime(e,t,0);return r<t?this.previousWorkerIndex=this.currentWorkerIndex:(this.previousWorkerIndex=this.currentWorkerIndex,this.currentWorkerIndex=this.pool.workers.length-1===this.currentWorkerIndex?0:this.currentWorkerIndex+1),this.pool.workers[this.currentWorkerIndex]}initWorkersTaskRunTime(){for(const e of this.pool.workers)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.getWorkerAverageTasksRunTime(e)}computeWorkerWeight(){let e=0;for(const r of s.cpus()){const t=r.speed.toString().length-1;e+=1/(r.speed/Math.pow(10,t))*Math.pow(10,t)}return Math.round(e/s.cpus().length)}}class g{static getWorkerChoiceStrategy(e,r=k.ROUND_ROBIN){switch(r){case k.ROUND_ROBIN:return new p(e);case k.LESS_RECENTLY_USED:return new l(e);case k.FAIR_SHARE:return new c(e);case k.WEIGHTED_ROUND_ROBIN:return new W(e);default:throw new Error(`Worker choice strategy '${r}' not found`)}}}class d extends u{constructor(e,r,t=k.ROUND_ROBIN){super(e),this.createDynamicallyWorkerCallback=r,this.workerChoiceStrategy=g.getWorkerChoiceStrategy(this.pool,t),this.requiredStatistics=this.workerChoiceStrategy.requiredStatistics}reset(){return this.workerChoiceStrategy.reset()}choose(){const e=this.pool.findFreeWorker();return e||(!0===this.pool.busy?this.workerChoiceStrategy.choose():this.createDynamicallyWorkerCallback())}}class m{constructor(e,r,t=k.ROUND_ROBIN){this.pool=e,this.createDynamicallyWorkerCallback=r,this.setWorkerChoiceStrategy(t)}getPoolWorkerChoiceStrategy(r=k.ROUND_ROBIN){return this.pool.type===e.DYNAMIC?new d(this.pool,this.createDynamicallyWorkerCallback,r):g.getWorkerChoiceStrategy(this.pool,r)}getWorkerChoiceStrategy(){return this.workerChoiceStrategy}setWorkerChoiceStrategy(e){this.workerChoiceStrategy?.reset(),this.workerChoiceStrategy=this.getPoolWorkerChoiceStrategy(e)}execute(){return this.workerChoiceStrategy.choose()}}class T{constructor(e,r,t){if(this.numberOfWorkers=e,this.filePath=r,this.opts=t,this.workers=[],this.workersTasksUsage=new Map,this.promiseMap=new Map,this.nextMessageId=0,!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.setupHook();for(let e=1;e<=this.numberOfWorkers;e++)this.createAndSetupWorker();this.opts.enableEvents&&(this.emitter=new n),this.workerChoiceStrategyContext=new m(this,(()=>{const e=this.createAndSetupWorker();return this.registerWorkerMessageListener(e,(r=>{var t;t=h.HARD,(r.kill===t||0===this.getWorkerRunningTasks(e))&&this.destroyWorker(e)})),e}),this.opts.workerChoiceStrategy)}checkFilePath(e){if(!e)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(!1===Number.isSafeInteger(r))throw new Error("Cannot instantiate a pool with a non integer number of workers");if(r<0)throw new Error("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??k.ROUND_ROBIN,this.opts.enableEvents=e.enableEvents??!0}get numberOfRunningTasks(){return this.promiseMap.size}getWorkerIndex(e){return this.workers.indexOf(e)}getWorkerRunningTasks(e){return this.workersTasksUsage.get(e)?.running}getWorkerAverageTasksRunTime(e){return this.workersTasksUsage.get(e)?.avgRunTime}setWorkerChoiceStrategy(e){this.opts.workerChoiceStrategy=e;for(const e of this.workers)this.resetWorkerTasksUsage(e);this.workerChoiceStrategyContext.setWorkerChoiceStrategy(e)}internalGetBusyStatus(){return this.numberOfRunningTasks>=this.numberOfWorkers&&!1===this.findFreeWorker()}findFreeWorker(){for(const e of this.workers)if(0===this.getWorkerRunningTasks(e))return e;return!1}execute(e){const r=this.chooseWorker(),t=++this.nextMessageId,s=this.internalExecute(r,t);return this.checkAndEmitBusy(),e=e??{},this.sendToWorker(r,{data:e,id:t}),s}async destroy(){await Promise.all(this.workers.map((e=>this.destroyWorker(e))))}setupHook(){}beforePromiseWorkerResponseHook(e){this.increaseWorkerRunningTasks(e)}afterPromiseWorkerResponseHook(e,r){this.decreaseWorkerRunningTasks(r.worker),this.stepWorkerRunTasks(r.worker,1),this.updateWorkerTasksRunTime(r.worker,e.taskRunTime)}removeWorker(e){this.workers.splice(this.getWorkerIndex(e),1),this.removeWorkerTasksUsage(e)}chooseWorker(){return this.workerChoiceStrategyContext.execute()}internalExecute(e,r){return this.beforePromiseWorkerResponseHook(e),new Promise(((t,s)=>{this.promiseMap.set(r,{resolve:t,reject:s,worker:e})}))}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.workers.push(e),this.initWorkerTasksUsage(e),this.afterWorkerSetup(e),e}workerListener(){return e=>{if(void 0!==e.id){const r=this.promiseMap.get(e.id);void 0!==r&&(this.afterPromiseWorkerResponseHook(e,r),e.error?r.reject(e.error):r.resolve(e.data),this.promiseMap.delete(e.id))}}}checkAndEmitBusy(){this.opts.enableEvents&&this.busy&&this.emitter?.emit("busy")}increaseWorkerRunningTasks(e){this.stepWorkerRunningTasks(e,1)}decreaseWorkerRunningTasks(e){this.stepWorkerRunningTasks(e,-1)}stepWorkerRunningTasks(e,r){const t=this.workersTasksUsage.get(e);if(void 0===t)throw new Error("Worker could not be found in worker tasks usage map");t.running=t.running+r,this.workersTasksUsage.set(e,t)}stepWorkerRunTasks(e,r){const t=this.workersTasksUsage.get(e);if(void 0===t)throw new Error("Worker could not be found in worker tasks usage map");t.run=t.run+r,this.workersTasksUsage.set(e,t)}updateWorkerTasksRunTime(e,r){if(!0===this.workerChoiceStrategyContext.getWorkerChoiceStrategy().requiredStatistics.runTime){const t=this.workersTasksUsage.get(e);if(void 0===t)throw new Error("Worker could not be found in worker tasks usage map");t.runTime+=r??0,0!==t.run&&(t.avgRunTime=t.runTime/t.run),this.workersTasksUsage.set(e,t)}}initWorkerTasksUsage(e){this.workersTasksUsage.set(e,{run:0,running:0,runTime:0,avgRunTime:0})}removeWorkerTasksUsage(e){this.workersTasksUsage.delete(e)}resetWorkerTasksUsage(e){this.removeWorkerTasksUsage(e),this.initWorkerTasksUsage(e)}}class w extends T{constructor(e,r,t={}){super(e,r,t),this.opts=t}setupHook(){t.setupPrimary({exec:this.filePath})}isMain(){return t.isPrimary}destroyWorker(e){this.sendToWorker(e,{kill:1}),e.kill()}sendToWorker(e,r){e.send(r)}registerWorkerMessageListener(e,r){e.on("message",r)}createWorker(){return t.fork(this.opts.env)}afterWorkerSetup(e){this.registerWorkerMessageListener(e,super.workerListener())}get type(){return e.FIXED}get busy(){return this.internalGetBusyStatus()}}class y extends T{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 busy(){return this.internalGetBusyStatus()}}const R=h.SOFT;class f extends i.AsyncResource{constructor(e,r,t,s,o={killBehavior:R,maxInactiveTime:6e4}){super(e),this.mainWorker=s,this.opts=o,this.checkFunctionInput(t),this.checkWorkerOptions(this.opts),this.lastTaskTimestamp=Date.now(),!1===r&&(this.aliveInterval=setInterval(this.checkAlive.bind(this),(this.opts.maxInactiveTime??6e4)/2),this.checkAlive.bind(this)()),this.mainWorker?.on("message",(e=>{this.messageListener(e,t)}))}messageListener(e,r){void 0!==e.data&&void 0!==e.id?this.opts.async?this.runInAsyncScope(this.runAsync.bind(this),this,r,e):this.runInAsyncScope(this.run.bind(this),this,r,e):void 0!==e.parent?this.mainWorker=e.parent:void 0!==e.kill&&(this.aliveInterval&&clearInterval(this.aliveInterval),this.emitDestroy())}checkWorkerOptions(e){this.opts.killBehavior=e.killBehavior??R,this.opts.maxInactiveTime=e.maxInactiveTime??6e4,this.opts.async=!!e.async}checkFunctionInput(e){if(!e)throw new Error("fn parameter is mandatory")}getMainWorker(){if(!this.mainWorker)throw new Error("Main worker was not set");return this.mainWorker}checkAlive(){Date.now()-this.lastTaskTimestamp>(this.opts.maxInactiveTime??6e4)&&this.sendToMainWorker({kill:this.opts.killBehavior})}handleError(e){return e}run(e,r){try{const t=Date.now(),s=e(r.data),o=Date.now()-t;this.sendToMainWorker({data:s,id:r.id,taskRunTime:o})}catch(e){const t=this.handleError(e);this.sendToMainWorker({error:t,id:r.id})}finally{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,taskRunTime:s}),null})).catch((e=>{const t=this.handleError(e);this.sendToMainWorker({error:t,id:r.id})})).finally((()=>{this.lastTaskTimestamp=Date.now()})).catch(a)}}exports.AbstractWorker=f,exports.ClusterWorker=class extends f{constructor(e,r={}){super("worker-cluster-pool:poolifier",t.isPrimary,e,t.worker,r)}sendToMainWorker(e){this.getMainWorker().send(e)}handleError(e){return e instanceof Error?e.message:e}},exports.DynamicClusterPool=class extends w{constructor(e,r,t,s={}){super(e,t,s),this.max=r}get type(){return e.DYNAMIC}get busy(){return this.workers.length===this.max}},exports.DynamicThreadPool=class extends y{constructor(e,r,t,s={}){super(e,t,s),this.max=r}get type(){return e.DYNAMIC}get busy(){return this.workers.length===this.max}},exports.FixedClusterPool=w,exports.FixedThreadPool=y,exports.KillBehaviors=h,exports.ThreadWorker=class extends f{constructor(e,r={}){super("worker-thread-pool:poolifier",o.isMainThread,e,o.parentPort,r)}sendToMainWorker(e){this.getMainWorker().postMessage(e)}},exports.WorkerChoiceStrategies=k;
|
|
1
|
+
"use strict";var e,r=require("events"),t=require("cluster"),s=require("os"),o=require("worker_threads"),i=require("async_hooks");!function(e){e.FIXED="fixed",e.DYNAMIC="dynamic"}(e||(e={}));class n extends r{}const a=()=>{},h=Object.freeze({SOFT:"SOFT",HARD:"HARD"});const k=Object.freeze({ROUND_ROBIN:"ROUND_ROBIN",LESS_RECENTLY_USED:"LESS_RECENTLY_USED",FAIR_SHARE:"FAIR_SHARE",WEIGHTED_ROUND_ROBIN:"WEIGHTED_ROUND_ROBIN"});class u{constructor(r){this.pool=r,this.isDynamicPool=this.pool.type===e.DYNAMIC,this.requiredStatistics={runTime:!1}}}class c extends u{constructor(){super(...arguments),this.requiredStatistics={runTime:!0},this.workerLastVirtualTaskTimestamp=new Map}reset(){return this.workerLastVirtualTaskTimestamp.clear(),!0}choose(){let e,r=1/0;for(const t of this.pool.workers){this.computeWorkerLastVirtualTaskTimestamp(t);const s=this.workerLastVirtualTaskTimestamp.get(t)?.end??0;s<r&&(r=s,e=t)}return e}computeWorkerLastVirtualTaskTimestamp(e){const r=Math.max(Date.now(),this.workerLastVirtualTaskTimestamp.get(e)?.end??-1/0),t=r+(this.pool.getWorkerAverageTasksRunTime(e)??0);this.workerLastVirtualTaskTimestamp.set(e,{start:r,end:t})}}class l extends u{reset(){return!0}choose(){let e,r=1/0;for(const t of this.pool.workers){const s=this.pool.getWorkerRunningTasks(t);if(!1===this.isDynamicPool&&0===s)return t;s<r&&(e=t,r=s)}return e}}class p extends u{constructor(){super(...arguments),this.nextWorkerIndex=0}reset(){return this.nextWorkerIndex=0,!0}choose(){const e=this.pool.workers[this.nextWorkerIndex];return this.nextWorkerIndex=this.nextWorkerIndex===this.pool.workers.length-1?0:this.nextWorkerIndex+1,e}}class g extends u{constructor(e){super(e),this.requiredStatistics={runTime:!0},this.currentWorkerIndex=0,this.workersTaskRunTime=new Map,this.defaultWorkerWeight=this.computeWorkerWeight(),this.initWorkersTaskRunTime()}reset(){return this.currentWorkerIndex=0,this.workersTaskRunTime.clear(),this.initWorkersTaskRunTime(),!0}choose(){const e=this.pool.workers[this.currentWorkerIndex];!0===this.isDynamicPool&&!1===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.currentWorkerIndex=this.currentWorkerIndex===this.pool.workers.length-1?0:this.currentWorkerIndex+1,this.setWorkerTaskRunTime(this.pool.workers[this.currentWorkerIndex],t,0)),e}initWorkersTaskRunTime(){for(const e of this.pool.workers)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.getWorkerAverageTasksRunTime(e)}computeWorkerWeight(){let e=0;for(const r of s.cpus()){const t=r.speed.toString().length-1;e+=1/(r.speed/Math.pow(10,t))*Math.pow(10,t)}return Math.round(e/s.cpus().length)}}class m{static getWorkerChoiceStrategy(e,r=k.ROUND_ROBIN){switch(r){case k.ROUND_ROBIN:return new p(e);case k.LESS_RECENTLY_USED:return new l(e);case k.FAIR_SHARE:return new c(e);case k.WEIGHTED_ROUND_ROBIN:return new g(e);default:throw new Error(`Worker choice strategy '${r}' not found`)}}}class W extends u{constructor(e,r,t=k.ROUND_ROBIN){super(e),this.createDynamicallyWorkerCallback=r,this.workerChoiceStrategy=m.getWorkerChoiceStrategy(this.pool,t),this.requiredStatistics=this.workerChoiceStrategy.requiredStatistics}reset(){return this.workerChoiceStrategy.reset()}choose(){const e=this.pool.findFreeWorker();return e||(!0===this.pool.busy?this.workerChoiceStrategy.choose():this.createDynamicallyWorkerCallback())}}class T{constructor(e,r,t=k.ROUND_ROBIN){this.pool=e,this.createDynamicallyWorkerCallback=r,this.setWorkerChoiceStrategy(t)}getPoolWorkerChoiceStrategy(r=k.ROUND_ROBIN){return this.pool.type===e.DYNAMIC?new W(this.pool,this.createDynamicallyWorkerCallback,r):m.getWorkerChoiceStrategy(this.pool,r)}getWorkerChoiceStrategy(){return this.workerChoiceStrategy}setWorkerChoiceStrategy(e){this.workerChoiceStrategy?.reset(),this.workerChoiceStrategy=this.getPoolWorkerChoiceStrategy(e)}execute(){return this.workerChoiceStrategy.choose()}}class d{constructor(e,r,t){if(this.numberOfWorkers=e,this.filePath=r,this.opts=t,this.workers=[],this.workersTasksUsage=new Map,this.promiseMap=new Map,this.nextMessageId=0,!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.setupHook();for(let e=1;e<=this.numberOfWorkers;e++)this.createAndSetupWorker();this.opts.enableEvents&&(this.emitter=new n),this.workerChoiceStrategyContext=new T(this,(()=>{const e=this.createAndSetupWorker();return this.registerWorkerMessageListener(e,(r=>{var t;t=h.HARD,(r.kill===t||0===this.getWorkerRunningTasks(e))&&this.destroyWorker(e)})),e}),this.opts.workerChoiceStrategy)}checkFilePath(e){if(!e)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(!1===Number.isSafeInteger(r))throw new Error("Cannot instantiate a pool with a non integer number of workers");if(r<0)throw new Error("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??k.ROUND_ROBIN,this.opts.enableEvents=e.enableEvents??!0}get numberOfRunningTasks(){return this.promiseMap.size}getWorkerIndex(e){return this.workers.indexOf(e)}getWorkerRunningTasks(e){return this.workersTasksUsage.get(e)?.running}getWorkerAverageTasksRunTime(e){return this.workersTasksUsage.get(e)?.avgRunTime}setWorkerChoiceStrategy(e){this.opts.workerChoiceStrategy=e;for(const e of this.workers)this.resetWorkerTasksUsage(e);this.workerChoiceStrategyContext.setWorkerChoiceStrategy(e)}internalGetBusyStatus(){return this.numberOfRunningTasks>=this.numberOfWorkers&&!1===this.findFreeWorker()}findFreeWorker(){for(const e of this.workers)if(0===this.getWorkerRunningTasks(e))return e;return!1}execute(e){const r=this.chooseWorker(),t=++this.nextMessageId,s=this.internalExecute(r,t);return this.checkAndEmitBusy(),e=e??{},this.sendToWorker(r,{data:e,id:t}),s}async destroy(){await Promise.all(this.workers.map((e=>this.destroyWorker(e))))}setupHook(){}beforePromiseWorkerResponseHook(e){this.increaseWorkerRunningTasks(e)}afterPromiseWorkerResponseHook(e,r){this.decreaseWorkerRunningTasks(r.worker),this.stepWorkerRunTasks(r.worker,1),this.updateWorkerTasksRunTime(r.worker,e.taskRunTime)}removeWorker(e){this.workers.splice(this.getWorkerIndex(e),1),this.removeWorkerTasksUsage(e)}chooseWorker(){return this.workerChoiceStrategyContext.execute()}internalExecute(e,r){return this.beforePromiseWorkerResponseHook(e),new Promise(((t,s)=>{this.promiseMap.set(r,{resolve:t,reject:s,worker:e})}))}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.workers.push(e),this.initWorkerTasksUsage(e),this.afterWorkerSetup(e),e}workerListener(){return e=>{if(void 0!==e.id){const r=this.promiseMap.get(e.id);void 0!==r&&(this.afterPromiseWorkerResponseHook(e,r),e.error?r.reject(e.error):r.resolve(e.data),this.promiseMap.delete(e.id))}}}checkAndEmitBusy(){this.opts.enableEvents&&this.busy&&this.emitter?.emit("busy")}increaseWorkerRunningTasks(e){this.stepWorkerRunningTasks(e,1)}decreaseWorkerRunningTasks(e){this.stepWorkerRunningTasks(e,-1)}stepWorkerRunningTasks(e,r){const t=this.workersTasksUsage.get(e);if(void 0===t)throw new Error("Worker could not be found in worker tasks usage map");t.running=t.running+r,this.workersTasksUsage.set(e,t)}stepWorkerRunTasks(e,r){const t=this.workersTasksUsage.get(e);if(void 0===t)throw new Error("Worker could not be found in worker tasks usage map");t.run=t.run+r,this.workersTasksUsage.set(e,t)}updateWorkerTasksRunTime(e,r){if(!0===this.workerChoiceStrategyContext.getWorkerChoiceStrategy().requiredStatistics.runTime){const t=this.workersTasksUsage.get(e);if(void 0===t)throw new Error("Worker could not be found in worker tasks usage map");t.runTime+=r??0,0!==t.run&&(t.avgRunTime=t.runTime/t.run),this.workersTasksUsage.set(e,t)}}initWorkerTasksUsage(e){this.workersTasksUsage.set(e,{run:0,running:0,runTime:0,avgRunTime:0})}removeWorkerTasksUsage(e){this.workersTasksUsage.delete(e)}resetWorkerTasksUsage(e){this.removeWorkerTasksUsage(e),this.initWorkerTasksUsage(e)}}class w extends d{constructor(e,r,t={}){super(e,r,t),this.opts=t}setupHook(){t.setupPrimary({...this.opts.settings,exec:this.filePath})}isMain(){return t.isPrimary}destroyWorker(e){this.sendToWorker(e,{kill:1}),e.kill()}sendToWorker(e,r){e.send(r)}registerWorkerMessageListener(e,r){e.on("message",r)}createWorker(){return t.fork(this.opts.env)}afterWorkerSetup(e){this.registerWorkerMessageListener(e,super.workerListener())}get type(){return e.FIXED}get busy(){return this.internalGetBusyStatus()}}class y extends d{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 busy(){return this.internalGetBusyStatus()}}const R=h.SOFT;class f extends i.AsyncResource{constructor(e,r,t,s,o={killBehavior:R,maxInactiveTime:6e4}){super(e),this.mainWorker=s,this.opts=o,this.checkFunctionInput(t),this.checkWorkerOptions(this.opts),this.lastTaskTimestamp=Date.now(),!1===r&&(this.aliveInterval=setInterval(this.checkAlive.bind(this),(this.opts.maxInactiveTime??6e4)/2),this.checkAlive.bind(this)()),this.mainWorker?.on("message",(e=>{this.messageListener(e,t)}))}messageListener(e,r){void 0!==e.data&&void 0!==e.id?this.opts.async?this.runInAsyncScope(this.runAsync.bind(this),this,r,e):this.runInAsyncScope(this.run.bind(this),this,r,e):void 0!==e.parent?this.mainWorker=e.parent:void 0!==e.kill&&(this.aliveInterval&&clearInterval(this.aliveInterval),this.emitDestroy())}checkWorkerOptions(e){this.opts.killBehavior=e.killBehavior??R,this.opts.maxInactiveTime=e.maxInactiveTime??6e4,this.opts.async=!!e.async}checkFunctionInput(e){if(!e)throw new Error("fn parameter is mandatory")}getMainWorker(){if(!this.mainWorker)throw new Error("Main worker was not set");return this.mainWorker}checkAlive(){Date.now()-this.lastTaskTimestamp>(this.opts.maxInactiveTime??6e4)&&this.sendToMainWorker({kill:this.opts.killBehavior})}handleError(e){return e}run(e,r){try{const t=Date.now(),s=e(r.data),o=Date.now()-t;this.sendToMainWorker({data:s,id:r.id,taskRunTime:o})}catch(e){const t=this.handleError(e);this.sendToMainWorker({error:t,id:r.id})}finally{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,taskRunTime:s}),null})).catch((e=>{const t=this.handleError(e);this.sendToMainWorker({error:t,id:r.id})})).finally((()=>{this.lastTaskTimestamp=Date.now()})).catch(a)}}exports.AbstractWorker=f,exports.ClusterWorker=class extends f{constructor(e,r={}){super("worker-cluster-pool:poolifier",t.isPrimary,e,t.worker,r)}sendToMainWorker(e){this.getMainWorker().send(e)}handleError(e){return e instanceof Error?e.message:e}},exports.DynamicClusterPool=class extends w{constructor(e,r,t,s={}){super(e,t,s),this.max=r}get type(){return e.DYNAMIC}get busy(){return this.workers.length===this.max}},exports.DynamicThreadPool=class extends y{constructor(e,r,t,s={}){super(e,t,s),this.max=r}get type(){return e.DYNAMIC}get busy(){return this.workers.length===this.max}},exports.FixedClusterPool=w,exports.FixedThreadPool=y,exports.KillBehaviors=h,exports.ThreadWorker=class extends f{constructor(e,r={}){super("worker-thread-pool:poolifier",o.isMainThread,e,o.parentPort,r)}sendToMainWorker(e){this.getMainWorker().postMessage(e)}},exports.WorkerChoiceStrategies=k;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "poolifier",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.3",
|
|
4
4
|
"description": "A fast, easy to use Node.js Worker Thread Pool and Cluster Pool implementation",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"lib"
|
|
65
65
|
],
|
|
66
66
|
"devDependencies": {
|
|
67
|
-
"@types/node": "^18.
|
|
67
|
+
"@types/node": "^18.11.0",
|
|
68
68
|
"@typescript-eslint/eslint-plugin": "^5.40.0",
|
|
69
69
|
"@typescript-eslint/parser": "^5.40.0",
|
|
70
70
|
"benchmark": "^2.1.4",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"eslint-plugin-prettierx": "^0.18.0",
|
|
80
80
|
"eslint-plugin-promise": "^6.1.0",
|
|
81
81
|
"eslint-plugin-spellcheck": "^0.0.19",
|
|
82
|
-
"expect": "^29.
|
|
82
|
+
"expect": "^29.2.0",
|
|
83
83
|
"husky": "^8.0.1",
|
|
84
84
|
"lint-staged": "^13.0.3",
|
|
85
85
|
"microtime": "^3.1.1",
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
"prettier": "^2.7.1",
|
|
90
90
|
"prettier-plugin-organize-imports": "^3.1.1",
|
|
91
91
|
"prettierx": "^0.18.3",
|
|
92
|
-
"rollup": "^3.
|
|
92
|
+
"rollup": "^3.2.0",
|
|
93
93
|
"rollup-plugin-analyzer": "^4.0.0",
|
|
94
94
|
"rollup-plugin-command": "^1.1.3",
|
|
95
95
|
"rollup-plugin-delete": "^2.0.0",
|