From 6703b9f4492e347500111c42ffddbd8341c8f262 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Mon, 11 Sep 2023 12:41:50 +0200 Subject: [PATCH] feat: add task tunctions handling on the pool side MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Signed-off-by: Jérôme Benoit --- docs/api.md | 8 +- src/index.ts | 2 +- src/pools/abstract-pool.ts | 88 +++++++++++++---- src/pools/pool.ts | 36 ++++++- src/pools/worker-node.ts | 10 +- src/pools/worker.ts | 2 +- src/utility-types.ts | 29 ++++-- src/worker/abstract-worker.ts | 179 ++++++++++++++++++++-------------- src/worker/cluster-worker.ts | 4 +- src/worker/thread-worker.ts | 4 +- 10 files changed, 251 insertions(+), 111 deletions(-) diff --git a/docs/api.md b/docs/api.md index 07133926..ad8ede83 100644 --- a/docs/api.md +++ b/docs/api.md @@ -7,7 +7,7 @@ - [`pool = new DynamicThreadPool/DynamicClusterPool(min, max, filePath, opts)`](#pool--new-dynamicthreadpooldynamicclusterpoolmin-max-filepath-opts) - [`pool.execute(data, name, transferList)`](#poolexecutedata-name-transferlist) - [`pool.destroy()`](#pooldestroy) - - [`pool.listTaskFunctions()`](#poollisttaskfunctions) + - [`pool.listTaskFunctionNames()`](#poollisttaskfunctionnames) - [`PoolOptions`](#pooloptions) - [`ThreadPoolOptions extends PoolOptions`](#threadpooloptions-extends-pooloptions) - [`ClusterPoolOptions extends PoolOptions`](#clusterpooloptions-extends-pooloptions) @@ -16,7 +16,7 @@ - [`YourWorker.hasTaskFunction(name)`](#yourworkerhastaskfunctionname) - [`YourWorker.addTaskFunction(name, fn)`](#yourworkeraddtaskfunctionname-fn) - [`YourWorker.removeTaskFunction(name)`](#yourworkerremovetaskfunctionname) - - [`YourWorker.listTaskFunctions()`](#yourworkerlisttaskfunctions) + - [`YourWorker.listTaskFunctionNames()`](#yourworkerlisttaskfunctionnames) - [`YourWorker.setDefaultTaskFunction(name)`](#yourworkersetdefaulttaskfunctionname) ## Pool @@ -46,7 +46,7 @@ This method is available on both pool implementations and returns a promise with This method is available on both pool implementations and will call the terminate method on each worker. -### `pool.listTaskFunctions()` +### `pool.listTaskFunctionNames()` This method is available on both pool implementations and returns an array of the task function names. @@ -149,7 +149,7 @@ This method is available on both worker implementations and returns a boolean. This method is available on both worker implementations and returns a boolean. -#### `YourWorker.listTaskFunctions()` +#### `YourWorker.listTaskFunctionNames()` This method is available on both worker implementations and returns an array of the task function names. diff --git a/src/index.ts b/src/index.ts index 08943274..d1610953 100644 --- a/src/index.ts +++ b/src/index.ts @@ -67,7 +67,7 @@ export type { MessageValue, PromiseResponseWrapper, Task, - TaskError, + WorkerError, TaskPerformance, WorkerStatistics, Writable diff --git a/src/pools/abstract-pool.ts b/src/pools/abstract-pool.ts index 6457b155..90cede3a 100644 --- a/src/pools/abstract-pool.ts +++ b/src/pools/abstract-pool.ts @@ -21,6 +21,7 @@ import { updateMeasurementStatistics } from '../utils' import { KillBehaviors } from '../worker/worker-options' +import type { TaskFunction } from '../worker/task-functions' import { type IPool, PoolEmitter, @@ -722,19 +723,68 @@ export abstract class AbstractPool< } } + 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 + } + /** @inheritDoc */ - public listTaskFunctions (): string[] { + public hasTaskFunction (name: string): boolean { + this.sendToWorkers({ + taskFunctionOperation: 'has', + taskFunctionName: name + }) + return true + } + + /** @inheritDoc */ + public addTaskFunction (name: string, taskFunction: TaskFunction): boolean { + this.sendToWorkers({ + taskFunctionOperation: 'add', + taskFunctionName: name, + taskFunction: taskFunction.toString() + }) + return true + } + + /** @inheritDoc */ + public removeTaskFunction (name: string): boolean { + this.sendToWorkers({ + taskFunctionOperation: 'remove', + taskFunctionName: name + }) + return true + } + + /** @inheritDoc */ + public listTaskFunctionNames (): string[] { for (const workerNode of this.workerNodes) { if ( - Array.isArray(workerNode.info.taskFunctions) && - workerNode.info.taskFunctions.length > 0 + Array.isArray(workerNode.info.taskFunctionNames) && + workerNode.info.taskFunctionNames.length > 0 ) { - return workerNode.info.taskFunctions + return workerNode.info.taskFunctionNames } } return [] } + /** @inheritDoc */ + public setDefaultTaskFunction (name: string): boolean { + this.sendToWorkers({ + taskFunctionOperation: 'default', + taskFunctionName: name + }) + return true + } + private shallExecuteTask (workerNodeKey: number): boolean { return ( this.tasksQueueSize(workerNodeKey) === 0 && @@ -921,8 +971,8 @@ export abstract class AbstractPool< const workerInfo = this.getWorkerInfo(workerNodeKey) return ( workerInfo != null && - Array.isArray(workerInfo.taskFunctions) && - workerInfo.taskFunctions.length > 2 + Array.isArray(workerInfo.taskFunctionNames) && + workerInfo.taskFunctionNames.length > 2 ) } @@ -937,7 +987,7 @@ export abstract class AbstractPool< ) { --workerTaskStatistics.executing } - if (message.taskError == null) { + if (message.workerError == null) { ++workerTaskStatistics.executed } else { ++workerTaskStatistics.failed @@ -948,7 +998,7 @@ export abstract class AbstractPool< workerUsage: WorkerUsage, message: MessageValue ): void { - if (message.taskError != null) { + if (message.workerError != null) { return } updateMeasurementStatistics( @@ -975,7 +1025,7 @@ export abstract class AbstractPool< workerUsage: WorkerUsage, message: MessageValue ): void { - if (message.taskError != null) { + if (message.workerError != null) { return } const eluTaskStatisticsRequirements: MeasurementStatisticsRequirements = @@ -1320,17 +1370,19 @@ export abstract class AbstractPool< protected workerListener (): (message: MessageValue) => void { return message => { this.checkMessageWorkerId(message) - if (message.ready != null && message.taskFunctions != null) { + 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.taskFunctions != null) { - // Task functions message received from worker + } else if (message.taskFunctionNames != null) { + // Task function names message received from worker this.getWorkerInfo( this.getWorkerNodeKeyByWorkerId(message.workerId) - ).taskFunctions = message.taskFunctions + ).taskFunctionNames = message.taskFunctionNames + } else if (message.taskFunctionOperation != null) { + // Task function operation response received from worker } } } @@ -1343,19 +1395,19 @@ export abstract class AbstractPool< this.getWorkerNodeKeyByWorkerId(message.workerId) ) workerInfo.ready = message.ready as boolean - workerInfo.taskFunctions = message.taskFunctions + workerInfo.taskFunctionNames = message.taskFunctionNames if (this.emitter != null && this.ready) { this.emitter.emit(PoolEvents.ready, this.info) } } private handleTaskExecutionResponse (message: MessageValue): void { - const { taskId, taskError, data } = message + const { taskId, workerError, data } = message const promiseResponse = this.promiseResponseMap.get(taskId as string) if (promiseResponse != null) { - if (taskError != null) { - this.emitter?.emit(PoolEvents.taskError, taskError) - promiseResponse.reject(taskError.message) + if (workerError != null) { + this.emitter?.emit(PoolEvents.taskError, workerError) + promiseResponse.reject(workerError.message) } else { promiseResponse.resolve(data as Response) } diff --git a/src/pools/pool.ts b/src/pools/pool.ts index 7a103733..6156a042 100644 --- a/src/pools/pool.ts +++ b/src/pools/pool.ts @@ -1,5 +1,6 @@ import { EventEmitter } from 'node:events' import { type TransferListItem } from 'node:worker_threads' +import type { TaskFunction } from '../worker/task-functions' import type { ErrorHandler, ExitHandler, @@ -233,12 +234,45 @@ export interface IPool< * Terminates all workers in this pool. */ readonly destroy: () => Promise + /** + * Whether the specified task function exists in this pool. + * + * @param name - The name of the task function. + * @returns `true` if the task function exists, `false` otherwise. + */ + readonly hasTaskFunction: (name: string) => boolean + /** + * Adds a task function to this pool. + * If a task function with the same name already exists, it will be overwritten. + * + * @param name - The name of the task function. + * @param taskFunction - The task function. + * @returns `true` if the task function was added, `false` otherwise. + */ + readonly addTaskFunction: ( + name: string, + taskFunction: TaskFunction + ) => boolean + /** + * Removes a task function from this pool. + * + * @param name - The name of the task function. + * @returns `true` if the task function was removed, `false` otherwise. + */ + readonly removeTaskFunction: (name: string) => boolean /** * Lists the names of task function available in this pool. * * @returns The names of task function available in this pool. */ - readonly listTaskFunctions: () => string[] + readonly listTaskFunctionNames: () => string[] + /** + * Sets the default task function in this pool. + * + * @param name - The name of the task function. + * @returns `true` if the default task function was set, `false` otherwise. + */ + readonly setDefaultTaskFunction: (name: string) => boolean /** * Sets the worker choice strategy in this pool. * diff --git a/src/pools/worker-node.ts b/src/pools/worker-node.ts index ca275ded..78756560 100644 --- a/src/pools/worker-node.ts +++ b/src/pools/worker-node.ts @@ -139,21 +139,21 @@ implements IWorkerNode { /** @inheritdoc */ public getTaskFunctionWorkerUsage (name: string): WorkerUsage | undefined { - if (!Array.isArray(this.info.taskFunctions)) { + if (!Array.isArray(this.info.taskFunctionNames)) { throw new Error( `Cannot get task function worker usage for task function name '${name}' when task function names list is not yet defined` ) } if ( - Array.isArray(this.info.taskFunctions) && - this.info.taskFunctions.length < 3 + Array.isArray(this.info.taskFunctionNames) && + this.info.taskFunctionNames.length < 3 ) { throw new Error( `Cannot get task function worker usage for task function name '${name}' when task function names list has less than 3 elements` ) } if (name === DEFAULT_TASK_NAME) { - name = this.info.taskFunctions[1] + name = this.info.taskFunctionNames[1] } if (!this.taskFunctionsUsage.has(name)) { this.taskFunctionsUsage.set(name, this.initTaskFunctionWorkerUsage(name)) @@ -227,7 +227,7 @@ implements IWorkerNode { for (const task of this.tasksQueue) { if ( (task.name === DEFAULT_TASK_NAME && - name === (this.info.taskFunctions as string[])[1]) || + name === (this.info.taskFunctionNames as string[])[1]) || (task.name !== DEFAULT_TASK_NAME && name === task.name) ) { ++taskFunctionQueueSize diff --git a/src/pools/worker.ts b/src/pools/worker.ts index 29050455..37d63085 100644 --- a/src/pools/worker.ts +++ b/src/pools/worker.ts @@ -144,7 +144,7 @@ export interface WorkerInfo { /** * Task function names. */ - taskFunctions?: string[] + taskFunctionNames?: string[] } /** diff --git a/src/utility-types.ts b/src/utility-types.ts index e1fb311e..b2470bea 100644 --- a/src/utility-types.ts +++ b/src/utility-types.ts @@ -3,13 +3,13 @@ import type { MessagePort, TransferListItem } from 'node:worker_threads' import type { KillBehavior } from './worker/worker-options' /** - * Task error. + * Worker error. * * @typeParam Data - Type of data sent to the worker triggering an error. This can only be structured-cloneable data. */ -export interface TaskError { +export interface WorkerError { /** - * Task name triggering the error. + * Task function name triggering the error. */ readonly name: string /** @@ -109,17 +109,34 @@ export interface MessageValue */ readonly kill?: KillBehavior | true | 'success' | 'failure' /** - * Task error. + * Worker error. */ - readonly taskError?: TaskError + readonly workerError?: WorkerError /** * Task performance. */ readonly taskPerformance?: TaskPerformance + /** + * Task function operation: + * - `'has'` - Check if a task function exists. + * - `'add'` - Add a task function. + * - `'delete'` - Delete a task function. + * - `'default'` - Set a task function as default. + */ + readonly taskFunctionOperation?: 'has' | 'add' | 'remove' | 'default' + readonly taskFunctionOperationStatus?: boolean + /** + * Task function serialized to string. + */ + readonly taskFunction?: string + /** + * Task function name. + */ + readonly taskFunctionName?: string /** * Task function names. */ - readonly taskFunctions?: string[] + readonly taskFunctionNames?: string[] /** * Whether the worker computes the given statistics or not. */ diff --git a/src/worker/abstract-worker.ts b/src/worker/abstract-worker.ts index 5b3c3a3f..1b64dc4f 100644 --- a/src/worker/abstract-worker.ts +++ b/src/worker/abstract-worker.ts @@ -22,6 +22,11 @@ import type { TaskSyncFunction } from './task-functions' +interface TaskFunctionOperationReturnType { + status: boolean + error?: Error +} + const DEFAULT_MAX_INACTIVE_TIME = 60000 const DEFAULT_WORKER_OPTIONS: WorkerOptions = { /** @@ -172,11 +177,14 @@ export abstract class AbstractWorker< * * @param name - The name of the task function to check. * @returns Whether the worker has a task function with the given name or not. - * @throws {@link https://nodejs.org/api/errors.html#class-typeerror} If the `name` parameter is not a string or an empty string. */ - public hasTaskFunction (name: string): boolean { - this.checkTaskFunctionName(name) - return this.taskFunctions.has(name) + public hasTaskFunction (name: string): TaskFunctionOperationReturnType { + try { + this.checkTaskFunctionName(name) + } catch (error) { + return { status: false, error: error as Error } + } + return { status: this.taskFunctions.has(name) } } /** @@ -186,24 +194,21 @@ export abstract class AbstractWorker< * @param name - The name of the task function to add. * @param fn - The task function to add. * @returns Whether the task function was added or not. - * @throws {@link https://nodejs.org/api/errors.html#class-typeerror} If the `name` parameter is not a string or an empty string. - * @throws {@link https://nodejs.org/api/errors.html#class-error} If the `name` parameter is the default task function reserved name. - * @throws {@link https://nodejs.org/api/errors.html#class-typeerror} If the `fn` parameter is not a function. */ public addTaskFunction ( name: string, fn: TaskFunction - ): boolean { - this.checkTaskFunctionName(name) - if (name === DEFAULT_TASK_NAME) { - throw new Error( - 'Cannot add a task function with the default reserved name' - ) - } - if (typeof fn !== 'function') { - throw new TypeError('fn parameter is not a function') - } + ): TaskFunctionOperationReturnType { try { + this.checkTaskFunctionName(name) + if (name === DEFAULT_TASK_NAME) { + throw new Error( + 'Cannot add a task function with the default reserved name' + ) + } + if (typeof fn !== 'function') { + throw new TypeError('fn parameter is not a function') + } const boundFn = fn.bind(this) if ( this.taskFunctions.get(name) === @@ -213,9 +218,9 @@ export abstract class AbstractWorker< } this.taskFunctions.set(name, boundFn) this.sendTaskFunctionsListToMainWorker() - return true - } catch { - return false + return { status: true } + } catch (error) { + return { status: false, error: error as Error } } } @@ -224,27 +229,29 @@ export abstract class AbstractWorker< * * @param name - The name of the task function to remove. * @returns Whether the task function existed and was removed or not. - * @throws {@link https://nodejs.org/api/errors.html#class-typeerror} If the `name` parameter is not a string or an empty string. - * @throws {@link https://nodejs.org/api/errors.html#class-error} If the `name` parameter is the default task function reserved name. - * @throws {@link https://nodejs.org/api/errors.html#class-error} If the `name` parameter is the task function used as default task function. */ - public removeTaskFunction (name: string): boolean { - this.checkTaskFunctionName(name) - if (name === DEFAULT_TASK_NAME) { - throw new Error( - 'Cannot remove the task function with the default reserved name' - ) - } - if ( - this.taskFunctions.get(name) === this.taskFunctions.get(DEFAULT_TASK_NAME) - ) { - throw new Error( - 'Cannot remove the task function used as the default task function' - ) + public removeTaskFunction (name: string): TaskFunctionOperationReturnType { + try { + this.checkTaskFunctionName(name) + if (name === DEFAULT_TASK_NAME) { + throw new Error( + 'Cannot remove the task function with the default reserved name' + ) + } + if ( + this.taskFunctions.get(name) === + this.taskFunctions.get(DEFAULT_TASK_NAME) + ) { + throw new Error( + 'Cannot remove the task function used as the default task function' + ) + } + const deleteStatus = this.taskFunctions.delete(name) + this.sendTaskFunctionsListToMainWorker() + return { status: deleteStatus } + } catch (error) { + return { status: false, error: error as Error } } - const deleteStatus = this.taskFunctions.delete(name) - this.sendTaskFunctionsListToMainWorker() - return deleteStatus } /** @@ -252,7 +259,7 @@ export abstract class AbstractWorker< * * @returns The names of the worker's task functions. */ - public listTaskFunctions (): string[] { + public listTaskFunctionNames (): string[] { const names: string[] = [...this.taskFunctions.keys()] let defaultTaskFunctionName: string = DEFAULT_TASK_NAME for (const [name, fn] of this.taskFunctions) { @@ -278,30 +285,27 @@ export abstract class AbstractWorker< * * @param name - The name of the task function to use as default task function. * @returns Whether the default task function was set or not. - * @throws {@link https://nodejs.org/api/errors.html#class-typeerror} If the `name` parameter is not a string or an empty string. - * @throws {@link https://nodejs.org/api/errors.html#class-error} If the `name` parameter is the default task function reserved name. - * @throws {@link https://nodejs.org/api/errors.html#class-error} If the `name` parameter is a non-existing task function. */ - public setDefaultTaskFunction (name: string): boolean { - this.checkTaskFunctionName(name) - if (name === DEFAULT_TASK_NAME) { - throw new Error( - 'Cannot set the default task function reserved name as the default task function' - ) - } - if (!this.taskFunctions.has(name)) { - throw new Error( - 'Cannot set the default task function to a non-existing task function' - ) - } + public setDefaultTaskFunction (name: string): TaskFunctionOperationReturnType { try { + this.checkTaskFunctionName(name) + if (name === DEFAULT_TASK_NAME) { + throw new Error( + 'Cannot set the default task function reserved name as the default task function' + ) + } + if (!this.taskFunctions.has(name)) { + throw new Error( + 'Cannot set the default task function to a non-existing task function' + ) + } this.taskFunctions.set( DEFAULT_TASK_NAME, this.taskFunctions.get(name) as TaskFunction ) - return true - } catch { - return false + return { status: true } + } catch (error) { + return { status: false, error: error as Error } } } @@ -334,6 +338,9 @@ export abstract class AbstractWorker< } else if (message.checkActive != null) { // Check active message received message.checkActive ? this.startCheckActive() : this.stopCheckActive() + } else if (message.taskFunctionOperation != null) { + // Task function operation message received + this.handleTaskFunctionOperationMessage(message) } else if (message.taskId != null && message.data != null) { // Task message received this.run(message) @@ -343,6 +350,38 @@ export abstract class AbstractWorker< } } + protected handleTaskFunctionOperationMessage ( + message: MessageValue + ): void { + const { taskFunctionOperation, taskFunction, taskFunctionName } = message + let response!: TaskFunctionOperationReturnType + if (taskFunctionOperation === 'has') { + response = this.hasTaskFunction(taskFunctionName as string) + } else if (taskFunctionOperation === 'add') { + response = this.addTaskFunction( + taskFunctionName as string, + // eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func + new Function(`return ${taskFunction as string}`)() as TaskFunction< + Data, + Response + > + ) + } else if (taskFunctionOperation === 'remove') { + response = this.removeTaskFunction(taskFunctionName as string) + } else if (taskFunctionOperation === 'default') { + response = this.setDefaultTaskFunction(taskFunctionName as string) + } + this.sendToMainWorker({ + taskFunctionOperation, + taskFunctionOperationStatus: response.status, + workerError: { + name: taskFunctionName as string, + message: this.handleError(response.error as Error | string) + }, + workerId: this.id + }) + } + /** * Handles a kill message sent by the main worker. * @@ -452,7 +491,7 @@ export abstract class AbstractWorker< */ protected sendTaskFunctionsListToMainWorker (): void { this.sendToMainWorker({ - taskFunctions: this.listTaskFunctions(), + taskFunctionNames: this.listTaskFunctionNames(), workerId: this.id }) } @@ -460,11 +499,11 @@ export abstract class AbstractWorker< /** * Handles an error and convert it to a string so it can be sent back to the main worker. * - * @param e - The error raised by the worker. + * @param error - The error raised by the worker. * @returns The error message. */ - protected handleError (e: Error | string): string { - return e instanceof Error ? e.message : e + protected handleError (error: Error | string): string { + return error instanceof Error ? error.message : error } /** @@ -478,7 +517,7 @@ export abstract class AbstractWorker< const fn = this.taskFunctions.get(name ?? DEFAULT_TASK_NAME) if (fn == null) { this.sendToMainWorker({ - taskError: { + workerError: { name: name as string, message: `Task function '${name as string}' not found`, data @@ -516,12 +555,11 @@ export abstract class AbstractWorker< workerId: this.id, taskId }) - } catch (e) { - const errorMessage = this.handleError(e as Error | string) + } catch (error) { this.sendToMainWorker({ - taskError: { + workerError: { name: name as string, - message: errorMessage, + message: this.handleError(error as Error | string), data }, workerId: this.id, @@ -555,12 +593,11 @@ export abstract class AbstractWorker< }) return null }) - .catch(e => { - const errorMessage = this.handleError(e as Error | string) + .catch(error => { this.sendToMainWorker({ - taskError: { + workerError: { name: name as string, - message: errorMessage, + message: this.handleError(error as Error | string), data }, workerId: this.id, diff --git a/src/worker/cluster-worker.ts b/src/worker/cluster-worker.ts index 26964aa4..1f9895d6 100644 --- a/src/worker/cluster-worker.ts +++ b/src/worker/cluster-worker.ts @@ -48,13 +48,13 @@ export class ClusterWorker< this.getMainWorker().on('message', this.messageListener.bind(this)) this.sendToMainWorker({ ready: true, - taskFunctions: this.listTaskFunctions(), + taskFunctionNames: this.listTaskFunctionNames(), workerId: this.id }) } catch { this.sendToMainWorker({ ready: false, - taskFunctions: this.listTaskFunctions(), + taskFunctionNames: this.listTaskFunctionNames(), workerId: this.id }) } diff --git a/src/worker/thread-worker.ts b/src/worker/thread-worker.ts index 8d30ef21..ede0b3b5 100644 --- a/src/worker/thread-worker.ts +++ b/src/worker/thread-worker.ts @@ -62,13 +62,13 @@ export class ThreadWorker< this.port.on('message', this.messageListener.bind(this)) this.sendToMainWorker({ ready: true, - taskFunctions: this.listTaskFunctions(), + taskFunctionNames: this.listTaskFunctionNames(), workerId: this.id }) } catch { this.sendToMainWorker({ ready: false, - taskFunctions: this.listTaskFunctions(), + taskFunctionNames: this.listTaskFunctionNames(), workerId: this.id }) } -- 2.34.1