X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fpools%2Fabstract-pool.ts;h=0069fb7fadc61c95196982365adfaf60561069fa;hb=c05f0d5094264db0ff50636e794201e60ba7f448;hp=2b59abf27cfa2a37a518610afce2998d2dff1c6a;hpb=6c6afb8463782af0689101b7c67cea80df83018f;p=poolifier.git diff --git a/src/pools/abstract-pool.ts b/src/pools/abstract-pool.ts index 2b59abf2..0069fb7f 100644 --- a/src/pools/abstract-pool.ts +++ b/src/pools/abstract-pool.ts @@ -4,10 +4,12 @@ import type { MessageValue, PromiseResponseWrapper } from '../utility-types' import { DEFAULT_WORKER_CHOICE_STRATEGY_OPTIONS, EMPTY_FUNCTION, + isKillBehavior, isPlainObject, - median + median, + round } from '../utils' -import { KillBehaviors, isKillBehavior } from '../worker/worker-options' +import { KillBehaviors } from '../worker/worker-options' import { CircularArray } from '../circular-array' import { Queue } from '../queue' import { @@ -19,16 +21,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 +43,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, @@ -75,6 +79,11 @@ export abstract class AbstractPool< Response > + /** + * The start timestamp of the pool. + */ + private readonly startTimestamp + /** * Constructs a new poolifier pool. * @@ -114,9 +123,11 @@ export abstract class AbstractPool< this.setupHook() - for (let i = 1; i <= this.numberOfWorkers; i++) { + while (this.workerNodes.length < this.numberOfWorkers) { this.createAndSetupWorker() } + + this.startTimestamp = performance.now() } private checkFilePath (filePath: string): void { @@ -199,6 +210,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 +228,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}'` ) } } @@ -223,48 +253,80 @@ export abstract class AbstractPool< worker: this.worker, minSize: this.minSize, maxSize: this.maxSize, + ...(this.workerChoiceStrategyContext.getTaskStatisticsRequirements() + .runTime.aggregate && + this.workerChoiceStrategyContext.getTaskStatisticsRequirements().runTime + .aggregate && { utilization: round(this.utilization) }), 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 ) } } + /** + * Gets the pool run time. + * + * @returns The pool run time in milliseconds. + */ + private get runTime (): number { + return performance.now() - this.startTimestamp + } + + /** + * Gets the approximate pool utilization. + * + * @returns The pool utilization. + */ + private get utilization (): number { + const poolRunTimeCapacity = this.runTime * this.maxSize + const totalTasksRunTime = this.workerNodes.reduce( + (accumulator, workerNode) => + accumulator + workerNode.usage.runTime.aggregate, + 0 + ) + const totalTasksWaitTime = this.workerNodes.reduce( + (accumulator, workerNode) => + accumulator + workerNode.usage.waitTime.aggregate, + 0 + ) + return (totalTasksRunTime + totalTasksWaitTime) / poolRunTimeCapacity + } + /** * Pool type. * @@ -287,11 +349,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 +385,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) } @@ -387,7 +460,7 @@ export abstract class AbstractPool< protected internalBusy (): boolean { return ( this.workerNodes.findIndex(workerNode => { - return workerNode.workerUsage.tasks.executing === 0 + return workerNode.usage.tasks.executing === 0 }) === -1 ) } @@ -413,7 +486,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)) ) { @@ -421,7 +494,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 @@ -446,8 +518,8 @@ export abstract class AbstractPool< 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 */ @@ -471,7 +543,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) } @@ -487,8 +559,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) @@ -656,12 +727,15 @@ 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) + } /** - * Creates a newly worker. + * Creates a new worker. * * @returns Newly created worker. */ @@ -669,12 +743,14 @@ export abstract class AbstractPool< /** * 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. @@ -717,16 +793,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) } }) @@ -740,32 +817,55 @@ 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 + this.handleWorkerStartedMessage(message) + } 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) - } - } else { - promiseResponse.resolve(message.data as Response) - } - this.afterTaskExecutionHook(promiseResponse.worker, message) - this.promiseResponseMap.delete(message.id) - const workerNodeKey = this.getWorkerNodeKey(promiseResponse.worker) - if ( - this.opts.enableTasksQueue === true && - this.tasksQueueSize(workerNodeKey) > 0 - ) { - this.executeTask( - workerNodeKey, - this.dequeueTask(workerNodeKey) as Task - ) - } + this.handleTaskExecutionResponse(message) + } + } + } + + private handleWorkerStartedMessage (message: MessageValue): void { + // Worker started message received + const worker = this.getWorkerById(message.workerId as number) + if (worker != null) { + this.workerNodes[this.getWorkerNodeKey(worker)].info.started = + message.started as boolean + } else { + throw new Error( + `Worker started message received from unknown worker '${ + message.workerId as number + }'` + ) + } + } + + private handleTaskExecutionResponse (message: MessageValue): void { + const promiseResponse = this.promiseResponseMap.get(message.id as string) + if (promiseResponse != null) { + if (message.taskError != null) { + if (this.emitter != null) { + this.emitter.emit(PoolEvents.taskError, message.taskError) } + promiseResponse.reject(message.taskError.message) + } else { + promiseResponse.resolve(message.data as Response) + } + this.afterTaskExecutionHook(promiseResponse.worker, message) + this.promiseResponseMap.delete(message.id as string) + const workerNodeKey = this.getWorkerNodeKey(promiseResponse.worker) + if ( + this.opts.enableTasksQueue === true && + this.tasksQueueSize(workerNodeKey) > 0 + ) { + this.executeTask( + workerNodeKey, + this.dequeueTask(workerNodeKey) as Task + ) } + this.workerChoiceStrategyContext.update(workerNodeKey) } } @@ -790,7 +890,7 @@ export abstract class AbstractPool< workerNode: WorkerNode, workerUsage: WorkerUsage ): void { - workerNode.workerUsage = workerUsage + workerNode.usage = workerUsage } /** @@ -800,11 +900,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: true }, + 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 + } } // /** @@ -812,18 +933,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 // } // } @@ -858,6 +982,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++) { @@ -867,6 +995,7 @@ export abstract class AbstractPool< ) } } + this.workerNodes[workerNodeKey].tasksQueue.clear() } private flushTasksQueues (): void { @@ -887,9 +1016,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, @@ -919,17 +1064,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 - } - } }