X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fpools%2Fabstract-pool.ts;h=ef5ebeadd6ad08e3ce6a37eb04aa425280a618af;hb=7c48b2a96e4a42d627bc08f67173956f1d2e30af;hp=90cede3a8227f7e21cd7a0f32ec84e4f8e6bdbf6;hpb=6703b9f4492e347500111c42ffddbd8341c8f262;p=poolifier.git diff --git a/src/pools/abstract-pool.ts b/src/pools/abstract-pool.ts index 90cede3a..ef5ebead 100644 --- a/src/pools/abstract-pool.ts +++ b/src/pools/abstract-pool.ts @@ -1,7 +1,7 @@ import { randomUUID } from 'node:crypto' import { performance } from 'node:perf_hooks' -import { existsSync } from 'node:fs' -import { type TransferListItem } from 'node:worker_threads' +import type { TransferListItem } from 'node:worker_threads' +import { EventEmitterAsyncResource } from 'node:events' import type { MessageValue, PromiseResponseWrapper, @@ -17,14 +17,12 @@ import { max, median, min, - round, - updateMeasurementStatistics + round } from '../utils' import { KillBehaviors } from '../worker/worker-options' import type { TaskFunction } from '../worker/task-functions' import { type IPool, - PoolEmitter, PoolEvents, type PoolInfo, type PoolOptions, @@ -36,6 +34,7 @@ import type { IWorker, IWorkerNode, WorkerInfo, + WorkerNodeEventDetail, WorkerType, WorkerUsage } from './worker' @@ -49,6 +48,12 @@ import { import { WorkerChoiceStrategyContext } from './selection-strategies/worker-choice-strategy-context' import { version } from './version' import { WorkerNode } from './worker-node' +import { + checkFilePath, + checkValidTasksQueueOptions, + checkValidWorkerChoiceStrategy, + updateMeasurementStatistics +} from './utils' /** * Base class that implements some shared logic for all poolifier pools. @@ -66,11 +71,15 @@ export abstract class AbstractPool< public readonly workerNodes: Array> = [] /** @inheritDoc */ - public readonly emitter?: PoolEmitter + public emitter?: EventEmitterAsyncResource /** - * The task execution response promise map. - * + * Dynamic pool maximum size property placeholder. + */ + protected readonly max?: number + + /** + * The task execution response promise map: * - `key`: The message id of each submitted task. * - `value`: An object that contains the worker, the execution response promise resolve and reject callbacks. * @@ -89,18 +98,20 @@ export abstract class AbstractPool< > /** - * Dynamic pool maximum size property placeholder. + * The task functions added at runtime map: + * - `key`: The task function name. + * - `value`: The task function itself. */ - protected readonly max?: number + private readonly taskFunctions: Map> - /** - * Whether the pool is starting or not. - */ - private readonly starting: boolean /** * Whether the pool is started or not. */ private started: boolean + /** + * Whether the pool is starting or not. + */ + private starting: boolean /** * The start timestamp of the pool. */ @@ -123,8 +134,8 @@ export abstract class AbstractPool< 'Cannot start a pool from a worker with the same type as the pool' ) } + checkFilePath(this.filePath) this.checkNumberOfWorkers(this.numberOfWorkers) - this.checkFilePath(this.filePath) this.checkPoolOptions(this.opts) this.chooseWorkerNode = this.chooseWorkerNode.bind(this) @@ -132,7 +143,7 @@ export abstract class AbstractPool< this.enqueueTask = this.enqueueTask.bind(this) if (this.opts.enableEvents === true) { - this.emitter = new PoolEmitter() + this.initializeEventEmitter() } this.workerChoiceStrategyContext = new WorkerChoiceStrategyContext< Worker, @@ -146,27 +157,17 @@ export abstract class AbstractPool< this.setupHook() - this.starting = true - this.startPool() + this.taskFunctions = new Map>() + + this.started = false this.starting = false - this.started = true + if (this.opts.startWorkers === true) { + this.start() + } this.startTimestamp = performance.now() } - private checkFilePath (filePath: string): void { - if ( - filePath == null || - typeof filePath !== 'string' || - (typeof filePath === 'string' && filePath.trim().length === 0) - ) { - throw new Error('Please specify a file with a worker implementation') - } - if (!existsSync(filePath)) { - throw new Error(`Cannot find the worker file '${filePath}'`) - } - } - private checkNumberOfWorkers (numberOfWorkers: number): void { if (numberOfWorkers == null) { throw new Error( @@ -185,51 +186,26 @@ export abstract class AbstractPool< } } - protected checkDynamicPoolSize (min: number, max: number): void { - if (this.type === PoolTypes.dynamic) { - if (max == null) { - throw new TypeError( - 'Cannot instantiate a dynamic pool without specifying the maximum pool size' - ) - } else if (!Number.isSafeInteger(max)) { - throw new TypeError( - 'Cannot instantiate a dynamic pool with a non safe integer maximum pool size' - ) - } else if (min > max) { - throw new RangeError( - 'Cannot instantiate a dynamic pool with a maximum pool size inferior to the minimum pool size' - ) - } else if (max === 0) { - throw new RangeError( - 'Cannot instantiate a dynamic pool with a maximum pool size equal to zero' - ) - } else if (min === max) { - throw new RangeError( - 'Cannot instantiate a dynamic pool with a minimum pool size equal to the maximum pool size. Use a fixed pool instead' - ) - } - } - } - private checkPoolOptions (opts: PoolOptions): void { if (isPlainObject(opts)) { + this.opts.startWorkers = opts.startWorkers ?? true + checkValidWorkerChoiceStrategy( + opts.workerChoiceStrategy as WorkerChoiceStrategy + ) this.opts.workerChoiceStrategy = opts.workerChoiceStrategy ?? WorkerChoiceStrategies.ROUND_ROBIN - this.checkValidWorkerChoiceStrategy(this.opts.workerChoiceStrategy) + this.checkValidWorkerChoiceStrategyOptions( + opts.workerChoiceStrategyOptions as WorkerChoiceStrategyOptions + ) this.opts.workerChoiceStrategyOptions = { ...DEFAULT_WORKER_CHOICE_STRATEGY_OPTIONS, ...opts.workerChoiceStrategyOptions } - this.checkValidWorkerChoiceStrategyOptions( - this.opts.workerChoiceStrategyOptions - ) this.opts.restartWorkerOnError = opts.restartWorkerOnError ?? true this.opts.enableEvents = opts.enableEvents ?? true this.opts.enableTasksQueue = opts.enableTasksQueue ?? false if (this.opts.enableTasksQueue) { - this.checkValidTasksQueueOptions( - opts.tasksQueueOptions as TasksQueueOptions - ) + checkValidTasksQueueOptions(opts.tasksQueueOptions as TasksQueueOptions) this.opts.tasksQueueOptions = this.buildTasksQueueOptions( opts.tasksQueueOptions as TasksQueueOptions ) @@ -239,26 +215,19 @@ export abstract class AbstractPool< } } - private checkValidWorkerChoiceStrategy ( - workerChoiceStrategy: WorkerChoiceStrategy - ): void { - if (!Object.values(WorkerChoiceStrategies).includes(workerChoiceStrategy)) { - throw new Error( - `Invalid worker choice strategy '${workerChoiceStrategy}'` - ) - } - } - private checkValidWorkerChoiceStrategyOptions ( workerChoiceStrategyOptions: WorkerChoiceStrategyOptions ): void { - if (!isPlainObject(workerChoiceStrategyOptions)) { + if ( + workerChoiceStrategyOptions != null && + !isPlainObject(workerChoiceStrategyOptions) + ) { throw new TypeError( 'Invalid worker choice strategy options: must be a plain object' ) } if ( - workerChoiceStrategyOptions.retries != null && + workerChoiceStrategyOptions?.retries != null && !Number.isSafeInteger(workerChoiceStrategyOptions.retries) ) { throw new TypeError( @@ -266,7 +235,7 @@ export abstract class AbstractPool< ) } if ( - workerChoiceStrategyOptions.retries != null && + workerChoiceStrategyOptions?.retries != null && workerChoiceStrategyOptions.retries < 0 ) { throw new RangeError( @@ -274,7 +243,7 @@ export abstract class AbstractPool< ) } if ( - workerChoiceStrategyOptions.weights != null && + workerChoiceStrategyOptions?.weights != null && Object.keys(workerChoiceStrategyOptions.weights).length !== this.maxSize ) { throw new Error( @@ -282,7 +251,7 @@ export abstract class AbstractPool< ) } if ( - workerChoiceStrategyOptions.measurement != null && + workerChoiceStrategyOptions?.measurement != null && !Object.values(Measurements).includes( workerChoiceStrategyOptions.measurement ) @@ -293,58 +262,10 @@ export abstract class AbstractPool< } } - private checkValidTasksQueueOptions ( - tasksQueueOptions: TasksQueueOptions - ): void { - if (tasksQueueOptions != null && !isPlainObject(tasksQueueOptions)) { - throw new TypeError('Invalid tasks queue options: must be a plain object') - } - if ( - tasksQueueOptions?.concurrency != null && - !Number.isSafeInteger(tasksQueueOptions?.concurrency) - ) { - throw new TypeError( - 'Invalid worker node tasks concurrency: must be an integer' - ) - } - if ( - tasksQueueOptions?.concurrency != null && - tasksQueueOptions?.concurrency <= 0 - ) { - throw new RangeError( - `Invalid worker node tasks concurrency: ${tasksQueueOptions?.concurrency} is a negative integer or zero` - ) - } - if (tasksQueueOptions?.queueMaxSize != null) { - throw new Error( - 'Invalid tasks queue options: queueMaxSize is deprecated, please use size instead' - ) - } - if ( - tasksQueueOptions?.size != null && - !Number.isSafeInteger(tasksQueueOptions?.size) - ) { - throw new TypeError( - 'Invalid worker node tasks queue size: must be an integer' - ) - } - if (tasksQueueOptions?.size != null && tasksQueueOptions?.size <= 0) { - throw new RangeError( - `Invalid worker node tasks queue size: ${tasksQueueOptions?.size} is a negative integer or zero` - ) - } - } - - private startPool (): void { - while ( - this.workerNodes.reduce( - (accumulator, workerNode) => - !workerNode.info.dynamic ? accumulator + 1 : accumulator, - 0 - ) < this.numberOfWorkers - ) { - this.createAndSetupWorkerNode() - } + private initializeEventEmitter (): void { + this.emitter = new EventEmitterAsyncResource({ + name: `poolifier:${this.type}-${this.worker}-pool` + }) } /** @inheritDoc */ @@ -353,6 +274,7 @@ export abstract class AbstractPool< version, type: this.type, worker: this.worker, + started: this.started, ready: this.ready, strategy: this.opts.workerChoiceStrategy as WorkerChoiceStrategy, minSize: this.minSize, @@ -570,13 +492,10 @@ export abstract class AbstractPool< * @param message - The received message. * @throws {@link https://nodejs.org/api/errors.html#class-error} If the worker id is invalid. */ - private checkMessageWorkerId (message: MessageValue): void { + private checkMessageWorkerId (message: MessageValue): void { if (message.workerId == null) { throw new Error('Worker message received without worker id') - } else if ( - message.workerId != null && - this.getWorkerNodeKeyByWorkerId(message.workerId) === -1 - ) { + } else if (this.getWorkerNodeKeyByWorkerId(message.workerId) === -1) { throw new Error( `Worker message received from unknown worker '${message.workerId}'` ) @@ -601,7 +520,7 @@ export abstract class AbstractPool< * @param workerId - The worker id. * @returns The worker node key if the worker id is found in the pool worker nodes, `-1` otherwise. */ - private getWorkerNodeKeyByWorkerId (workerId: number): number { + private getWorkerNodeKeyByWorkerId (workerId: number | undefined): number { return this.workerNodes.findIndex( workerNode => workerNode.info.id === workerId ) @@ -612,7 +531,7 @@ export abstract class AbstractPool< workerChoiceStrategy: WorkerChoiceStrategy, workerChoiceStrategyOptions?: WorkerChoiceStrategyOptions ): void { - this.checkValidWorkerChoiceStrategy(workerChoiceStrategy) + checkValidWorkerChoiceStrategy(workerChoiceStrategy) this.opts.workerChoiceStrategy = workerChoiceStrategy this.workerChoiceStrategyContext.setWorkerChoiceStrategy( this.opts.workerChoiceStrategy @@ -646,6 +565,8 @@ export abstract class AbstractPool< tasksQueueOptions?: TasksQueueOptions ): void { if (this.opts.enableTasksQueue === true && !enable) { + this.unsetTaskStealing() + this.unsetTasksStealingOnBackPressure() this.flushTasksQueues() } this.opts.enableTasksQueue = enable @@ -655,33 +576,81 @@ export abstract class AbstractPool< /** @inheritDoc */ public setTasksQueueOptions (tasksQueueOptions: TasksQueueOptions): void { if (this.opts.enableTasksQueue === true) { - this.checkValidTasksQueueOptions(tasksQueueOptions) + checkValidTasksQueueOptions(tasksQueueOptions) this.opts.tasksQueueOptions = this.buildTasksQueueOptions(tasksQueueOptions) this.setTasksQueueSize(this.opts.tasksQueueOptions.size as number) + if (this.opts.tasksQueueOptions.taskStealing === true) { + this.setTaskStealing() + } else { + this.unsetTaskStealing() + } + if (this.opts.tasksQueueOptions.tasksStealingOnBackPressure === true) { + this.setTasksStealingOnBackPressure() + } else { + this.unsetTasksStealingOnBackPressure() + } } else if (this.opts.tasksQueueOptions != null) { delete this.opts.tasksQueueOptions } } - private setTasksQueueSize (size: number): void { - for (const workerNode of this.workerNodes) { - workerNode.tasksQueueBackPressureSize = size - } - } - private buildTasksQueueOptions ( tasksQueueOptions: TasksQueueOptions ): TasksQueueOptions { return { ...{ size: Math.pow(this.maxSize, 2), - concurrency: 1 + concurrency: 1, + taskStealing: true, + tasksStealingOnBackPressure: true }, ...tasksQueueOptions } } + private setTasksQueueSize (size: number): void { + for (const workerNode of this.workerNodes) { + workerNode.tasksQueueBackPressureSize = size + } + } + + private setTaskStealing (): void { + for (const [workerNodeKey] of this.workerNodes.entries()) { + this.workerNodes[workerNodeKey].addEventListener( + 'emptyqueue', + this.handleEmptyQueueEvent as EventListener + ) + } + } + + private unsetTaskStealing (): void { + for (const [workerNodeKey] of this.workerNodes.entries()) { + this.workerNodes[workerNodeKey].removeEventListener( + 'emptyqueue', + this.handleEmptyQueueEvent as EventListener + ) + } + } + + private setTasksStealingOnBackPressure (): void { + for (const [workerNodeKey] of this.workerNodes.entries()) { + this.workerNodes[workerNodeKey].addEventListener( + 'backpressure', + this.handleBackPressureEvent as EventListener + ) + } + } + + private unsetTasksStealingOnBackPressure (): void { + for (const [workerNodeKey] of this.workerNodes.entries()) { + this.workerNodes[workerNodeKey].removeEventListener( + 'backpressure', + this.handleBackPressureEvent as EventListener + ) + } + } + /** * Whether the pool is full or not. * @@ -713,54 +682,161 @@ export abstract class AbstractPool< (this.opts.tasksQueueOptions?.concurrency as number) ) === -1 ) - } else { - return ( - this.workerNodes.findIndex( - workerNode => - workerNode.info.ready && workerNode.usage.tasks.executing === 0 - ) === -1 - ) } + return ( + this.workerNodes.findIndex( + workerNode => + workerNode.info.ready && workerNode.usage.tasks.executing === 0 + ) === -1 + ) } - private sendToWorkers (message: Omit, 'workerId'>): number { - let messagesCount = 0 - for (const [workerNodeKey] of this.workerNodes.entries()) { - this.sendToWorker(workerNodeKey, { - ...message, - workerId: this.getWorkerInfo(workerNodeKey).id as number - }) - ++messagesCount - } - return messagesCount + private async sendTaskFunctionOperationToWorker ( + workerNodeKey: number, + message: MessageValue + ): Promise { + return await new Promise((resolve, reject) => { + const taskFunctionOperationListener = ( + message: MessageValue + ): void => { + this.checkMessageWorkerId(message) + const workerId = this.getWorkerInfo(workerNodeKey).id as number + if ( + message.taskFunctionOperationStatus != null && + message.workerId === workerId + ) { + if (message.taskFunctionOperationStatus) { + resolve(true) + } else if (!message.taskFunctionOperationStatus) { + reject( + new Error( + `Task function operation '${ + message.taskFunctionOperation as string + }' failed on worker ${message.workerId} with error: '${ + message.workerError?.message as string + }'` + ) + ) + } + this.deregisterWorkerMessageListener( + this.getWorkerNodeKeyByWorkerId(message.workerId), + taskFunctionOperationListener + ) + } + } + this.registerWorkerMessageListener( + workerNodeKey, + taskFunctionOperationListener + ) + this.sendToWorker(workerNodeKey, message) + }) + } + + private async sendTaskFunctionOperationToWorkers ( + message: MessageValue + ): Promise { + return await new Promise((resolve, reject) => { + const responsesReceived = new Array>() + const taskFunctionOperationsListener = ( + message: MessageValue + ): void => { + this.checkMessageWorkerId(message) + if (message.taskFunctionOperationStatus != null) { + responsesReceived.push(message) + if (responsesReceived.length === this.workerNodes.length) { + if ( + responsesReceived.every( + message => message.taskFunctionOperationStatus === true + ) + ) { + resolve(true) + } else if ( + responsesReceived.some( + message => message.taskFunctionOperationStatus === false + ) + ) { + const errorResponse = responsesReceived.find( + response => response.taskFunctionOperationStatus === false + ) + reject( + new Error( + `Task function operation '${ + message.taskFunctionOperation as string + }' failed on worker ${ + errorResponse?.workerId as number + } with error: '${ + errorResponse?.workerError?.message as string + }'` + ) + ) + } + this.deregisterWorkerMessageListener( + this.getWorkerNodeKeyByWorkerId(message.workerId), + taskFunctionOperationsListener + ) + } + } + } + for (const [workerNodeKey] of this.workerNodes.entries()) { + this.registerWorkerMessageListener( + workerNodeKey, + taskFunctionOperationsListener + ) + this.sendToWorker(workerNodeKey, message) + } + }) } /** @inheritDoc */ public hasTaskFunction (name: string): boolean { - this.sendToWorkers({ - taskFunctionOperation: 'has', - taskFunctionName: name - }) - return true + for (const workerNode of this.workerNodes) { + if ( + Array.isArray(workerNode.info.taskFunctionNames) && + workerNode.info.taskFunctionNames.includes(name) + ) { + return true + } + } + return false } /** @inheritDoc */ - public addTaskFunction (name: string, taskFunction: TaskFunction): boolean { - this.sendToWorkers({ + public async addTaskFunction ( + name: string, + fn: TaskFunction + ): Promise { + if (typeof name !== 'string') { + throw new TypeError('name argument must be a string') + } + if (typeof name === 'string' && name.trim().length === 0) { + throw new TypeError('name argument must not be an empty string') + } + if (typeof fn !== 'function') { + throw new TypeError('fn argument must be a function') + } + const opResult = await this.sendTaskFunctionOperationToWorkers({ taskFunctionOperation: 'add', taskFunctionName: name, - taskFunction: taskFunction.toString() + taskFunction: fn.toString() }) - return true + this.taskFunctions.set(name, fn) + return opResult } /** @inheritDoc */ - public removeTaskFunction (name: string): boolean { - this.sendToWorkers({ + public async removeTaskFunction (name: string): Promise { + if (!this.taskFunctions.has(name)) { + throw new Error( + 'Cannot remove a task function not handled on the pool side' + ) + } + const opResult = await this.sendTaskFunctionOperationToWorkers({ taskFunctionOperation: 'remove', taskFunctionName: name }) - return true + this.deleteTaskFunctionWorkerUsages(name) + this.taskFunctions.delete(name) + return opResult } /** @inheritDoc */ @@ -777,12 +853,17 @@ export abstract class AbstractPool< } /** @inheritDoc */ - public setDefaultTaskFunction (name: string): boolean { - this.sendToWorkers({ + public async setDefaultTaskFunction (name: string): Promise { + return await this.sendTaskFunctionOperationToWorkers({ taskFunctionOperation: 'default', taskFunctionName: name }) - return true + } + + private deleteTaskFunctionWorkerUsages (name: string): void { + for (const workerNode of this.workerNodes) { + workerNode.deleteTaskFunctionWorkerUsage(name) + } } private shallExecuteTask (workerNodeKey: number): boolean { @@ -801,7 +882,7 @@ export abstract class AbstractPool< ): Promise { return await new Promise((resolve, reject) => { if (!this.started) { - reject(new Error('Cannot execute a task on destroyed pool')) + reject(new Error('Cannot execute a task on not started pool')) return } if (name != null && typeof name !== 'string') { @@ -828,7 +909,6 @@ export abstract class AbstractPool< data: data ?? ({} as Data), transferList, timestamp, - workerId: this.getWorkerInfo(workerNodeKey).id as number, taskId: randomUUID() } this.promiseResponseMap.set(task.taskId as string, { @@ -848,6 +928,22 @@ export abstract class AbstractPool< }) } + /** @inheritdoc */ + public start (): void { + this.starting = true + while ( + this.workerNodes.reduce( + (accumulator, workerNode) => + !workerNode.info.dynamic ? accumulator + 1 : accumulator, + 0 + ) < this.numberOfWorkers + ) { + this.createAndSetupWorkerNode() + } + this.starting = false + this.started = true + } + /** @inheritDoc */ public async destroy (): Promise { await Promise.all( @@ -856,22 +952,30 @@ export abstract class AbstractPool< }) ) this.emitter?.emit(PoolEvents.destroy, this.info) + this.emitter?.emitDestroy() this.started = false } protected async sendKillMessageToWorker ( - workerNodeKey: number, - workerId: number + workerNodeKey: number ): Promise { await new Promise((resolve, reject) => { - this.registerWorkerMessageListener(workerNodeKey, message => { + const killMessageListener = (message: MessageValue): void => { + this.checkMessageWorkerId(message) if (message.kill === 'success') { resolve() } else if (message.kill === 'failure') { - reject(new Error(`Worker ${workerId} kill message handling failed`)) + reject( + new Error( + `Kill message handling failed on worker ${ + message.workerId as number + }` + ) + ) } - }) - this.sendToWorker(workerNodeKey, { kill: true, workerId }) + } + this.registerWorkerMessageListener(workerNodeKey, killMessageListener) + this.sendToWorker(workerNodeKey, { kill: true }) }) } @@ -1117,12 +1221,12 @@ export abstract class AbstractPool< const workerNodeKey = this.getWorkerNodeKeyByWorker(worker) const workerInfo = this.getWorkerInfo(workerNodeKey) workerInfo.ready = false - this.workerNodes[workerNodeKey].closeChannel() this.emitter?.emit(PoolEvents.error, error) + this.workerNodes[workerNodeKey].closeChannel() if ( - this.opts.restartWorkerOnError === true && this.started && - !this.starting + !this.starting && + this.opts.restartWorkerOnError === true ) { if (workerInfo.dynamic) { this.createAndSetupDynamicWorkerNode() @@ -1130,7 +1234,7 @@ export abstract class AbstractPool< this.createAndSetupWorkerNode() } } - if (this.opts.enableTasksQueue === true) { + if (this.started && this.opts.enableTasksQueue === true) { this.redistributeQueuedTasks(workerNodeKey) } }) @@ -1154,6 +1258,7 @@ export abstract class AbstractPool< protected createAndSetupDynamicWorkerNode (): number { const workerNodeKey = this.createAndSetupWorkerNode() this.registerWorkerMessageListener(workerNodeKey, message => { + this.checkMessageWorkerId(message) const localWorkerNodeKey = this.getWorkerNodeKeyByWorkerId( message.workerId ) @@ -1168,6 +1273,8 @@ export abstract class AbstractPool< workerUsage.tasks.executing === 0 && this.tasksQueueSize(localWorkerNodeKey) === 0))) ) { + // Flag the worker node as not ready immediately + this.flagWorkerNodeAsNotReady(localWorkerNodeKey) this.destroyWorkerNode(localWorkerNodeKey).catch(error => { this.emitter?.emit(PoolEvents.error, error) }) @@ -1175,9 +1282,19 @@ export abstract class AbstractPool< }) const workerInfo = this.getWorkerInfo(workerNodeKey) this.sendToWorker(workerNodeKey, { - checkActive: true, - workerId: workerInfo.id as number + checkActive: true }) + if (this.taskFunctions.size > 0) { + for (const [taskFunctionName, taskFunction] of this.taskFunctions) { + this.sendTaskFunctionOperationToWorker(workerNodeKey, { + taskFunctionOperation: 'add', + taskFunctionName, + taskFunction: taskFunction.toString() + }).catch(error => { + this.emitter?.emit(PoolEvents.error, error) + }) + } + } workerInfo.dynamic = true if ( this.workerChoiceStrategyContext.getStrategyPolicy().dynamicWorkerReady || @@ -1202,6 +1319,32 @@ export abstract class AbstractPool< listener: (message: MessageValue) => void ): void + /** + * Registers once a listener callback on the worker given its worker node key. + * + * @param workerNodeKey - The worker node key. + * @param listener - The message listener callback. + */ + protected abstract registerOnceWorkerMessageListener< + Message extends Data | Response + >( + workerNodeKey: number, + listener: (message: MessageValue) => void + ): void + + /** + * Deregisters a listener callback on the worker given its worker node key. + * + * @param workerNodeKey - The worker node key. + * @param listener - The message listener callback. + */ + protected abstract deregisterWorkerMessageListener< + Message extends Data | Response + >( + workerNodeKey: number, + listener: (message: MessageValue) => void + ): void + /** * Method hooked up after a worker node has been newly created. * Can be overridden. @@ -1210,16 +1353,27 @@ export abstract class AbstractPool< */ protected afterWorkerNodeSetup (workerNodeKey: number): void { // Listen to worker messages. - this.registerWorkerMessageListener(workerNodeKey, this.workerListener()) + this.registerWorkerMessageListener( + workerNodeKey, + this.workerMessageListener.bind(this) + ) // Send the startup message to worker. this.sendStartupMessageToWorker(workerNodeKey) // Send the statistics message to worker. this.sendStatisticsMessageToWorker(workerNodeKey) if (this.opts.enableTasksQueue === true) { - this.workerNodes[workerNodeKey].onEmptyQueue = - this.taskStealingOnEmptyQueue.bind(this) - this.workerNodes[workerNodeKey].onBackPressure = - this.tasksStealingOnBackPressure.bind(this) + if (this.opts.tasksQueueOptions?.taskStealing === true) { + this.workerNodes[workerNodeKey].addEventListener( + 'emptyqueue', + this.handleEmptyQueueEvent as EventListener + ) + } + if (this.opts.tasksQueueOptions?.tasksStealingOnBackPressure === true) { + this.workerNodes[workerNodeKey].addEventListener( + 'backpressure', + this.handleBackPressureEvent as EventListener + ) + } } } @@ -1243,8 +1397,7 @@ export abstract class AbstractPool< .runTime.aggregate, elu: this.workerChoiceStrategyContext.getTaskStatisticsRequirements() .elu.aggregate - }, - workerId: this.getWorkerInfo(workerNodeKey).id as number + } }) } @@ -1260,11 +1413,7 @@ export abstract class AbstractPool< }, 0 ) - const destinationWorkerNode = this.workerNodes[destinationWorkerNodeKey] - const task = { - ...(this.dequeueTask(workerNodeKey) as Task), - workerId: destinationWorkerNode.info.id as number - } + const task = this.dequeueTask(workerNodeKey) as Task if (this.shallExecuteTask(destinationWorkerNodeKey)) { this.executeTask(destinationWorkerNodeKey, task) } else { @@ -1292,9 +1441,12 @@ export abstract class AbstractPool< } } - private taskStealingOnEmptyQueue (workerId: number): void { - const destinationWorkerNodeKey = this.getWorkerNodeKeyByWorkerId(workerId) - const destinationWorkerNode = this.workerNodes[destinationWorkerNodeKey] + private readonly handleEmptyQueueEvent = ( + event: CustomEvent + ): void => { + const destinationWorkerNodeKey = this.getWorkerNodeKeyByWorkerId( + event.detail.workerId + ) const workerNodes = this.workerNodes .slice() .sort( @@ -1304,14 +1456,11 @@ export abstract class AbstractPool< const sourceWorkerNode = workerNodes.find( workerNode => workerNode.info.ready && - workerNode.info.id !== workerId && + workerNode.info.id !== event.detail.workerId && workerNode.usage.tasks.queued > 0 ) if (sourceWorkerNode != null) { - const task = { - ...(sourceWorkerNode.popTask() as Task), - workerId: destinationWorkerNode.info.id as number - } + const task = sourceWorkerNode.popTask() as Task if (this.shallExecuteTask(destinationWorkerNodeKey)) { this.executeTask(destinationWorkerNodeKey, task) } else { @@ -1324,13 +1473,15 @@ export abstract class AbstractPool< } } - private tasksStealingOnBackPressure (workerId: number): void { + private readonly handleBackPressureEvent = ( + event: CustomEvent + ): void => { const sizeOffset = 1 if ((this.opts.tasksQueueOptions?.size as number) <= sizeOffset) { return } const sourceWorkerNode = - this.workerNodes[this.getWorkerNodeKeyByWorkerId(workerId)] + this.workerNodes[this.getWorkerNodeKeyByWorkerId(event.detail.workerId)] const workerNodes = this.workerNodes .slice() .sort( @@ -1341,14 +1492,11 @@ export abstract class AbstractPool< if ( sourceWorkerNode.usage.tasks.queued > 0 && workerNode.info.ready && - workerNode.info.id !== workerId && + workerNode.info.id !== event.detail.workerId && workerNode.usage.tasks.queued < (this.opts.tasksQueueOptions?.size as number) - sizeOffset ) { - const task = { - ...(sourceWorkerNode.popTask() as Task), - workerId: workerNode.info.id as number - } + const task = sourceWorkerNode.popTask() as Task if (this.shallExecuteTask(workerNodeKey)) { this.executeTask(workerNodeKey, task) } else { @@ -1363,41 +1511,37 @@ export abstract class AbstractPool< } /** - * This method is the listener registered for each worker message. - * - * @returns The listener function to execute when a message is received from a worker. + * This method is the message listener registered on each worker. */ - protected workerListener (): (message: MessageValue) => void { - return message => { - this.checkMessageWorkerId(message) - if (message.ready != null && message.taskFunctionNames != null) { - // Worker ready response received from worker - this.handleWorkerReadyResponse(message) - } else if (message.taskId != null) { - // Task execution response received from worker - this.handleTaskExecutionResponse(message) - } else if (message.taskFunctionNames != null) { - // Task function names message received from worker - this.getWorkerInfo( - this.getWorkerNodeKeyByWorkerId(message.workerId) - ).taskFunctionNames = message.taskFunctionNames - } else if (message.taskFunctionOperation != null) { - // Task function operation response received from worker - } + protected workerMessageListener (message: MessageValue): void { + this.checkMessageWorkerId(message) + if (message.ready != null && message.taskFunctionNames != null) { + // Worker ready response received from worker + this.handleWorkerReadyResponse(message) + } else if (message.taskId != null) { + // Task execution response received from worker + this.handleTaskExecutionResponse(message) + } else if (message.taskFunctionNames != null) { + // Task function names message received from worker + this.getWorkerInfo( + this.getWorkerNodeKeyByWorkerId(message.workerId) + ).taskFunctionNames = message.taskFunctionNames } } private handleWorkerReadyResponse (message: MessageValue): void { if (message.ready === false) { - throw new Error(`Worker ${message.workerId} failed to initialize`) + throw new Error( + `Worker ${message.workerId as number} failed to initialize` + ) } const workerInfo = this.getWorkerInfo( this.getWorkerNodeKeyByWorkerId(message.workerId) ) workerInfo.ready = message.ready as boolean workerInfo.taskFunctionNames = message.taskFunctionNames - if (this.emitter != null && this.ready) { - this.emitter.emit(PoolEvents.ready, this.info) + if (this.ready) { + this.emitter?.emit(PoolEvents.ready, this.info) } } @@ -1456,7 +1600,7 @@ export abstract class AbstractPool< * @returns The worker information. */ protected getWorkerInfo (workerNodeKey: number): WorkerInfo { - return this.workerNodes[workerNodeKey].info + return this.workerNodes[workerNodeKey]?.info } /** @@ -1496,6 +1640,10 @@ export abstract class AbstractPool< } } + protected flagWorkerNodeAsNotReady (workerNodeKey: number): void { + this.getWorkerInfo(workerNodeKey).ready = false + } + /** @inheritDoc */ public hasWorkerNodeBackPressure (workerNodeKey: number): boolean { return (