X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fpools%2Fabstract-pool.ts;h=ba05d92959a83f362fd721b4ca3615e66c86d196;hb=76aa2b0f39bd376383f2fdc79046328693719590;hp=c3565446f254c0292978f1f3eed1c23c1b1ac267;hpb=930dcf12982091cb5658b5a44acd47676fbae631;p=poolifier.git diff --git a/src/pools/abstract-pool.ts b/src/pools/abstract-pool.ts index c3565446..ba05d929 100644 --- a/src/pools/abstract-pool.ts +++ b/src/pools/abstract-pool.ts @@ -19,16 +19,18 @@ import { type PoolType, PoolTypes, type TasksQueueOptions, - type WorkerType + type WorkerType, + WorkerTypes } from './pool' import type { IWorker, + MessageHandler, Task, - TaskStatistics, WorkerNode, WorkerUsage } from './worker' import { + Measurements, WorkerChoiceStrategies, type WorkerChoiceStrategy, type WorkerChoiceStrategyOptions @@ -39,8 +41,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 +201,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,15 +219,32 @@ 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}'` ) } } + private get starting (): boolean { + return this.workerNodes.some(workerNode => !workerNode.info.started) + } + + private get started (): boolean { + return this.workerNodes.some(workerNode => workerNode.info.started) + } + /** @inheritDoc */ public get info (): PoolInfo { return { @@ -226,40 +255,39 @@ export abstract class AbstractPool< workerNodes: this.workerNodes.length, idleWorkerNodes: this.workerNodes.reduce( (accumulator, workerNode) => - workerNode.workerUsage.tasks.executing === 0 + workerNode.usage.tasks.executing === 0 ? accumulator + 1 : accumulator, 0 ), busyWorkerNodes: this.workerNodes.reduce( (accumulator, workerNode) => - workerNode.workerUsage.tasks.executing > 0 - ? accumulator + 1 - : accumulator, + workerNode.usage.tasks.executing > 0 ? accumulator + 1 : accumulator, 0 ), executedTasks: this.workerNodes.reduce( (accumulator, workerNode) => - accumulator + workerNode.workerUsage.tasks.executed, + accumulator + workerNode.usage.tasks.executed, 0 ), executingTasks: this.workerNodes.reduce( (accumulator, workerNode) => - accumulator + workerNode.workerUsage.tasks.executing, + accumulator + workerNode.usage.tasks.executing, 0 ), queuedTasks: this.workerNodes.reduce( - (accumulator, workerNode) => accumulator + workerNode.tasksQueue.size, + (accumulator, workerNode) => + accumulator + workerNode.usage.tasks.queued, 0 ), maxQueuedTasks: this.workerNodes.reduce( (accumulator, workerNode) => - accumulator + workerNode.tasksQueue.maxSize, + accumulator + workerNode.usage.tasks.maxQueued, 0 ), failedTasks: this.workerNodes.reduce( (accumulator, workerNode) => - accumulator + workerNode.workerUsage.tasks.failed, + accumulator + workerNode.usage.tasks.failed, 0 ) } @@ -287,11 +315,22 @@ export abstract class AbstractPool< */ protected abstract get maxSize (): number + /** + * Get the worker given its id. + * + * @param workerId - The worker id. + * @returns The worker if found in the pool worker nodes, `undefined` otherwise. + */ + private getWorkerById (workerId: number): Worker | undefined { + return this.workerNodes.find(workerNode => workerNode.info.id === workerId) + ?.worker + } + /** * Gets the given worker its worker node key. * * @param worker - The worker. - * @returns The worker node key if the worker is found in the pool worker nodes, `-1` otherwise. + * @returns The worker node key if found in the pool worker nodes, `-1` otherwise. */ private getWorkerNodeKey (worker: Worker): number { return this.workerNodes.findIndex( @@ -312,10 +351,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,10 +418,15 @@ 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 => { - return workerNode.workerUsage.tasks.executing === 0 + return workerNode.usage.tasks.executing === 0 }) === -1 ) } @@ -408,7 +452,7 @@ export abstract class AbstractPool< if ( this.opts.enableTasksQueue === true && (this.busy || - this.workerNodes[workerNodeKey].workerUsage.tasks.executing >= + this.workerNodes[workerNodeKey].usage.tasks.executing >= ((this.opts.tasksQueueOptions as TasksQueueOptions) .concurrency as number)) ) { @@ -416,7 +460,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 +477,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 */ @@ -466,7 +509,7 @@ export abstract class AbstractPool< workerNodeKey: number, task: Task ): void { - const workerUsage = this.workerNodes[workerNodeKey].workerUsage + const workerUsage = this.workerNodes[workerNodeKey].usage ++workerUsage.tasks.executing this.updateWaitTimeWorkerUsage(workerUsage, task) } @@ -482,8 +525,7 @@ export abstract class AbstractPool< worker: Worker, message: MessageValue ): void { - const workerUsage = - this.workerNodes[this.getWorkerNodeKey(worker)].workerUsage + const workerUsage = this.workerNodes[this.getWorkerNodeKey(worker)].usage this.updateTaskStatisticsWorkerUsage(workerUsage, message) this.updateRunTimeWorkerUsage(workerUsage, message) this.updateEluWorkerUsage(workerUsage, message) @@ -609,18 +651,28 @@ 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 { + private chooseWorkerNode (): number { if (this.shallCreateDynamicWorker()) { - return this.getWorkerNodeKey(this.createAndSetupDynamicWorker()) + const worker = this.createAndSetupDynamicWorker() + if ( + this.workerChoiceStrategyContext.getStrategyPolicy().useDynamicWorker + ) { + return this.getWorkerNodeKey(worker) + } } return this.workerChoiceStrategyContext.execute() } - protected shallCreateDynamicWorker (): boolean { + /** + * 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() } @@ -641,23 +693,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. @@ -673,7 +732,7 @@ export abstract class AbstractPool< if (this.emitter != null) { this.emitter.emit(PoolEvents.error, error) } - if (this.opts.restartWorkerOnError === true) { + if (this.opts.restartWorkerOnError === true && !this.starting) { this.createAndSetupWorker() } }) @@ -700,16 +759,17 @@ export abstract class AbstractPool< protected createAndSetupDynamicWorker (): Worker { const worker = this.createAndSetupWorker() this.registerWorkerMessageListener(worker, message => { - const currentWorkerNodeKey = this.getWorkerNodeKey(worker) + const workerNodeKey = this.getWorkerNodeKey(worker) if ( isKillBehavior(KillBehaviors.HARD, message.kill) || (message.kill != null && - this.workerNodes[currentWorkerNodeKey].workerUsage.tasks.executing === - 0) + ((this.opts.enableTasksQueue === false && + this.workerNodes[workerNodeKey].usage.tasks.executing === 0) || + (this.opts.enableTasksQueue === true && + this.workerNodes[workerNodeKey].usage.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) - this.flushTasksQueue(currentWorkerNodeKey) - // FIXME: wait for tasks to be finished void (this.destroyWorker(worker) as Promise) } }) @@ -723,15 +783,24 @@ export abstract class AbstractPool< */ protected workerListener (): (message: MessageValue) => void { return message => { - if (message.id != null) { + if (message.workerId != null && message.started != null) { + // Worker started message received + const worker = this.getWorkerById(message.workerId) + if (worker != null) { + this.workerNodes[this.getWorkerNodeKey(worker)].info.started = + message.started + } else { + throw new Error('Worker started message received from unknown worker') + } + } else if (message.id != null) { // Task execution response received 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) } @@ -747,6 +816,7 @@ export abstract class AbstractPool< this.dequeueTask(workerNodeKey) as Task ) } + this.workerChoiceStrategyContext.update(workerNodeKey) } } } @@ -773,7 +843,7 @@ export abstract class AbstractPool< workerNode: WorkerNode, workerUsage: WorkerUsage ): void { - workerNode.workerUsage = workerUsage + workerNode.usage = workerUsage } /** @@ -783,11 +853,32 @@ 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), + info: { id: this.getWorkerId(worker), started: false }, + usage: this.getWorkerUsage(), tasksQueue: new Queue>() }) + const workerNodeKey = this.getWorkerNodeKey(worker) + this.setWorkerNodeTasksUsage( + this.workerNodes[workerNodeKey], + this.getWorkerUsage(workerNodeKey) + ) + return this.workerNodes.length + } + + /** + * Gets the worker id. + * + * @param worker - The worker. + * @returns The worker id. + */ + private getWorkerId (worker: Worker): number | undefined { + if (this.worker === WorkerTypes.thread) { + return worker.threadId + } else if (this.worker === WorkerTypes.cluster) { + return worker.id + } } // /** @@ -795,18 +886,21 @@ export abstract class AbstractPool< // * // * @param workerNodeKey - The worker node key. // * @param worker - The worker. + // * @param workerInfo - The worker info. // * @param workerUsage - The worker usage. // * @param tasksQueue - The worker task queue. // */ // private setWorkerNode ( // workerNodeKey: number, // worker: Worker, + // workerInfo: WorkerInfo, // workerUsage: WorkerUsage, // tasksQueue: Queue> // ): void { // this.workerNodes[workerNodeKey] = { // worker, - // workerUsage, + // info: workerInfo, + // usage: workerUsage, // tasksQueue // } // } @@ -841,6 +935,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++) { @@ -850,6 +948,7 @@ export abstract class AbstractPool< ) } } + this.workerNodes[workerNodeKey].tasksQueue.clear() } private flushTasksQueues (): void { @@ -870,9 +969,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, @@ -902,17 +1017,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 - } - } }