X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fpools%2Fabstract-pool.ts;h=da0f2e77358525336a5936a42841edce9016f44b;hb=70726c2810ba4768dae5ae1f40ed6ddcecf142ba;hp=db7c83ee2fed1ec558b6ee9eee5b7b8202368085;hpb=e3882e06ccfa54860d16d935c3a9c1077446fbdc;p=poolifier.git diff --git a/src/pools/abstract-pool.ts b/src/pools/abstract-pool.ts index db7c83ee..da0f2e77 100644 --- a/src/pools/abstract-pool.ts +++ b/src/pools/abstract-pool.ts @@ -23,12 +23,13 @@ import { } from './pool' import type { IWorker, + MessageHandler, Task, - TaskStatistics, WorkerNode, WorkerUsage } from './worker' import { + Measurements, WorkerChoiceStrategies, type WorkerChoiceStrategy, type WorkerChoiceStrategyOptions @@ -39,8 +40,8 @@ import { WorkerChoiceStrategyContext } from './selection-strategies/worker-choic * Base class that implements some shared logic for all poolifier pools. * * @typeParam Worker - Type of worker which manages this pool. - * @typeParam Data - Type of data sent to the worker. This can only be serializable data. - * @typeParam Response - Type of execution response. This can only be serializable data. + * @typeParam Data - Type of data sent to the worker. This can only be structured-cloneable data. + * @typeParam Response - Type of execution response. This can only be structured-cloneable data. */ export abstract class AbstractPool< Worker extends IWorker, @@ -199,6 +200,16 @@ export abstract class AbstractPool< 'Invalid worker choice strategy options: must have a weight for each worker node' ) } + if ( + workerChoiceStrategyOptions.measurement != null && + !Object.values(Measurements).includes( + workerChoiceStrategyOptions.measurement + ) + ) { + throw new Error( + `Invalid worker choice strategy options: invalid measurement '${workerChoiceStrategyOptions.measurement}'` + ) + } } private checkValidTasksQueueOptions ( @@ -207,11 +218,20 @@ export abstract class AbstractPool< if (tasksQueueOptions != null && !isPlainObject(tasksQueueOptions)) { throw new TypeError('Invalid tasks queue options: must be a plain object') } - if ((tasksQueueOptions?.concurrency as number) <= 0) { + if ( + tasksQueueOptions?.concurrency != null && + !Number.isSafeInteger(tasksQueueOptions.concurrency) + ) { + throw new TypeError( + 'Invalid worker tasks concurrency: must be an integer' + ) + } + if ( + tasksQueueOptions?.concurrency != null && + tasksQueueOptions.concurrency <= 0 + ) { throw new Error( - `Invalid worker tasks concurrency '${ - tasksQueueOptions.concurrency as number - }'` + `Invalid worker tasks concurrency '${tasksQueueOptions.concurrency}'` ) } } @@ -249,12 +269,13 @@ export abstract class AbstractPool< 0 ), queuedTasks: this.workerNodes.reduce( - (accumulator, workerNode) => accumulator + workerNode.tasksQueue.size, + (accumulator, workerNode) => + accumulator + workerNode.workerUsage.tasks.queued, 0 ), maxQueuedTasks: this.workerNodes.reduce( (accumulator, workerNode) => - accumulator + workerNode.tasksQueue.maxSize, + accumulator + workerNode.workerUsage.tasks.maxQueued, 0 ), failedTasks: this.workerNodes.reduce( @@ -312,10 +333,10 @@ export abstract class AbstractPool< if (workerChoiceStrategyOptions != null) { this.setWorkerChoiceStrategyOptions(workerChoiceStrategyOptions) } - for (const workerNode of this.workerNodes) { + for (const [workerNodeKey, workerNode] of this.workerNodes.entries()) { this.setWorkerNodeTasksUsage( workerNode, - this.getWorkerUsage(workerNode.worker) + this.getWorkerUsage(workerNodeKey) ) this.setWorkerStatistics(workerNode.worker) } @@ -379,6 +400,11 @@ export abstract class AbstractPool< */ protected abstract get busy (): boolean + /** + * Whether worker nodes are executing at least one task. + * + * @returns Worker nodes busyness boolean status. + */ protected internalBusy (): boolean { return ( this.workerNodes.findIndex(workerNode => { @@ -416,7 +442,6 @@ export abstract class AbstractPool< } else { this.executeTask(workerNodeKey, submittedTask) } - this.workerChoiceStrategyContext.update(workerNodeKey) this.checkAndEmitEvents() // eslint-disable-next-line @typescript-eslint/return-await return res @@ -434,15 +459,15 @@ export abstract class AbstractPool< } /** - * Shutdowns the given worker. + * Terminates the given worker. * * @param worker - A worker within `workerNodes`. */ protected abstract destroyWorker (worker: Worker): void | Promise /** - * Setup hook to execute code before worker node are created in the abstract constructor. - * Can be overridden + * Setup hook to execute code before worker nodes are created in the abstract constructor. + * Can be overridden. * * @virtual */ @@ -609,33 +634,29 @@ export abstract class AbstractPool< /** * Chooses a worker node for the next task. * - * The default worker choice strategy uses a round robin algorithm to distribute the load. + * The default worker choice strategy uses a round robin algorithm to distribute the tasks. * * @returns The worker node key */ - protected chooseWorkerNode (): number { - let workerNodeKey: number - if (this.type === PoolTypes.dynamic && !this.full && this.internalBusy()) { - const workerCreated = this.createAndSetupWorker() - this.registerWorkerMessageListener(workerCreated, message => { - const currentWorkerNodeKey = this.getWorkerNodeKey(workerCreated) - if ( - isKillBehavior(KillBehaviors.HARD, message.kill) || - (message.kill != null && - this.workerNodes[currentWorkerNodeKey].workerUsage.tasks - .executing === 0) - ) { - // Kill message received from the worker: no new tasks are submitted to that worker for a while ( > maxInactiveTime) - this.flushTasksQueue(currentWorkerNodeKey) - // FIXME: wait for tasks to be finished - void (this.destroyWorker(workerCreated) as Promise) - } - }) - workerNodeKey = this.getWorkerNodeKey(workerCreated) - } else { - workerNodeKey = this.workerChoiceStrategyContext.execute() + private chooseWorkerNode (): number { + if (this.shallCreateDynamicWorker()) { + const worker = this.createAndSetupDynamicWorker() + if ( + this.workerChoiceStrategyContext.getStrategyPolicy().useDynamicWorker + ) { + return this.getWorkerNodeKey(worker) + } } - return workerNodeKey + return this.workerChoiceStrategyContext.execute() + } + + /** + * Conditions for dynamic worker creation. + * + * @returns Whether to create a dynamic worker or not. + */ + private shallCreateDynamicWorker (): boolean { + return this.type === PoolTypes.dynamic && !this.full && this.internalBusy() } /** @@ -655,23 +676,30 @@ export abstract class AbstractPool< * @param worker - The worker which should register a listener. * @param listener - The message listener callback. */ - protected abstract registerWorkerMessageListener< - Message extends Data | Response - >(worker: Worker, listener: (message: MessageValue) => void): void + private registerWorkerMessageListener( + worker: Worker, + listener: (message: MessageValue) => void + ): void { + worker.on('message', listener as MessageHandler) + } /** - * Returns a newly created worker. + * Creates a new worker. + * + * @returns Newly created worker. */ protected abstract createWorker (): Worker /** * Function that can be hooked up when a worker has been newly created and moved to the pool worker nodes. - * - * Can be used to update the `maxListeners` or binding the `main-worker`\<-\>`worker` connection if not bind by default. + * Can be overridden. * * @param worker - The newly created worker. */ - protected abstract afterWorkerSetup (worker: Worker): void + protected afterWorkerSetup (worker: Worker): void { + // Listen to worker messages. + this.registerWorkerMessageListener(worker, this.workerListener()) + } /** * Creates a new worker and sets it up completely in the pool worker nodes. @@ -706,6 +734,33 @@ export abstract class AbstractPool< return worker } + /** + * Creates a new dynamic worker and sets it up completely in the pool worker nodes. + * + * @returns New, completely set up dynamic worker. + */ + protected createAndSetupDynamicWorker (): Worker { + const worker = this.createAndSetupWorker() + this.registerWorkerMessageListener(worker, message => { + const workerNodeKey = this.getWorkerNodeKey(worker) + if ( + isKillBehavior(KillBehaviors.HARD, message.kill) || + (message.kill != null && + ((this.opts.enableTasksQueue === false && + this.workerNodes[workerNodeKey].workerUsage.tasks.executing === + 0) || + (this.opts.enableTasksQueue === true && + this.workerNodes[workerNodeKey].workerUsage.tasks.executing === + 0 && + this.tasksQueueSize(workerNodeKey) === 0))) + ) { + // Kill message received from the worker: no new tasks are submitted to that worker for a while ( > maxInactiveTime) + void (this.destroyWorker(worker) as Promise) + } + }) + return worker + } + /** * This function is the listener registered for each worker message. * @@ -718,10 +773,10 @@ export abstract class AbstractPool< const promiseResponse = this.promiseResponseMap.get(message.id) if (promiseResponse != null) { if (message.taskError != null) { - promiseResponse.reject(message.taskError.message) if (this.emitter != null) { this.emitter.emit(PoolEvents.taskError, message.taskError) } + promiseResponse.reject(message.taskError.message) } else { promiseResponse.resolve(message.data as Response) } @@ -737,6 +792,7 @@ export abstract class AbstractPool< this.dequeueTask(workerNodeKey) as Task ) } + this.workerChoiceStrategyContext.update(workerNodeKey) } } } @@ -773,11 +829,17 @@ export abstract class AbstractPool< * @returns The worker nodes length. */ private pushWorkerNode (worker: Worker): number { - return this.workerNodes.push({ + this.workerNodes.push({ worker, - workerUsage: this.getWorkerUsage(worker), + workerUsage: this.getWorkerUsage(), tasksQueue: new Queue>() }) + const workerNodeKey = this.getWorkerNodeKey(worker) + this.setWorkerNodeTasksUsage( + this.workerNodes[workerNodeKey], + this.getWorkerUsage(workerNodeKey) + ) + return this.workerNodes.length } // /** @@ -831,6 +893,10 @@ export abstract class AbstractPool< return this.workerNodes[workerNodeKey].tasksQueue.size } + private tasksMaxQueueSize (workerNodeKey: number): number { + return this.workerNodes[workerNodeKey].tasksQueue.maxSize + } + private flushTasksQueue (workerNodeKey: number): void { if (this.tasksQueueSize(workerNodeKey) > 0) { for (let i = 0; i < this.tasksQueueSize(workerNodeKey); i++) { @@ -840,6 +906,7 @@ export abstract class AbstractPool< ) } } + this.workerNodes[workerNodeKey].tasksQueue.clear() } private flushTasksQueues (): void { @@ -860,9 +927,25 @@ export abstract class AbstractPool< }) } - private getWorkerUsage (worker: Worker): WorkerUsage { + private getWorkerUsage (workerNodeKey?: number): WorkerUsage { + const getTasksQueueSize = (workerNodeKey?: number): number => { + return workerNodeKey != null ? this.tasksQueueSize(workerNodeKey) : 0 + } + const getTasksMaxQueueSize = (workerNodeKey?: number): number => { + return workerNodeKey != null ? this.tasksMaxQueueSize(workerNodeKey) : 0 + } return { - tasks: this.getTaskStatistics(worker), + tasks: { + executed: 0, + executing: 0, + get queued (): number { + return getTasksQueueSize(workerNodeKey) + }, + get maxQueued (): number { + return getTasksMaxQueueSize(workerNodeKey) + }, + failed: 0 + }, runTime: { aggregate: 0, average: 0, @@ -892,17 +975,4 @@ export abstract class AbstractPool< } } } - - private getTaskStatistics (worker: Worker): TaskStatistics { - const queueSize = - this.workerNodes[this.getWorkerNodeKey(worker)]?.tasksQueue?.size - return { - executed: 0, - executing: 0, - get queued (): number { - return queueSize ?? 0 - }, - failed: 0 - } - } }