From: Jérôme Benoit Date: Wed, 20 Nov 2024 14:19:15 +0000 (+0100) Subject: chore(deps-dev): apply updates X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=5de88a2af954c95ebc499f700efdf2a9bfb6e855;p=poolifier.git chore(deps-dev): apply updates Signed-off-by: Jérôme Benoit --- diff --git a/.lintstagedrc.js b/.lintstagedrc.js index c7d31289..8e6fad9f 100644 --- a/.lintstagedrc.js +++ b/.lintstagedrc.js @@ -1,8 +1,8 @@ export default { + '**/*.json': ['biome format --no-errors-on-unmatched --write'], '**/*.{md,yml,yaml}': ['prettier --cache --write'], '**/*.{ts,tsx,js,jsx,cjs,mjs}': [ 'biome format --no-errors-on-unmatched --write', 'eslint --cache --fix', ], - '**/*.json': ['biome format --no-errors-on-unmatched --write'], } diff --git a/benchmarks/worker-selection/least.mjs b/benchmarks/worker-selection/least.mjs index 09679b0c..05e3dc6b 100644 --- a/benchmarks/worker-selection/least.mjs +++ b/benchmarks/worker-selection/least.mjs @@ -21,6 +21,23 @@ function generateRandomTasksMap ( const tasksMap = generateRandomTasksMap(60, 20) +/** + * + * @param tasksMap + * @returns + */ +function arraySortSelect (tasksMap) { + const tasksArray = Array.from(tasksMap) + return tasksArray.sort((a, b) => { + if (a[1] < b[1]) { + return -1 + } else if (a[1] > b[1]) { + return 1 + } + return 0 + })[0] +} + /** * * @param tasksMap @@ -40,23 +57,6 @@ function loopSelect (tasksMap) { return [minKey, minValue] } -/** - * - * @param tasksMap - * @returns - */ -function arraySortSelect (tasksMap) { - const tasksArray = Array.from(tasksMap) - return tasksArray.sort((a, b) => { - if (a[1] < b[1]) { - return -1 - } else if (a[1] > b[1]) { - return 1 - } - return 0 - })[0] -} - const defaultComparator = (a, b) => { return a < b } @@ -69,18 +69,6 @@ const randomPivotIndexSelect = (leftIndex, rightIndex) => { return randomInt(leftIndex, rightIndex) } -/** - * - * @param array - * @param index1 - * @param index2 - */ -function swap (array, index1, index2) { - const tmp = array[index1] - array[index1] = array[index2] - array[index2] = tmp -} - /** * * @param array @@ -110,6 +98,72 @@ function partition ( return storeIndex } +/** + * + * @param tasksMap + * @returns + */ +function quickSelectLoop (tasksMap) { + const tasksArray = Array.from(tasksMap) + + return selectLoop(tasksArray, 0, 0, tasksArray.length - 1, (a, b) => { + return a[1] < b[1] + }) +} + +/** + * + * @param tasksMap + * @returns + */ +function quickSelectLoopRandomPivot (tasksMap) { + const tasksArray = Array.from(tasksMap) + + return selectLoop( + tasksArray, + 0, + 0, + tasksArray.length - 1, + (a, b) => { + return a[1] < b[1] + }, + randomPivotIndexSelect + ) +} + +/** + * + * @param tasksMap + * @returns + */ +function quickSelectRecursion (tasksMap) { + const tasksArray = Array.from(tasksMap) + + return selectRecursion(tasksArray, 0, 0, tasksArray.length - 1, (a, b) => { + return a[1] < b[1] + }) +} + +/** + * + * @param tasksMap + * @returns + */ +function quickSelectRecursionRandomPivot (tasksMap) { + const tasksArray = Array.from(tasksMap) + + return selectRecursion( + tasksArray, + 0, + 0, + tasksArray.length - 1, + (a, b) => { + return a[1] < b[1] + }, + randomPivotIndexSelect + ) +} + /** * * @param array @@ -174,68 +228,14 @@ function selectRecursion ( /** * - * @param tasksMap - * @returns - */ -function quickSelectLoop (tasksMap) { - const tasksArray = Array.from(tasksMap) - - return selectLoop(tasksArray, 0, 0, tasksArray.length - 1, (a, b) => { - return a[1] < b[1] - }) -} - -/** - * - * @param tasksMap - * @returns - */ -function quickSelectLoopRandomPivot (tasksMap) { - const tasksArray = Array.from(tasksMap) - - return selectLoop( - tasksArray, - 0, - 0, - tasksArray.length - 1, - (a, b) => { - return a[1] < b[1] - }, - randomPivotIndexSelect - ) -} - -/** - * - * @param tasksMap - * @returns - */ -function quickSelectRecursion (tasksMap) { - const tasksArray = Array.from(tasksMap) - - return selectRecursion(tasksArray, 0, 0, tasksArray.length - 1, (a, b) => { - return a[1] < b[1] - }) -} - -/** - * - * @param tasksMap - * @returns + * @param array + * @param index1 + * @param index2 */ -function quickSelectRecursionRandomPivot (tasksMap) { - const tasksArray = Array.from(tasksMap) - - return selectRecursion( - tasksArray, - 0, - 0, - tasksArray.length - 1, - (a, b) => { - return a[1] < b[1] - }, - randomPivotIndexSelect - ) +function swap (array, index1, index2) { + const tmp = array[index1] + array[index1] = array[index2] + array[index2] = tmp } group('Least used worker tasks distribution', () => { diff --git a/benchmarks/worker-selection/round-robin.mjs b/benchmarks/worker-selection/round-robin.mjs index 64ea78da..4b357931 100644 --- a/benchmarks/worker-selection/round-robin.mjs +++ b/benchmarks/worker-selection/round-robin.mjs @@ -13,6 +13,16 @@ const workers = generateWorkersArray(60) let nextWorkerIndex +/** + * @returns + */ +function roundRobinIncrementModulo () { + const chosenWorker = workers[nextWorkerIndex] + nextWorkerIndex++ + nextWorkerIndex %= workers.length + return chosenWorker +} + /** * @returns */ @@ -43,16 +53,6 @@ function roundRobinTernaryWithPreChoosing () { return chosenWorker } -/** - * @returns - */ -function roundRobinIncrementModulo () { - const chosenWorker = workers[nextWorkerIndex] - nextWorkerIndex++ - nextWorkerIndex %= workers.length - return chosenWorker -} - group('Round robin tasks distribution', () => { bench('Ternary off by one', () => { nextWorkerIndex = 0 diff --git a/examples/typescript/http-client-pool/src/types.ts b/examples/typescript/http-client-pool/src/types.ts index 36df0cd3..83eaed0f 100644 --- a/examples/typescript/http-client-pool/src/types.ts +++ b/examples/typescript/http-client-pool/src/types.ts @@ -1,9 +1,9 @@ import type { AxiosRequestConfig } from 'axios' -import type { URL } from 'node:url' import type { RequestInfo as NodeFetchRequestInfo, RequestInit as NodeFetchRequestInit, } from 'node-fetch' +import type { URL } from 'node:url' export interface WorkerData { axiosRequestConfig?: AxiosRequestConfig diff --git a/examples/typescript/http-server-pool/express-cluster/src/worker.ts b/examples/typescript/http-server-pool/express-cluster/src/worker.ts index 35a08240..5f70b1da 100644 --- a/examples/typescript/http-server-pool/express-cluster/src/worker.ts +++ b/examples/typescript/http-server-pool/express-cluster/src/worker.ts @@ -7,6 +7,16 @@ import { ClusterWorker } from 'poolifier' import type { WorkerData, WorkerResponse } from './types.js' class ExpressWorker extends ClusterWorker { + private static server: Server + + public constructor () { + super(ExpressWorker.startExpress, { + killHandler: () => { + ExpressWorker.server.close() + }, + }) + } + private static readonly factorial = (n: bigint | number): bigint => { if (n === 0 || n === 1) { return 1n @@ -20,8 +30,6 @@ class ExpressWorker extends ClusterWorker { } } - private static server: Server - private static readonly startExpress = ( workerData?: WorkerData ): WorkerResponse => { @@ -58,14 +66,6 @@ class ExpressWorker extends ClusterWorker { status: true, } } - - public constructor () { - super(ExpressWorker.startExpress, { - killHandler: () => { - ExpressWorker.server.close() - }, - }) - } } export const expressWorker = new ExpressWorker() diff --git a/examples/typescript/http-server-pool/express-hybrid/src/express-worker.ts b/examples/typescript/http-server-pool/express-hybrid/src/express-worker.ts index 053a25ab..93f5ed94 100644 --- a/examples/typescript/http-server-pool/express-hybrid/src/express-worker.ts +++ b/examples/typescript/http-server-pool/express-hybrid/src/express-worker.ts @@ -31,6 +31,15 @@ class ExpressWorker extends ClusterWorker< private static server: Server + public constructor () { + super(ExpressWorker.startExpress, { + killHandler: async () => { + await ExpressWorker.requestHandlerPool.destroy() + ExpressWorker.server.close() + }, + }) + } + private static readonly startExpress = ( workerData?: ClusterWorkerData ): ClusterWorkerResponse => { @@ -84,15 +93,6 @@ class ExpressWorker extends ClusterWorker< status: true, } } - - public constructor () { - super(ExpressWorker.startExpress, { - killHandler: async () => { - await ExpressWorker.requestHandlerPool.destroy() - ExpressWorker.server.close() - }, - }) - } } export const expressWorker = new ExpressWorker() diff --git a/examples/typescript/http-server-pool/express-hybrid/src/request-handler-worker.ts b/examples/typescript/http-server-pool/express-hybrid/src/request-handler-worker.ts index aac0d8d0..5edaddad 100644 --- a/examples/typescript/http-server-pool/express-hybrid/src/request-handler-worker.ts +++ b/examples/typescript/http-server-pool/express-hybrid/src/request-handler-worker.ts @@ -10,19 +10,6 @@ class RequestHandlerWorker< Data extends ThreadWorkerData, Response extends ThreadWorkerResponse > extends ThreadWorker { - private static readonly factorial = (n: bigint | number): bigint => { - if (n === 0 || n === 1) { - return 1n - } else { - n = BigInt(n) - let factorial = 1n - for (let i = 1n; i <= n; i++) { - factorial *= i - } - return factorial - } - } - public constructor () { super({ echo: (workerData?: Data) => { @@ -40,6 +27,19 @@ class RequestHandlerWorker< }, }) } + + private static readonly factorial = (n: bigint | number): bigint => { + if (n === 0 || n === 1) { + return 1n + } else { + n = BigInt(n) + let factorial = 1n + for (let i = 1n; i <= n; i++) { + factorial *= i + } + return factorial + } + } } export const requestHandlerWorker = new RequestHandlerWorker< diff --git a/examples/typescript/http-server-pool/express-worker_threads/src/worker.ts b/examples/typescript/http-server-pool/express-worker_threads/src/worker.ts index 3e8699bc..9796ccb7 100644 --- a/examples/typescript/http-server-pool/express-worker_threads/src/worker.ts +++ b/examples/typescript/http-server-pool/express-worker_threads/src/worker.ts @@ -6,19 +6,6 @@ class RequestHandlerWorker< Data extends WorkerData, Response extends WorkerResponse > extends ThreadWorker { - private static readonly factorial: (n: bigint | number) => bigint = n => { - if (n === 0 || n === 1) { - return 1n - } else { - n = BigInt(n) - let factorial = 1n - for (let i = 1n; i <= n; i++) { - factorial *= i - } - return factorial - } - } - public constructor () { super({ echo: (workerData?: Data) => { @@ -36,6 +23,19 @@ class RequestHandlerWorker< }, }) } + + private static readonly factorial: (n: bigint | number) => bigint = n => { + if (n === 0 || n === 1) { + return 1n + } else { + n = BigInt(n) + let factorial = 1n + for (let i = 1n; i <= n; i++) { + factorial *= i + } + return factorial + } + } } export const requestHandlerWorker = new RequestHandlerWorker< diff --git a/examples/typescript/http-server-pool/fastify-cluster/src/worker.ts b/examples/typescript/http-server-pool/fastify-cluster/src/worker.ts index c723e207..7ef97637 100644 --- a/examples/typescript/http-server-pool/fastify-cluster/src/worker.ts +++ b/examples/typescript/http-server-pool/fastify-cluster/src/worker.ts @@ -6,6 +6,16 @@ import { ClusterWorker } from 'poolifier' import type { WorkerData, WorkerResponse } from './types.js' class FastifyWorker extends ClusterWorker { + private static fastify: FastifyInstance + + public constructor () { + super(FastifyWorker.startFastify, { + killHandler: async () => { + await FastifyWorker.fastify.close() + }, + }) + } + private static readonly factorial = (n: bigint | number): bigint => { if (n === 0 || n === 1) { return 1n @@ -19,8 +29,6 @@ class FastifyWorker extends ClusterWorker { } } - private static fastify: FastifyInstance - private static readonly startFastify = async ( workerData?: WorkerData ): Promise => { @@ -48,14 +56,6 @@ class FastifyWorker extends ClusterWorker { status: true, } } - - public constructor () { - super(FastifyWorker.startFastify, { - killHandler: async () => { - await FastifyWorker.fastify.close() - }, - }) - } } export const fastifyWorker = new FastifyWorker() diff --git a/examples/typescript/http-server-pool/fastify-hybrid/src/fastify-worker.ts b/examples/typescript/http-server-pool/fastify-hybrid/src/fastify-worker.ts index 6b10dbd9..a93e229b 100644 --- a/examples/typescript/http-server-pool/fastify-hybrid/src/fastify-worker.ts +++ b/examples/typescript/http-server-pool/fastify-hybrid/src/fastify-worker.ts @@ -13,6 +13,15 @@ class FastifyWorker extends ClusterWorker< > { private static fastify: FastifyInstance + public constructor () { + super(FastifyWorker.startFastify, { + killHandler: async () => { + await FastifyWorker.fastify.pool.destroy() + await FastifyWorker.fastify.close() + }, + }) + } + private static readonly startFastify = async ( workerData?: ClusterWorkerData ): Promise => { @@ -49,15 +58,6 @@ class FastifyWorker extends ClusterWorker< status: true, } } - - public constructor () { - super(FastifyWorker.startFastify, { - killHandler: async () => { - await FastifyWorker.fastify.pool.destroy() - await FastifyWorker.fastify.close() - }, - }) - } } export const fastifyWorker = new FastifyWorker() diff --git a/examples/typescript/http-server-pool/fastify-hybrid/src/request-handler-worker.ts b/examples/typescript/http-server-pool/fastify-hybrid/src/request-handler-worker.ts index aac0d8d0..5edaddad 100644 --- a/examples/typescript/http-server-pool/fastify-hybrid/src/request-handler-worker.ts +++ b/examples/typescript/http-server-pool/fastify-hybrid/src/request-handler-worker.ts @@ -10,19 +10,6 @@ class RequestHandlerWorker< Data extends ThreadWorkerData, Response extends ThreadWorkerResponse > extends ThreadWorker { - private static readonly factorial = (n: bigint | number): bigint => { - if (n === 0 || n === 1) { - return 1n - } else { - n = BigInt(n) - let factorial = 1n - for (let i = 1n; i <= n; i++) { - factorial *= i - } - return factorial - } - } - public constructor () { super({ echo: (workerData?: Data) => { @@ -40,6 +27,19 @@ class RequestHandlerWorker< }, }) } + + private static readonly factorial = (n: bigint | number): bigint => { + if (n === 0 || n === 1) { + return 1n + } else { + n = BigInt(n) + let factorial = 1n + for (let i = 1n; i <= n; i++) { + factorial *= i + } + return factorial + } + } } export const requestHandlerWorker = new RequestHandlerWorker< diff --git a/examples/typescript/http-server-pool/fastify-hybrid/src/types.ts b/examples/typescript/http-server-pool/fastify-hybrid/src/types.ts index b1a6971b..28ddd6ac 100644 --- a/examples/typescript/http-server-pool/fastify-hybrid/src/types.ts +++ b/examples/typescript/http-server-pool/fastify-hybrid/src/types.ts @@ -13,6 +13,12 @@ export interface DataPayload { number?: number } +export interface FastifyPoolifierOptions extends ThreadPoolOptions { + maxWorkers?: number + minWorkers?: number + workerFile: string +} + export interface ThreadWorkerData { data: T } @@ -20,9 +26,3 @@ export interface ThreadWorkerData { export interface ThreadWorkerResponse { data: T } - -export interface FastifyPoolifierOptions extends ThreadPoolOptions { - maxWorkers?: number - minWorkers?: number - workerFile: string -} diff --git a/examples/typescript/http-server-pool/fastify-worker_threads/src/types.ts b/examples/typescript/http-server-pool/fastify-worker_threads/src/types.ts index 255a0fcb..6a61b3fe 100644 --- a/examples/typescript/http-server-pool/fastify-worker_threads/src/types.ts +++ b/examples/typescript/http-server-pool/fastify-worker_threads/src/types.ts @@ -4,6 +4,12 @@ export interface BodyPayload { number?: number } +export interface FastifyPoolifierOptions extends ThreadPoolOptions { + maxWorkers?: number + minWorkers?: number + workerFile: string +} + export interface WorkerData { body: T } @@ -11,9 +17,3 @@ export interface WorkerData { export interface WorkerResponse { body: T } - -export interface FastifyPoolifierOptions extends ThreadPoolOptions { - maxWorkers?: number - minWorkers?: number - workerFile: string -} diff --git a/examples/typescript/http-server-pool/fastify-worker_threads/src/worker.ts b/examples/typescript/http-server-pool/fastify-worker_threads/src/worker.ts index 3e8699bc..9796ccb7 100644 --- a/examples/typescript/http-server-pool/fastify-worker_threads/src/worker.ts +++ b/examples/typescript/http-server-pool/fastify-worker_threads/src/worker.ts @@ -6,19 +6,6 @@ class RequestHandlerWorker< Data extends WorkerData, Response extends WorkerResponse > extends ThreadWorker { - private static readonly factorial: (n: bigint | number) => bigint = n => { - if (n === 0 || n === 1) { - return 1n - } else { - n = BigInt(n) - let factorial = 1n - for (let i = 1n; i <= n; i++) { - factorial *= i - } - return factorial - } - } - public constructor () { super({ echo: (workerData?: Data) => { @@ -36,6 +23,19 @@ class RequestHandlerWorker< }, }) } + + private static readonly factorial: (n: bigint | number) => bigint = n => { + if (n === 0 || n === 1) { + return 1n + } else { + n = BigInt(n) + let factorial = 1n + for (let i = 1n; i <= n; i++) { + factorial *= i + } + return factorial + } + } } export const requestHandlerWorker = new RequestHandlerWorker< diff --git a/examples/typescript/websocket-server-pool/ws-cluster/src/types.ts b/examples/typescript/websocket-server-pool/ws-cluster/src/types.ts index 21c37e37..2149ec47 100644 --- a/examples/typescript/websocket-server-pool/ws-cluster/src/types.ts +++ b/examples/typescript/websocket-server-pool/ws-cluster/src/types.ts @@ -3,15 +3,15 @@ export enum MessageType { factorial = 'factorial', } +export interface DataPayload { + number?: number +} + export interface MessagePayload { data: T type: MessageType } -export interface DataPayload { - number?: number -} - export interface WorkerData { port: number } diff --git a/examples/typescript/websocket-server-pool/ws-cluster/src/worker.ts b/examples/typescript/websocket-server-pool/ws-cluster/src/worker.ts index a5577d7a..5b44a950 100644 --- a/examples/typescript/websocket-server-pool/ws-cluster/src/worker.ts +++ b/examples/typescript/websocket-server-pool/ws-cluster/src/worker.ts @@ -10,6 +10,16 @@ import { } from './types.js' class WebSocketServerWorker extends ClusterWorker { + private static wss: WebSocketServer + + public constructor () { + super(WebSocketServerWorker.startWebSocketServer, { + killHandler: () => { + WebSocketServerWorker.wss.close() + }, + }) + } + private static readonly factorial = (n: bigint | number): bigint => { if (n === 0 || n === 1) { return 1n @@ -39,6 +49,7 @@ class WebSocketServerWorker extends ClusterWorker { ws.on('error', console.error) ws.on('message', (message: RawData) => { const { data, type } = JSON.parse( + // eslint-disable-next-line @typescript-eslint/no-base-to-string message.toString() ) as MessagePayload switch (type) { @@ -72,16 +83,6 @@ class WebSocketServerWorker extends ClusterWorker { status: true, } } - - private static wss: WebSocketServer - - public constructor () { - super(WebSocketServerWorker.startWebSocketServer, { - killHandler: () => { - WebSocketServerWorker.wss.close() - }, - }) - } } export const webSocketServerWorker = new WebSocketServerWorker() diff --git a/examples/typescript/websocket-server-pool/ws-hybrid/src/request-handler-worker.ts b/examples/typescript/websocket-server-pool/ws-hybrid/src/request-handler-worker.ts index 539a03ff..89f53f9d 100644 --- a/examples/typescript/websocket-server-pool/ws-hybrid/src/request-handler-worker.ts +++ b/examples/typescript/websocket-server-pool/ws-hybrid/src/request-handler-worker.ts @@ -10,19 +10,6 @@ class RequestHandlerWorker< Data extends ThreadWorkerData, Response extends ThreadWorkerResponse > extends ThreadWorker { - private static readonly factorial = (n: bigint | number): bigint => { - if (n === 0 || n === 1) { - return 1n - } else { - n = BigInt(n) - let factorial = 1n - for (let i = 1n; i <= n; i++) { - factorial *= i - } - return factorial - } - } - public constructor () { super({ echo: (workerData?: Data) => { @@ -38,6 +25,19 @@ class RequestHandlerWorker< }, }) } + + private static readonly factorial = (n: bigint | number): bigint => { + if (n === 0 || n === 1) { + return 1n + } else { + n = BigInt(n) + let factorial = 1n + for (let i = 1n; i <= n; i++) { + factorial *= i + } + return factorial + } + } } export const requestHandlerWorker = new RequestHandlerWorker< diff --git a/examples/typescript/websocket-server-pool/ws-hybrid/src/types.ts b/examples/typescript/websocket-server-pool/ws-hybrid/src/types.ts index 8bc8a667..843536b8 100644 --- a/examples/typescript/websocket-server-pool/ws-hybrid/src/types.ts +++ b/examples/typescript/websocket-server-pool/ws-hybrid/src/types.ts @@ -5,15 +5,6 @@ export enum MessageType { factorial = 'factorial', } -export interface MessagePayload { - data: T - type: MessageType -} - -export interface DataPayload { - number?: number -} - export interface ClusterWorkerData extends ThreadPoolOptions { maxWorkers?: number minWorkers?: number @@ -26,6 +17,15 @@ export interface ClusterWorkerResponse { status: boolean } +export interface DataPayload { + number?: number +} + +export interface MessagePayload { + data: T + type: MessageType +} + export interface ThreadWorkerData { data: T } diff --git a/examples/typescript/websocket-server-pool/ws-hybrid/src/websocket-server-worker.ts b/examples/typescript/websocket-server-pool/ws-hybrid/src/websocket-server-worker.ts index 68cbdf2a..d5695575 100644 --- a/examples/typescript/websocket-server-pool/ws-hybrid/src/websocket-server-worker.ts +++ b/examples/typescript/websocket-server-pool/ws-hybrid/src/websocket-server-worker.ts @@ -28,6 +28,17 @@ class WebSocketServerWorker extends ClusterWorker< ThreadWorkerResponse > + private static wss: WebSocketServer + + public constructor () { + super(WebSocketServerWorker.startWebSocketServer, { + killHandler: async () => { + await WebSocketServerWorker.requestHandlerPool.destroy() + WebSocketServerWorker.wss.close() + }, + }) + } + private static readonly startWebSocketServer = ( workerData?: ClusterWorkerData ): ClusterWorkerResponse => { @@ -55,6 +66,7 @@ class WebSocketServerWorker extends ClusterWorker< ws.on('error', console.error) ws.on('message', (message: RawData) => { const { data, type } = JSON.parse( + // eslint-disable-next-line @typescript-eslint/no-base-to-string message.toString() ) as MessagePayload switch (type) { @@ -98,17 +110,6 @@ class WebSocketServerWorker extends ClusterWorker< status: true, } } - - private static wss: WebSocketServer - - public constructor () { - super(WebSocketServerWorker.startWebSocketServer, { - killHandler: async () => { - await WebSocketServerWorker.requestHandlerPool.destroy() - WebSocketServerWorker.wss.close() - }, - }) - } } export const webSocketServerWorker = new WebSocketServerWorker() diff --git a/examples/typescript/websocket-server-pool/ws-worker_threads/src/types.ts b/examples/typescript/websocket-server-pool/ws-worker_threads/src/types.ts index b16cd4a4..95499a32 100644 --- a/examples/typescript/websocket-server-pool/ws-worker_threads/src/types.ts +++ b/examples/typescript/websocket-server-pool/ws-worker_threads/src/types.ts @@ -3,15 +3,15 @@ export enum MessageType { factorial = 'factorial', } +export interface DataPayload { + number?: number +} + export interface MessagePayload { data: T type: MessageType } -export interface DataPayload { - number?: number -} - export interface WorkerData { data: T } diff --git a/examples/typescript/websocket-server-pool/ws-worker_threads/src/worker.ts b/examples/typescript/websocket-server-pool/ws-worker_threads/src/worker.ts index 57506690..9bc8ed65 100644 --- a/examples/typescript/websocket-server-pool/ws-worker_threads/src/worker.ts +++ b/examples/typescript/websocket-server-pool/ws-worker_threads/src/worker.ts @@ -6,19 +6,6 @@ class RequestHandlerWorker< Data extends WorkerData, Response extends WorkerResponse > extends ThreadWorker { - private static readonly factorial = (n: bigint | number): bigint => { - if (n === 0 || n === 1) { - return 1n - } else { - n = BigInt(n) - let factorial = 1n - for (let i = 1n; i <= n; i++) { - factorial *= i - } - return factorial - } - } - public constructor () { super({ echo: (workerData?: Data) => { @@ -34,6 +21,19 @@ class RequestHandlerWorker< }, }) } + + private static readonly factorial = (n: bigint | number): bigint => { + if (n === 0 || n === 1) { + return 1n + } else { + n = BigInt(n) + let factorial = 1n + for (let i = 1n; i <= n; i++) { + factorial *= i + } + return factorial + } + } } export const requestHandlerWorker = new RequestHandlerWorker< diff --git a/package.json b/package.json index b0d25cfb..b755d1c7 100644 --- a/package.json +++ b/package.json @@ -119,7 +119,7 @@ "eslint": "^9.15.0", "eslint-define-config": "^2.1.0", "eslint-plugin-jsdoc": "^50.5.0", - "eslint-plugin-perfectionist": "^3.9.1", + "eslint-plugin-perfectionist": "^4.0.3", "globals": "^15.12.0", "husky": "^9.1.7", "lint-staged": "^15.2.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d61bdaa..2731ba0a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,8 +57,8 @@ importers: specifier: ^50.5.0 version: 50.5.0(eslint@9.15.0(jiti@1.21.6)) eslint-plugin-perfectionist: - specifier: ^3.9.1 - version: 3.9.1(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3) + specifier: ^4.0.3 + version: 4.0.3(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3) globals: specifier: ^15.12.0 version: 15.12.0 @@ -726,8 +726,8 @@ packages: '@sinonjs/text-encoding@0.7.3': resolution: {integrity: sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==} - '@stylistic/eslint-plugin@2.10.1': - resolution: {integrity: sha512-U+4yzNXElTf9q0kEfnloI9XbOyD4cnEQCxjUI94q0+W++0GAEQvJ/slwEj9lwjDHfGADRSr+Tco/z0XJvmDfCQ==} + '@stylistic/eslint-plugin@2.11.0': + resolution: {integrity: sha512-PNRHbydNG5EH8NK4c+izdJlxajIR6GxcUhzsYNRsn6Myep4dsZt0qFCz3rCPnkvgO5FYibDcMqgNHUT+zvjYZw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: '>=8.40.0' @@ -1315,24 +1315,11 @@ packages: peerDependencies: eslint: '>=8.23.0' - eslint-plugin-perfectionist@3.9.1: - resolution: {integrity: sha512-9WRzf6XaAxF4Oi5t/3TqKP5zUjERhasHmLFHin2Yw6ZAp/EP/EVA2dr3BhQrrHWCm5SzTMZf0FcjDnBkO2xFkA==} + eslint-plugin-perfectionist@4.0.3: + resolution: {integrity: sha512-CyafnreF6boy4lf1XaF72U8NbkwrfjU/mOf1y6doaDMS9zGXhUU1DSk+ZPf/rVwCf1PL1m+rhHqFs+IcB8kDmA==} engines: {node: ^18.0.0 || >=20.0.0} peerDependencies: - astro-eslint-parser: ^1.0.2 eslint: '>=8.0.0' - svelte: '>=3.0.0' - svelte-eslint-parser: ^0.41.1 - vue-eslint-parser: '>=9.0.0' - peerDependenciesMeta: - astro-eslint-parser: - optional: true - svelte: - optional: true - svelte-eslint-parser: - optional: true - vue-eslint-parser: - optional: true eslint-plugin-promise@7.1.0: resolution: {integrity: sha512-8trNmPxdAy3W620WKDpaS65NlM5yAumod6XeC4LOb+jxlkG4IVcp68c6dXY2ev+uT4U1PtG57YDV6EGAXN0GbQ==} @@ -2056,12 +2043,13 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - natural-compare-lite@1.4.0: - resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} - natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + natural-orderby@5.0.0: + resolution: {integrity: sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg==} + engines: {node: '>=18'} + neostandard@0.11.8: resolution: {integrity: sha512-gKgvK0EDlA0x7F1hMu/UdPCRtSSG4d9CBxVym4PMWoHz8+a1ffEz2fPU5FMH3RUqzL1ACvroUMiywMGWZSY+Mw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2730,8 +2718,8 @@ packages: engines: {node: '>= 14'} hasBin: true - yaml@2.6.0: - resolution: {integrity: sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==} + yaml@2.6.1: + resolution: {integrity: sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==} engines: {node: '>= 14'} hasBin: true @@ -3371,7 +3359,7 @@ snapshots: '@sinonjs/text-encoding@0.7.3': {} - '@stylistic/eslint-plugin@2.10.1(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3)': + '@stylistic/eslint-plugin@2.11.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3)': dependencies: '@typescript-eslint/utils': 8.15.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3) eslint: 9.15.0(jiti@1.21.6) @@ -3806,7 +3794,7 @@ snapshots: dependencies: '@cspell/cspell-types': 8.16.0 comment-json: 4.2.5 - yaml: 2.6.0 + yaml: 2.6.1 cspell-dictionary@8.16.0: dependencies: @@ -4111,13 +4099,12 @@ snapshots: minimatch: 9.0.5 semver: 7.6.3 - eslint-plugin-perfectionist@3.9.1(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3): + eslint-plugin-perfectionist@4.0.3(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3): dependencies: '@typescript-eslint/types': 8.15.0 '@typescript-eslint/utils': 8.15.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3) eslint: 9.15.0(jiti@1.21.6) - minimatch: 9.0.5 - natural-compare-lite: 1.4.0 + natural-orderby: 5.0.0 transitivePeerDependencies: - supports-color - typescript @@ -4914,14 +4901,14 @@ snapshots: ms@2.1.3: {} - natural-compare-lite@1.4.0: {} - natural-compare@1.4.0: {} + natural-orderby@5.0.0: {} + neostandard@0.11.8(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3): dependencies: '@humanwhocodes/gitignore-to-minimatch': 1.0.2 - '@stylistic/eslint-plugin': 2.10.1(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3) + '@stylistic/eslint-plugin': 2.11.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3) eslint: 9.15.0(jiti@1.21.6) eslint-plugin-n: 17.13.2(eslint@9.15.0(jiti@1.21.6)) eslint-plugin-promise: 7.1.0(eslint@9.15.0(jiti@1.21.6)) @@ -5509,7 +5496,7 @@ snapshots: minimatch: 9.0.5 shiki: 1.23.1 typescript: 5.6.3 - yaml: 2.6.0 + yaml: 2.6.1 typescript-eslint@8.15.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3): dependencies: @@ -5662,7 +5649,7 @@ snapshots: yaml@2.5.1: {} - yaml@2.6.0: {} + yaml@2.6.1: {} yargs-parser@20.2.9: {} diff --git a/src/circular-buffer.ts b/src/circular-buffer.ts index 803af129..bcd5920a 100644 --- a/src/circular-buffer.ts +++ b/src/circular-buffer.ts @@ -8,11 +8,11 @@ export const defaultBufferSize = 2048 * @internal */ export class CircularBuffer { + public size: number private readonly items: Float32Array private readonly maxArrayIdx: number private readIdx: number private writeIdx: number - public size: number /** * @param size - Buffer size. @defaultValue defaultBufferSize @@ -27,23 +27,6 @@ export class CircularBuffer { this.items = new Float32Array(size).fill(-1) } - /** - * Checks the buffer size. - * @param size - Buffer size. - */ - private checkSize (size: number): void { - if (!Number.isSafeInteger(size)) { - throw new TypeError( - `Invalid circular buffer size: '${size.toString()}' is not an integer` - ) - } - if (size < 0) { - throw new RangeError( - `Invalid circular buffer size: ${size.toString()} < 0` - ) - } - } - /** * Checks whether the buffer is empty. * @returns Whether the buffer is empty. @@ -94,4 +77,21 @@ export class CircularBuffer { public toArray (): number[] { return Array.from(this.items.filter(item => item !== -1)) } + + /** + * Checks the buffer size. + * @param size - Buffer size. + */ + private checkSize (size: number): void { + if (!Number.isSafeInteger(size)) { + throw new TypeError( + `Invalid circular buffer size: '${size.toString()}' is not an integer` + ) + } + if (size < 0) { + throw new RangeError( + `Invalid circular buffer size: ${size.toString()} < 0` + ) + } + } } diff --git a/src/pools/abstract-pool.ts b/src/pools/abstract-pool.ts index 6b551d70..f3f1d849 100644 --- a/src/pools/abstract-pool.ts +++ b/src/pools/abstract-pool.ts @@ -82,2307 +82,2306 @@ export abstract class AbstractPool< Data = unknown, Response = unknown > implements IPool { - /** - * The task execution response promise map: - * - `key`: The message id of each submitted task. - * - `value`: An object that contains task's worker node key, execution response promise resolve and reject callbacks, async resource. - * - * When we receive a message from the worker, we get a map entry with the promise resolve/reject bound to the message id. - */ - protected promiseResponseMap: Map< - `${string}-${string}-${string}-${string}-${string}`, - PromiseResponseWrapper - > = new Map< - `${string}-${string}-${string}-${string}-${string}`, - PromiseResponseWrapper - >() - - /** - * Worker choice strategies context referencing worker choice algorithms implementation. - */ - protected workerChoiceStrategiesContext?: WorkerChoiceStrategiesContext< - Worker, - Data, - Response - > - - /** - * This method is the message listener registered on each worker. - * @param message - The message received from the worker. - */ - protected readonly workerMessageListener = ( - message: MessageValue - ): void => { - this.checkMessageWorkerId(message) - const { ready, taskFunctionsProperties, taskId, workerId } = message - if (ready != null && taskFunctionsProperties != null) { - // Worker ready response received from worker - this.handleWorkerReadyResponse(message) - } else if (taskFunctionsProperties != null) { - // Task function properties message received from worker - const workerNodeKey = this.getWorkerNodeKeyByWorkerId(workerId) - const workerInfo = this.getWorkerInfo(workerNodeKey) - if (workerInfo != null) { - workerInfo.taskFunctionsProperties = taskFunctionsProperties - this.sendStatisticsMessageToWorker(workerNodeKey) - this.setTasksQueuePriority(workerNodeKey) - } - } else if (taskId != null) { - // Task execution response received from worker - this.handleTaskExecutionResponse(message) - } - } - - /** - * Whether the pool back pressure event has been emitted or not. - */ - private backPressureEventEmitted: boolean - - /** - * Whether the pool busy event has been emitted or not. - */ - private busyEventEmitted: boolean - - /** - * Whether the pool is destroying or not. - */ - private destroying: boolean - - /** - * Gets task function worker choice strategy, if any. - * @param name - The task function name. - * @returns The task function worker choice strategy if the task function worker choice strategy is defined, `undefined` otherwise. - */ - private readonly getTaskFunctionWorkerChoiceStrategy = ( - name?: string - ): undefined | WorkerChoiceStrategy => { - name = name ?? DEFAULT_TASK_NAME - const taskFunctionsProperties = this.listTaskFunctionsProperties() - if (name === DEFAULT_TASK_NAME) { - name = taskFunctionsProperties[1]?.name - } - return taskFunctionsProperties.find( - (taskFunctionProperties: TaskFunctionProperties) => - taskFunctionProperties.name === name - )?.strategy - } + /** @inheritDoc */ + public emitter?: EventEmitterAsyncResource + /** @inheritDoc */ + public readonly workerNodes: IWorkerNode[] = [] - /** - * Gets the worker choice strategies registered in this pool. - * @returns The worker choice strategies. - */ - private readonly getWorkerChoiceStrategies = - (): Set => { - return new Set([ - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.opts.workerChoiceStrategy!, - ...this.listTaskFunctionsProperties() - .map( - (taskFunctionProperties: TaskFunctionProperties) => - taskFunctionProperties.strategy - ) - .filter( - (strategy: undefined | WorkerChoiceStrategy) => strategy != null + /** @inheritDoc */ + public get info (): PoolInfo { + return { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + defaultStrategy: this.opts.workerChoiceStrategy!, + maxSize: this.maximumNumberOfWorkers ?? this.minimumNumberOfWorkers, + minSize: this.minimumNumberOfWorkers, + ready: this.ready, + started: this.started, + strategyRetries: this.workerChoiceStrategiesContext?.retriesCount ?? 0, + type: this.type, + version, + worker: this.worker, + ...(this.workerChoiceStrategiesContext?.getTaskStatisticsRequirements() + .runTime.aggregate === true && + this.workerChoiceStrategiesContext.getTaskStatisticsRequirements() + .waitTime.aggregate && { + utilization: round(this.utilization), + }), + busyWorkerNodes: this.workerNodes.reduce( + (accumulator, _, workerNodeKey) => + this.isWorkerNodeBusy(workerNodeKey) ? accumulator + 1 : accumulator, + 0 + ), + executedTasks: this.workerNodes.reduce( + (accumulator, workerNode) => + accumulator + workerNode.usage.tasks.executed, + 0 + ), + executingTasks: this.workerNodes.reduce( + (accumulator, workerNode) => + accumulator + workerNode.usage.tasks.executing, + 0 + ), + failedTasks: this.workerNodes.reduce( + (accumulator, workerNode) => + accumulator + workerNode.usage.tasks.failed, + 0 + ), + idleWorkerNodes: this.workerNodes.reduce( + (accumulator, _, workerNodeKey) => + this.isWorkerNodeIdle(workerNodeKey) ? accumulator + 1 : accumulator, + 0 + ), + workerNodes: this.workerNodes.length, + ...(this.type === PoolTypes.dynamic && { + dynamicWorkerNodes: this.workerNodes.reduce( + (accumulator, workerNode) => + workerNode.info.dynamic ? accumulator + 1 : accumulator, + 0 + ), + }), + ...(this.opts.enableTasksQueue === true && { + backPressure: this.backPressure, + backPressureWorkerNodes: this.workerNodes.reduce( + (accumulator, _, workerNodeKey) => + this.isWorkerNodeBackPressured(workerNodeKey) + ? accumulator + 1 + : accumulator, + 0 + ), + maxQueuedTasks: this.workerNodes.reduce( + (accumulator, workerNode) => + accumulator + (workerNode.usage.tasks.maxQueued ?? 0), + 0 + ), + queuedTasks: this.workerNodes.reduce( + (accumulator, workerNode) => + accumulator + workerNode.usage.tasks.queued, + 0 + ), + stealingWorkerNodes: this.workerNodes.reduce( + (accumulator, _, workerNodeKey) => + this.isWorkerNodeStealing(workerNodeKey) + ? accumulator + 1 + : accumulator, + 0 + ), + stolenTasks: this.workerNodes.reduce( + (accumulator, workerNode) => + accumulator + workerNode.usage.tasks.stolen, + 0 + ), + }), + ...(this.workerChoiceStrategiesContext?.getTaskStatisticsRequirements() + .runTime.aggregate === true && { + runTime: { + maximum: round( + max( + ...this.workerNodes.map( + workerNode => + workerNode.usage.runTime.maximum ?? Number.NEGATIVE_INFINITY + ) + ) ), - ]) - } - - /** - * Gets worker node task function priority, if any. - * @param workerNodeKey - The worker node key. - * @param name - The task function name. - * @returns The worker node task function priority if the worker node task function priority is defined, `undefined` otherwise. - */ - private readonly getWorkerNodeTaskFunctionPriority = ( - workerNodeKey: number, - name?: string - ): number | undefined => { - const workerInfo = this.getWorkerInfo(workerNodeKey) - if (workerInfo == null) { - return - } - name = name ?? DEFAULT_TASK_NAME - if (name === DEFAULT_TASK_NAME) { - name = workerInfo.taskFunctionsProperties?.[1]?.name - } - return workerInfo.taskFunctionsProperties?.find( - (taskFunctionProperties: TaskFunctionProperties) => - taskFunctionProperties.name === name - )?.priority - } - - /** - * Gets worker node task function worker choice strategy, if any. - * @param workerNodeKey - The worker node key. - * @param name - The task function name. - * @returns The worker node task function worker choice strategy if the worker node task function worker choice strategy is defined, `undefined` otherwise. - */ - private readonly getWorkerNodeTaskFunctionWorkerChoiceStrategy = ( - workerNodeKey: number, - name?: string - ): undefined | WorkerChoiceStrategy => { - const workerInfo = this.getWorkerInfo(workerNodeKey) - if (workerInfo == null) { - return - } - name = name ?? DEFAULT_TASK_NAME - if (name === DEFAULT_TASK_NAME) { - name = workerInfo.taskFunctionsProperties?.[1]?.name - } - return workerInfo.taskFunctionsProperties?.find( - (taskFunctionProperties: TaskFunctionProperties) => - taskFunctionProperties.name === name - )?.strategy - } - - private readonly handleWorkerNodeBackPressureEvent = ( - eventDetail: WorkerNodeEventDetail - ): void => { - if ( - this.cannotStealTask() || - this.backPressure || - this.isStealingRatioReached() - ) { - return - } - const sizeOffset = 1 - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - if (this.opts.tasksQueueOptions!.size! <= sizeOffset) { - return - } - const { workerId } = eventDetail - const sourceWorkerNode = - this.workerNodes[this.getWorkerNodeKeyByWorkerId(workerId)] - const workerNodes = this.workerNodes - .slice() - .sort( - (workerNodeA, workerNodeB) => - workerNodeA.usage.tasks.queued - workerNodeB.usage.tasks.queued - ) - for (const [workerNodeKey, workerNode] of workerNodes.entries()) { - if (sourceWorkerNode.usage.tasks.queued === 0) { - break - } - if ( - workerNode.info.id !== workerId && - !workerNode.info.backPressureStealing && - workerNode.usage.tasks.queued < - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.opts.tasksQueueOptions!.size! - sizeOffset - ) { - workerNode.info.backPressureStealing = true - this.stealTask(sourceWorkerNode, workerNodeKey) - workerNode.info.backPressureStealing = false - } - } - } - - private readonly handleWorkerNodeIdleEvent = ( - eventDetail: WorkerNodeEventDetail, - previousStolenTask?: Task - ): void => { - const { workerNodeKey } = eventDetail - if (workerNodeKey == null) { - throw new Error( - "WorkerNode event detail 'workerNodeKey' property must be defined" - ) - } - const workerNode = this.workerNodes[workerNodeKey] - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (workerNode == null) { - return - } - if ( - !workerNode.info.continuousStealing && - (this.cannotStealTask() || this.isStealingRatioReached()) - ) { - return - } - const workerNodeTasksUsage = workerNode.usage.tasks - if ( - workerNode.info.continuousStealing && - !this.isWorkerNodeIdle(workerNodeKey) - ) { - workerNode.info.continuousStealing = false - if (workerNodeTasksUsage.sequentiallyStolen > 0) { - this.resetTaskSequentiallyStolenStatisticsWorkerUsage( - workerNodeKey, - previousStolenTask?.name - ) - } - return + minimum: round( + min( + ...this.workerNodes.map( + workerNode => + workerNode.usage.runTime.minimum ?? Number.POSITIVE_INFINITY + ) + ) + ), + ...(this.workerChoiceStrategiesContext.getTaskStatisticsRequirements() + .runTime.average && { + average: round( + average( + this.workerNodes.reduce( + (accumulator, workerNode) => + accumulator.concat( + workerNode.usage.runTime.history.toArray() + ), + [] + ) + ) + ), + }), + ...(this.workerChoiceStrategiesContext.getTaskStatisticsRequirements() + .runTime.median && { + median: round( + median( + this.workerNodes.reduce( + (accumulator, workerNode) => + accumulator.concat( + workerNode.usage.runTime.history.toArray() + ), + [] + ) + ) + ), + }), + }, + }), + ...(this.workerChoiceStrategiesContext?.getTaskStatisticsRequirements() + .waitTime.aggregate === true && { + waitTime: { + maximum: round( + max( + ...this.workerNodes.map( + workerNode => + workerNode.usage.waitTime.maximum ?? Number.NEGATIVE_INFINITY + ) + ) + ), + minimum: round( + min( + ...this.workerNodes.map( + workerNode => + workerNode.usage.waitTime.minimum ?? Number.POSITIVE_INFINITY + ) + ) + ), + ...(this.workerChoiceStrategiesContext.getTaskStatisticsRequirements() + .waitTime.average && { + average: round( + average( + this.workerNodes.reduce( + (accumulator, workerNode) => + accumulator.concat( + workerNode.usage.waitTime.history.toArray() + ), + [] + ) + ) + ), + }), + ...(this.workerChoiceStrategiesContext.getTaskStatisticsRequirements() + .waitTime.median && { + median: round( + median( + this.workerNodes.reduce( + (accumulator, workerNode) => + accumulator.concat( + workerNode.usage.waitTime.history.toArray() + ), + [] + ) + ) + ), + }), + }, + }), + ...(this.workerChoiceStrategiesContext?.getTaskStatisticsRequirements() + .elu.aggregate === true && { + elu: { + active: { + maximum: round( + max( + ...this.workerNodes.map( + workerNode => + workerNode.usage.elu.active.maximum ?? + Number.NEGATIVE_INFINITY + ) + ) + ), + minimum: round( + min( + ...this.workerNodes.map( + workerNode => + workerNode.usage.elu.active.minimum ?? + Number.POSITIVE_INFINITY + ) + ) + ), + ...(this.workerChoiceStrategiesContext.getTaskStatisticsRequirements() + .elu.average && { + average: round( + average( + this.workerNodes.reduce( + (accumulator, workerNode) => + accumulator.concat( + workerNode.usage.elu.active.history.toArray() + ), + [] + ) + ) + ), + }), + ...(this.workerChoiceStrategiesContext.getTaskStatisticsRequirements() + .elu.median && { + median: round( + median( + this.workerNodes.reduce( + (accumulator, workerNode) => + accumulator.concat( + workerNode.usage.elu.active.history.toArray() + ), + [] + ) + ) + ), + }), + }, + idle: { + maximum: round( + max( + ...this.workerNodes.map( + workerNode => + workerNode.usage.elu.idle.maximum ?? + Number.NEGATIVE_INFINITY + ) + ) + ), + minimum: round( + min( + ...this.workerNodes.map( + workerNode => + workerNode.usage.elu.idle.minimum ?? + Number.POSITIVE_INFINITY + ) + ) + ), + ...(this.workerChoiceStrategiesContext.getTaskStatisticsRequirements() + .elu.average && { + average: round( + average( + this.workerNodes.reduce( + (accumulator, workerNode) => + accumulator.concat( + workerNode.usage.elu.idle.history.toArray() + ), + [] + ) + ) + ), + }), + ...(this.workerChoiceStrategiesContext.getTaskStatisticsRequirements() + .elu.median && { + median: round( + median( + this.workerNodes.reduce( + (accumulator, workerNode) => + accumulator.concat( + workerNode.usage.elu.idle.history.toArray() + ), + [] + ) + ) + ), + }), + }, + utilization: { + average: round( + average( + this.workerNodes.map( + workerNode => workerNode.usage.elu.utilization ?? 0 + ) + ) + ), + median: round( + median( + this.workerNodes.map( + workerNode => workerNode.usage.elu.utilization ?? 0 + ) + ) + ), + }, + }, + }), } - workerNode.info.continuousStealing = true - const stolenTask = this.workerNodeStealTask(workerNodeKey) - this.updateTaskSequentiallyStolenStatisticsWorkerUsage( - workerNodeKey, - stolenTask?.name, - previousStolenTask?.name - ) - sleep(exponentialDelay(workerNodeTasksUsage.sequentiallyStolen)) - .then(() => { - this.handleWorkerNodeIdleEvent(eventDetail, stolenTask) - return undefined - }) - .catch((error: unknown) => { - this.emitter?.emit(PoolEvents.error, error) - }) - } - - private readonly isStealingRatioReached = (): boolean => { - return ( - this.opts.tasksQueueOptions?.tasksStealingRatio === 0 || - (this.info.stealingWorkerNodes ?? 0) > - Math.ceil( - this.workerNodes.length * - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.opts.tasksQueueOptions!.tasksStealingRatio! - ) - ) } /** - * Whether the pool ready event has been emitted or not. - */ - private readyEventEmitted: boolean - - /** - * Whether the pool is started or not. - */ - private started: boolean - - /** - * Whether the pool is starting or not. - */ - private starting: boolean - - /** - * Whether the minimum number of workers is starting or not. + * The task execution response promise map: + * - `key`: The message id of each submitted task. + * - `value`: An object that contains task's worker node key, execution response promise resolve and reject callbacks, async resource. + * + * When we receive a message from the worker, we get a map entry with the promise resolve/reject bound to the message id. */ - private startingMinimumNumberOfWorkers: boolean + protected promiseResponseMap: Map< + `${string}-${string}-${string}-${string}-${string}`, + PromiseResponseWrapper + > = new Map< + `${string}-${string}-${string}-${string}-${string}`, + PromiseResponseWrapper + >() /** - * The start timestamp of the pool. + * Worker choice strategies context referencing worker choice algorithms implementation. */ - private startTimestamp?: number - - private readonly stealTask = ( - sourceWorkerNode: IWorkerNode, - destinationWorkerNodeKey: number - ): Task | undefined => { - const destinationWorkerNode = this.workerNodes[destinationWorkerNodeKey] - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (destinationWorkerNode == null) { - return - } - // Avoid cross and cascade task stealing. Could be smarter by checking stealing/stolen worker ids pair. - if ( - !sourceWorkerNode.info.ready || - sourceWorkerNode.info.stolen || - sourceWorkerNode.info.stealing || - !destinationWorkerNode.info.ready || - destinationWorkerNode.info.stolen || - destinationWorkerNode.info.stealing - ) { - return - } - destinationWorkerNode.info.stealing = true - sourceWorkerNode.info.stolen = true - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const stolenTask = sourceWorkerNode.dequeueLastPrioritizedTask()! - sourceWorkerNode.info.stolen = false - destinationWorkerNode.info.stealing = false - this.handleTask(destinationWorkerNodeKey, stolenTask) - this.updateTaskStolenStatisticsWorkerUsage( - destinationWorkerNodeKey, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - stolenTask.name! - ) - return stolenTask - } + protected workerChoiceStrategiesContext?: WorkerChoiceStrategiesContext< + Worker, + Data, + Response + > /** - * The task functions added at runtime map: - * - `key`: The task function name. - * - `value`: The task function object. + * Whether the pool is back pressured or not. + * @returns The pool back pressure boolean status. */ - private readonly taskFunctions: Map< - string, - TaskFunctionObject - > - - private readonly workerNodeStealTask = ( - workerNodeKey: number - ): Task | undefined => { - const workerNodes = this.workerNodes - .slice() - .sort( - (workerNodeA, workerNodeB) => - workerNodeB.usage.tasks.queued - workerNodeA.usage.tasks.queued - ) - const sourceWorkerNode = workerNodes.find( - (sourceWorkerNode, sourceWorkerNodeKey) => - sourceWorkerNodeKey !== workerNodeKey && - sourceWorkerNode.usage.tasks.queued > 0 - ) - if (sourceWorkerNode != null) { - return this.stealTask(sourceWorkerNode, workerNodeKey) - } - } - - /** @inheritDoc */ - public emitter?: EventEmitterAsyncResource - - /** @inheritDoc */ - public readonly workerNodes: IWorkerNode[] = [] + protected abstract get backPressure (): boolean /** - * Constructs a new poolifier pool. - * @param minimumNumberOfWorkers - Minimum number of workers that this pool manages. - * @param filePath - Path to the worker file. - * @param opts - Options for the pool. - * @param maximumNumberOfWorkers - Maximum number of workers that this pool manages. - */ - public constructor ( - protected readonly minimumNumberOfWorkers: number, - protected readonly filePath: string, - protected readonly opts: PoolOptions, - protected readonly maximumNumberOfWorkers?: number - ) { - if (!this.isMain()) { - throw new Error( - 'Cannot start a pool from a worker with the same type as the pool' - ) - } - this.checkPoolType() - checkFilePath(this.filePath) - this.checkMinimumNumberOfWorkers(this.minimumNumberOfWorkers) - this.checkPoolOptions(this.opts) + * Whether the pool is busy or not. + * @returns The pool busyness boolean status. + */ + protected abstract get busy (): boolean - this.chooseWorkerNode = this.chooseWorkerNode.bind(this) - this.executeTask = this.executeTask.bind(this) - this.enqueueTask = this.enqueueTask.bind(this) + /** + * The pool type. + * + * If it is `'dynamic'`, it provides the `max` property. + */ + protected abstract get type (): PoolType - if (this.opts.enableEvents === true) { - this.initEventEmitter() - } - this.workerChoiceStrategiesContext = new WorkerChoiceStrategiesContext< - Worker, - Data, - Response - >( - this, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - [this.opts.workerChoiceStrategy!], - this.opts.workerChoiceStrategyOptions - ) + /** + * The worker type. + */ + protected abstract get worker (): WorkerType - this.setupHook() + /** + * Whether the pool back pressure event has been emitted or not. + */ + private backPressureEventEmitted: boolean - this.taskFunctions = new Map>() + /** + * Whether the pool busy event has been emitted or not. + */ + private busyEventEmitted: boolean - this.started = false - this.starting = false - this.destroying = false - this.readyEventEmitted = false - this.busyEventEmitted = false - this.backPressureEventEmitted = false - this.startingMinimumNumberOfWorkers = false - if (this.opts.startWorkers === true) { - this.start() - } - } + /** + * Whether the pool is destroying or not. + */ + private destroying: boolean /** - * Hook executed after the worker task execution. - * Can be overridden. - * @param workerNodeKey - The worker node key. - * @param message - The received message. + * Whether the pool ready event has been emitted or not. */ - protected afterTaskExecutionHook ( - workerNodeKey: number, - message: MessageValue - ): void { - let needWorkerChoiceStrategiesUpdate = false - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (this.workerNodes[workerNodeKey]?.usage != null) { - const workerUsage = this.workerNodes[workerNodeKey].usage - updateTaskStatisticsWorkerUsage(workerUsage, message) - updateRunTimeWorkerUsage( - this.workerChoiceStrategiesContext, - workerUsage, - message - ) - updateEluWorkerUsage( - this.workerChoiceStrategiesContext, - workerUsage, - message - ) - needWorkerChoiceStrategiesUpdate = true - } - if ( - this.shallUpdateTaskFunctionWorkerUsage(workerNodeKey) && - message.taskPerformance?.name != null && - this.workerNodes[workerNodeKey].getTaskFunctionWorkerUsage( - message.taskPerformance.name - ) != null - ) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const taskFunctionWorkerUsage = this.workerNodes[ - workerNodeKey - ].getTaskFunctionWorkerUsage(message.taskPerformance.name)! - updateTaskStatisticsWorkerUsage(taskFunctionWorkerUsage, message) - updateRunTimeWorkerUsage( - this.workerChoiceStrategiesContext, - taskFunctionWorkerUsage, - message - ) - updateEluWorkerUsage( - this.workerChoiceStrategiesContext, - taskFunctionWorkerUsage, - message - ) - needWorkerChoiceStrategiesUpdate = true - } - if (needWorkerChoiceStrategiesUpdate) { - this.workerChoiceStrategiesContext?.update(workerNodeKey) - } - } + private readyEventEmitted: boolean /** - * Method hooked up after a worker node has been newly created. - * Can be overridden. - * @param workerNodeKey - The newly created worker node key. + * Whether the pool is started or not. */ - protected afterWorkerNodeSetup (workerNodeKey: number): void { - // Listen to worker messages. - this.registerWorkerMessageListener( - workerNodeKey, - this.workerMessageListener - ) - // Send the startup message to worker. - this.sendStartupMessageToWorker(workerNodeKey) - // Send the statistics message to worker. - this.sendStatisticsMessageToWorker(workerNodeKey) - if (this.opts.enableTasksQueue === true) { - if (this.opts.tasksQueueOptions?.taskStealing === true) { - this.workerNodes[workerNodeKey].on( - 'idle', - this.handleWorkerNodeIdleEvent - ) - } - if (this.opts.tasksQueueOptions?.tasksStealingOnBackPressure === true) { - this.workerNodes[workerNodeKey].on( - 'backPressure', - this.handleWorkerNodeBackPressureEvent - ) - } - } - } + private started: boolean /** - * Hook executed before the worker task execution. - * Can be overridden. - * @param workerNodeKey - The worker node key. - * @param task - The task to execute. + * Whether the pool is starting or not. */ - protected beforeTaskExecutionHook ( - workerNodeKey: number, - task: Task - ): void { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (this.workerNodes[workerNodeKey]?.usage != null) { - const workerUsage = this.workerNodes[workerNodeKey].usage - ++workerUsage.tasks.executing - updateWaitTimeWorkerUsage( - this.workerChoiceStrategiesContext, - workerUsage, - task - ) - } - if ( - this.shallUpdateTaskFunctionWorkerUsage(workerNodeKey) && - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.workerNodes[workerNodeKey].getTaskFunctionWorkerUsage(task.name!) != - null - ) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const taskFunctionWorkerUsage = this.workerNodes[ - workerNodeKey - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - ].getTaskFunctionWorkerUsage(task.name!)! - ++taskFunctionWorkerUsage.tasks.executing - updateWaitTimeWorkerUsage( - this.workerChoiceStrategiesContext, - taskFunctionWorkerUsage, - task - ) - } - } + private starting: boolean /** - * Emits dynamic worker creation events. + * Whether the minimum number of workers is starting or not. */ - protected abstract checkAndEmitDynamicWorkerCreationEvents (): void + private startingMinimumNumberOfWorkers: boolean /** - * Emits dynamic worker destruction events. + * The start timestamp of the pool. */ - protected abstract checkAndEmitDynamicWorkerDestructionEvents (): void + private startTimestamp?: number /** - * Creates a new, completely set up dynamic worker node. - * @returns New, completely set up dynamic worker node key. + * The task functions added at runtime map: + * - `key`: The task function name. + * - `value`: The task function object. */ - protected createAndSetupDynamicWorkerNode (): number { - const workerNodeKey = this.createAndSetupWorkerNode() - this.registerWorkerMessageListener(workerNodeKey, message => { - this.checkMessageWorkerId(message) - const localWorkerNodeKey = this.getWorkerNodeKeyByWorkerId( - message.workerId - ) - // Kill message received from worker - if ( - isKillBehavior(KillBehaviors.HARD, message.kill) || - (isKillBehavior(KillBehaviors.SOFT, message.kill) && - this.isWorkerNodeIdle(localWorkerNodeKey) && - !this.isWorkerNodeStealing(localWorkerNodeKey)) - ) { - // Flag the worker node as not ready immediately - this.flagWorkerNodeAsNotReady(localWorkerNodeKey) - this.destroyWorkerNode(localWorkerNodeKey).catch((error: unknown) => { - this.emitter?.emit(PoolEvents.error, error) - }) - } - }) - this.sendToWorker(workerNodeKey, { - checkActive: true, - }) - if (this.taskFunctions.size > 0) { - for (const [taskFunctionName, taskFunctionObject] of this.taskFunctions) { - this.sendTaskFunctionOperationToWorker(workerNodeKey, { - taskFunction: taskFunctionObject.taskFunction.toString(), - taskFunctionOperation: 'add', - taskFunctionProperties: buildTaskFunctionProperties( - taskFunctionName, - taskFunctionObject - ), - }).catch((error: unknown) => { - this.emitter?.emit(PoolEvents.error, error) - }) - } - } - const workerNode = this.workerNodes[workerNodeKey] - workerNode.info.dynamic = true - if ( - this.workerChoiceStrategiesContext?.getPolicy().dynamicWorkerReady === - true - ) { - workerNode.info.ready = true + private readonly taskFunctions: Map< + string, + TaskFunctionObject + > + + /** + * Whether the pool is ready or not. + * @returns The pool readiness boolean status. + */ + private get ready (): boolean { + if (!this.started) { + return false } - this.initWorkerNodeUsage(workerNode) - this.checkAndEmitDynamicWorkerCreationEvents() - return workerNodeKey + return ( + this.workerNodes.reduce( + (accumulator, workerNode) => + !workerNode.info.dynamic && workerNode.info.ready + ? accumulator + 1 + : accumulator, + 0 + ) >= this.minimumNumberOfWorkers + ) } /** - * Creates a new, completely set up worker node. - * @returns New, completely set up worker node key. + * The approximate pool utilization. + * @returns The pool utilization. */ - protected createAndSetupWorkerNode (): number { - const workerNode = this.createWorkerNode() - workerNode.registerWorkerEventHandler( - 'online', - this.opts.onlineHandler ?? EMPTY_FUNCTION - ) - workerNode.registerWorkerEventHandler( - 'message', - this.opts.messageHandler ?? EMPTY_FUNCTION - ) - workerNode.registerWorkerEventHandler( - 'error', - this.opts.errorHandler ?? EMPTY_FUNCTION + private get utilization (): number { + if (this.startTimestamp == null) { + return 0 + } + const poolTimeCapacity = + (performance.now() - this.startTimestamp) * + (this.maximumNumberOfWorkers ?? this.minimumNumberOfWorkers) + const totalTasksRunTime = this.workerNodes.reduce( + (accumulator, workerNode) => + accumulator + (workerNode.usage.runTime.aggregate ?? 0), + 0 ) - workerNode.registerOnceWorkerEventHandler('error', (error: Error) => { - workerNode.info.ready = false - this.emitter?.emit(PoolEvents.error, error) - if ( - this.started && - !this.destroying && - this.opts.restartWorkerOnError === true - ) { - if (workerNode.info.dynamic) { - this.createAndSetupDynamicWorkerNode() - } else if (!this.startingMinimumNumberOfWorkers) { - this.startMinimumNumberOfWorkers(true) - } - } - if ( - this.started && - !this.destroying && - this.opts.enableTasksQueue === true - ) { - this.redistributeQueuedTasks(this.workerNodes.indexOf(workerNode)) - } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, promise/no-promise-in-callback - workerNode?.terminate().catch((error: unknown) => { - this.emitter?.emit(PoolEvents.error, error) - }) - }) - workerNode.registerWorkerEventHandler( - 'exit', - this.opts.exitHandler ?? EMPTY_FUNCTION + const totalTasksWaitTime = this.workerNodes.reduce( + (accumulator, workerNode) => + accumulator + (workerNode.usage.waitTime.aggregate ?? 0), + 0 ) - workerNode.registerOnceWorkerEventHandler('exit', () => { - this.removeWorkerNode(workerNode) - if ( - this.started && - !this.startingMinimumNumberOfWorkers && - !this.destroying - ) { - this.startMinimumNumberOfWorkers(true) - } - }) - const workerNodeKey = this.addWorkerNode(workerNode) - this.afterWorkerNodeSetup(workerNodeKey) - return workerNodeKey + return (totalTasksRunTime + totalTasksWaitTime) / poolTimeCapacity } /** - * Deregisters a listener callback on the worker given its worker node key. - * @param workerNodeKey - The worker node key. - * @param listener - The message listener callback. + * Constructs a new poolifier pool. + * @param minimumNumberOfWorkers - Minimum number of workers that this pool manages. + * @param filePath - Path to the worker file. + * @param opts - Options for the pool. + * @param maximumNumberOfWorkers - Maximum number of workers that this pool manages. */ - protected abstract deregisterWorkerMessageListener< - Message extends Data | Response - >( - workerNodeKey: number, - listener: (message: MessageValue) => void - ): void + public constructor ( + protected readonly minimumNumberOfWorkers: number, + protected readonly filePath: string, + protected readonly opts: PoolOptions, + protected readonly maximumNumberOfWorkers?: number + ) { + if (!this.isMain()) { + throw new Error( + 'Cannot start a pool from a worker with the same type as the pool' + ) + } + this.checkPoolType() + checkFilePath(this.filePath) + this.checkMinimumNumberOfWorkers(this.minimumNumberOfWorkers) + this.checkPoolOptions(this.opts) - /** - * Terminates the worker node given its worker node key. - * @param workerNodeKey - The worker node key. - */ - protected async destroyWorkerNode (workerNodeKey: number): Promise { - this.flagWorkerNodeAsNotReady(workerNodeKey) - const flushedTasks = this.flushTasksQueue(workerNodeKey) - const workerNode = this.workerNodes[workerNodeKey] - await waitWorkerNodeEvents( - workerNode, - 'taskFinished', - flushedTasks, - this.opts.tasksQueueOptions?.tasksFinishedTimeout ?? - getDefaultTasksQueueOptions( - this.maximumNumberOfWorkers ?? this.minimumNumberOfWorkers - ).tasksFinishedTimeout + this.chooseWorkerNode = this.chooseWorkerNode.bind(this) + this.executeTask = this.executeTask.bind(this) + this.enqueueTask = this.enqueueTask.bind(this) + + if (this.opts.enableEvents === true) { + this.initEventEmitter() + } + this.workerChoiceStrategiesContext = new WorkerChoiceStrategiesContext< + Worker, + Data, + Response + >( + this, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + [this.opts.workerChoiceStrategy!], + this.opts.workerChoiceStrategyOptions ) - await this.sendKillMessageToWorker(workerNodeKey) - await workerNode.terminate() + + this.setupHook() + + this.taskFunctions = new Map>() + + this.started = false + this.starting = false + this.destroying = false + this.readyEventEmitted = false + this.busyEventEmitted = false + this.backPressureEventEmitted = false + this.startingMinimumNumberOfWorkers = false + if (this.opts.startWorkers === true) { + this.start() + } } - protected flagWorkerNodeAsNotReady (workerNodeKey: number): void { - const workerInfo = this.getWorkerInfo(workerNodeKey) - if (workerInfo != null) { - workerInfo.ready = false + /** @inheritDoc */ + public async addTaskFunction ( + name: string, + fn: TaskFunction | TaskFunctionObject + ): 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') { + fn = { taskFunction: fn } satisfies TaskFunctionObject + } + if (typeof fn.taskFunction !== 'function') { + throw new TypeError('taskFunction property must be a function') + } + checkValidPriority(fn.priority) + checkValidWorkerChoiceStrategy(fn.strategy) + const opResult = await this.sendTaskFunctionOperationToWorkers({ + taskFunction: fn.taskFunction.toString(), + taskFunctionOperation: 'add', + taskFunctionProperties: buildTaskFunctionProperties(name, fn), + }) + this.taskFunctions.set(name, fn) + this.workerChoiceStrategiesContext?.syncWorkerChoiceStrategies( + this.getWorkerChoiceStrategies() + ) + for (const workerNodeKey of this.workerNodes.keys()) { + this.sendStatisticsMessageToWorker(workerNodeKey) + } + return opResult + } + + /** @inheritDoc */ + public async destroy (): Promise { + if (!this.started) { + throw new Error('Cannot destroy an already destroyed pool') + } + if (this.starting) { + throw new Error('Cannot destroy an starting pool') + } + if (this.destroying) { + throw new Error('Cannot destroy an already destroying pool') + } + this.destroying = true + await Promise.all( + this.workerNodes.map(async (_, workerNodeKey) => { + await this.destroyWorkerNode(workerNodeKey) + }) + ) + if (this.emitter != null) { + this.emitter.emit(PoolEvents.destroy, this.info) + this.emitter.emitDestroy() + this.readyEventEmitted = false } + delete this.startTimestamp + this.destroying = false + this.started = false } - protected flushTasksQueue (workerNodeKey: number): number { - let flushedTasks = 0 - while (this.tasksQueueSize(workerNodeKey) > 0) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.executeTask(workerNodeKey, this.dequeueTask(workerNodeKey)!) - ++flushedTasks + /** @inheritDoc */ + public enableTasksQueue ( + enable: boolean, + tasksQueueOptions?: TasksQueueOptions + ): void { + if (this.opts.enableTasksQueue === true && !enable) { + this.unsetTaskStealing() + this.unsetTasksStealingOnBackPressure() + this.flushTasksQueues() + } + this.opts.enableTasksQueue = enable + this.setTasksQueueOptions(tasksQueueOptions) + } + + /** @inheritDoc */ + public async execute ( + data?: Data, + name?: string, + transferList?: readonly TransferListItem[] + ): Promise { + if (!this.started) { + throw new Error('Cannot execute a task on not started pool') + } + if (this.destroying) { + throw new Error('Cannot execute a task on destroying pool') + } + if (name != null && typeof name !== 'string') { + throw new TypeError('name argument must be a string') + } + if (name != null && typeof name === 'string' && name.trim().length === 0) { + throw new TypeError('name argument must not be an empty string') } - this.workerNodes[workerNodeKey].clearTasksQueue() - return flushedTasks - } - - /** - * Gets the worker information given its worker node key. - * @param workerNodeKey - The worker node key. - * @returns The worker information. - */ - protected getWorkerInfo (workerNodeKey: number): undefined | WorkerInfo { - return this.workerNodes[workerNodeKey]?.info + if (transferList != null && !Array.isArray(transferList)) { + throw new TypeError('transferList argument must be an array') + } + return await this.internalExecute(data, name, transferList) } - /** - * Whether the worker nodes are back pressured or not. - * @returns Worker nodes back pressure boolean status. - */ - protected internalBackPressure (): boolean { - return ( - this.workerNodes.reduce( - (accumulator, _, workerNodeKey) => - this.isWorkerNodeBackPressured(workerNodeKey) - ? accumulator + 1 - : accumulator, - 0 - ) === this.workerNodes.length + /** @inheritDoc */ + public hasTaskFunction (name: string): boolean { + return this.listTaskFunctionsProperties().some( + taskFunctionProperties => taskFunctionProperties.name === name ) } - /** - * Whether worker nodes are executing concurrently their tasks quota or not. - * @returns Worker nodes busyness boolean status. - */ - protected internalBusy (): boolean { - return ( - this.workerNodes.reduce( - (accumulator, _, workerNodeKey) => - this.isWorkerNodeBusy(workerNodeKey) ? accumulator + 1 : accumulator, - 0 - ) === this.workerNodes.length - ) + /** @inheritDoc */ + public listTaskFunctionsProperties (): TaskFunctionProperties[] { + for (const workerNode of this.workerNodes) { + if ( + Array.isArray(workerNode.info.taskFunctionsProperties) && + workerNode.info.taskFunctionsProperties.length > 0 + ) { + return workerNode.info.taskFunctionsProperties + } + } + return [] } - /** - * Returns whether the worker is the main worker or not. - * @returns `true` if the worker is the main worker, `false` otherwise. - */ - protected abstract isMain (): boolean - - /** - * 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 - - /** - * Registers 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 registerWorkerMessageListener< - Message extends Data | Response - >( - workerNodeKey: number, - listener: (message: MessageValue) => void - ): void - - /** - * Sends the startup message to worker given its worker node key. - * @param workerNodeKey - The worker node key. - */ - protected abstract sendStartupMessageToWorker (workerNodeKey: number): void - - /** - * Sends a message to worker given its worker node key. - * @param workerNodeKey - The worker node key. - * @param message - The message. - * @param transferList - The optional array of transferable objects. - */ - protected abstract sendToWorker ( - workerNodeKey: number, - message: MessageValue, + /** @inheritDoc */ + public async mapExecute ( + data: Iterable, + name?: string, transferList?: readonly TransferListItem[] - ): void - - /** - * Setup hook to execute code before worker nodes are created in the abstract constructor. - * Can be overridden. - */ - protected setupHook (): void { - /* Intentionally empty */ + ): Promise { + if (!this.started) { + throw new Error('Cannot execute task(s) on not started pool') + } + if (this.destroying) { + throw new Error('Cannot execute task(s) on destroying pool') + } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (data == null) { + throw new TypeError('data argument must be a defined iterable') + } + if (typeof data[Symbol.iterator] !== 'function') { + throw new TypeError('data argument must be an iterable') + } + if (name != null && typeof name !== 'string') { + throw new TypeError('name argument must be a string') + } + if (name != null && typeof name === 'string' && name.trim().length === 0) { + throw new TypeError('name argument must not be an empty string') + } + if (transferList != null && !Array.isArray(transferList)) { + throw new TypeError('transferList argument must be an array') + } + if (!Array.isArray(data)) { + data = [...data] + } + return await Promise.all( + (data as Data[]).map(data => + this.internalExecute(data, name, transferList) + ) + ) } - /** - * Conditions for dynamic worker creation. - * @returns Whether to create a dynamic worker or not. - */ - protected abstract shallCreateDynamicWorker (): boolean - - /** - * Adds the given worker node in the pool worker nodes. - * @param workerNode - The worker node. - * @returns The added worker node key. - * @throws {@link https://nodejs.org/api/errors.html#class-error} If the added worker node is not found. - */ - private addWorkerNode (workerNode: IWorkerNode): number { - this.workerNodes.push(workerNode) - const workerNodeKey = this.workerNodes.indexOf(workerNode) - if (workerNodeKey === -1) { - throw new Error('Worker added not found in worker nodes') + /** @inheritDoc */ + 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' + ) } - return workerNodeKey + const opResult = await this.sendTaskFunctionOperationToWorkers({ + taskFunctionOperation: 'remove', + taskFunctionProperties: buildTaskFunctionProperties( + name, + this.taskFunctions.get(name) + ), + }) + for (const workerNode of this.workerNodes) { + workerNode.deleteTaskFunctionWorkerUsage(name) + } + this.taskFunctions.delete(name) + this.workerChoiceStrategiesContext?.syncWorkerChoiceStrategies( + this.getWorkerChoiceStrategies() + ) + for (const workerNodeKey of this.workerNodes.keys()) { + this.sendStatisticsMessageToWorker(workerNodeKey) + } + return opResult } - private buildTasksQueueOptions ( - tasksQueueOptions: TasksQueueOptions | undefined - ): TasksQueueOptions { - return { - ...getDefaultTasksQueueOptions( - this.maximumNumberOfWorkers ?? this.minimumNumberOfWorkers + /** @inheritDoc */ + public async setDefaultTaskFunction (name: string): Promise { + return await this.sendTaskFunctionOperationToWorkers({ + taskFunctionOperation: 'default', + taskFunctionProperties: buildTaskFunctionProperties( + name, + this.taskFunctions.get(name) ), - ...this.opts.tasksQueueOptions, - ...tasksQueueOptions, - } + }) } - private cannotStealTask (): boolean { - return this.workerNodes.length <= 1 || this.info.queuedTasks === 0 + /** @inheritDoc */ + public setTasksQueueOptions ( + tasksQueueOptions: TasksQueueOptions | undefined + ): void { + if (this.opts.enableTasksQueue === true) { + checkValidTasksQueueOptions(tasksQueueOptions) + this.opts.tasksQueueOptions = + this.buildTasksQueueOptions(tasksQueueOptions) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.setTasksQueueSize(this.opts.tasksQueueOptions.size!) + if (this.opts.tasksQueueOptions.taskStealing === true) { + this.unsetTaskStealing() + this.setTaskStealing() + } else { + this.unsetTaskStealing() + } + if (this.opts.tasksQueueOptions.tasksStealingOnBackPressure === true) { + this.unsetTasksStealingOnBackPressure() + this.setTasksStealingOnBackPressure() + } else { + this.unsetTasksStealingOnBackPressure() + } + } else if (this.opts.tasksQueueOptions != null) { + delete this.opts.tasksQueueOptions + } } - private checkAndEmitReadyEvent (): void { - if (this.emitter != null && !this.readyEventEmitted && this.ready) { - this.emitter.emit(PoolEvents.ready, this.info) - this.readyEventEmitted = true + /** @inheritDoc */ + public setWorkerChoiceStrategy ( + workerChoiceStrategy: WorkerChoiceStrategy, + workerChoiceStrategyOptions?: WorkerChoiceStrategyOptions + ): void { + let requireSync = false + checkValidWorkerChoiceStrategy(workerChoiceStrategy) + if (workerChoiceStrategyOptions != null) { + requireSync = !this.setWorkerChoiceStrategyOptions( + workerChoiceStrategyOptions + ) + } + if (workerChoiceStrategy !== this.opts.workerChoiceStrategy) { + this.opts.workerChoiceStrategy = workerChoiceStrategy + this.workerChoiceStrategiesContext?.setDefaultWorkerChoiceStrategy( + this.opts.workerChoiceStrategy, + this.opts.workerChoiceStrategyOptions + ) + requireSync = true + } + if (requireSync) { + this.workerChoiceStrategiesContext?.syncWorkerChoiceStrategies( + this.getWorkerChoiceStrategies(), + this.opts.workerChoiceStrategyOptions + ) + for (const workerNodeKey of this.workerNodes.keys()) { + this.sendStatisticsMessageToWorker(workerNodeKey) + } } } - private checkAndEmitTaskDequeuingEvents (): void { - if ( - this.emitter != null && - this.backPressureEventEmitted && - !this.backPressure - ) { - this.emitter.emit(PoolEvents.backPressureEnd, this.info) - this.backPressureEventEmitted = false + /** @inheritDoc */ + public setWorkerChoiceStrategyOptions ( + workerChoiceStrategyOptions: undefined | WorkerChoiceStrategyOptions + ): boolean { + this.checkValidWorkerChoiceStrategyOptions(workerChoiceStrategyOptions) + if (workerChoiceStrategyOptions != null) { + this.opts.workerChoiceStrategyOptions = { + ...this.opts.workerChoiceStrategyOptions, + ...workerChoiceStrategyOptions, + } + this.workerChoiceStrategiesContext?.setOptions( + this.opts.workerChoiceStrategyOptions + ) + this.workerChoiceStrategiesContext?.syncWorkerChoiceStrategies( + this.getWorkerChoiceStrategies(), + this.opts.workerChoiceStrategyOptions + ) + for (const workerNodeKey of this.workerNodes.keys()) { + this.sendStatisticsMessageToWorker(workerNodeKey) + } + return true } + return false } - private checkAndEmitTaskExecutionEvents (): void { - if (this.emitter != null && !this.busyEventEmitted && this.busy) { - this.emitter.emit(PoolEvents.busy, this.info) - this.busyEventEmitted = true + /** @inheritdoc */ + public start (): void { + if (this.started) { + throw new Error('Cannot start an already started pool') } - } - - private checkAndEmitTaskExecutionFinishedEvents (): void { - if (this.emitter != null && this.busyEventEmitted && !this.busy) { - this.emitter.emit(PoolEvents.busyEnd, this.info) - this.busyEventEmitted = false + if (this.starting) { + throw new Error('Cannot start an already starting pool') } - } - - private checkAndEmitTaskQueuingEvents (): void { - if ( - this.emitter != null && - !this.backPressureEventEmitted && - this.backPressure - ) { - this.emitter.emit(PoolEvents.backPressure, this.info) - this.backPressureEventEmitted = true + if (this.destroying) { + throw new Error('Cannot start a destroying pool') } + this.starting = true + this.startMinimumNumberOfWorkers() + this.startTimestamp = performance.now() + this.starting = false + this.started = true } /** - * Checks if the worker id sent in the received message from a worker is valid. + * Hook executed after the worker task execution. + * Can be overridden. + * @param workerNodeKey - The worker node key. * @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 { - if (message.workerId == null) { - throw new Error('Worker message received without worker id') - } else if (this.getWorkerNodeKeyByWorkerId(message.workerId) === -1) { - throw new Error( - `Worker message received from unknown worker '${message.workerId.toString()}'` - ) - } - } - - private checkMinimumNumberOfWorkers ( - minimumNumberOfWorkers: number | undefined + protected afterTaskExecutionHook ( + workerNodeKey: number, + message: MessageValue ): void { - if (minimumNumberOfWorkers == null) { - throw new Error( - 'Cannot instantiate a pool without specifying the number of workers' + let needWorkerChoiceStrategiesUpdate = false + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (this.workerNodes[workerNodeKey]?.usage != null) { + const workerUsage = this.workerNodes[workerNodeKey].usage + updateTaskStatisticsWorkerUsage(workerUsage, message) + updateRunTimeWorkerUsage( + this.workerChoiceStrategiesContext, + workerUsage, + message ) - } else if (!Number.isSafeInteger(minimumNumberOfWorkers)) { - throw new TypeError( - 'Cannot instantiate a pool with a non safe integer number of workers' + updateEluWorkerUsage( + this.workerChoiceStrategiesContext, + workerUsage, + message ) - } else if (minimumNumberOfWorkers < 0) { - throw new RangeError( - 'Cannot instantiate a pool with a negative number of workers' + needWorkerChoiceStrategiesUpdate = true + } + if ( + this.shallUpdateTaskFunctionWorkerUsage(workerNodeKey) && + message.taskPerformance?.name != null && + this.workerNodes[workerNodeKey].getTaskFunctionWorkerUsage( + message.taskPerformance.name + ) != null + ) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const taskFunctionWorkerUsage = this.workerNodes[ + workerNodeKey + ].getTaskFunctionWorkerUsage(message.taskPerformance.name)! + updateTaskStatisticsWorkerUsage(taskFunctionWorkerUsage, message) + updateRunTimeWorkerUsage( + this.workerChoiceStrategiesContext, + taskFunctionWorkerUsage, + message ) - } else if (this.type === PoolTypes.fixed && minimumNumberOfWorkers === 0) { - throw new RangeError('Cannot instantiate a fixed pool with zero worker') + updateEluWorkerUsage( + this.workerChoiceStrategiesContext, + taskFunctionWorkerUsage, + message + ) + needWorkerChoiceStrategiesUpdate = true + } + if (needWorkerChoiceStrategiesUpdate) { + this.workerChoiceStrategiesContext?.update(workerNodeKey) } } - private checkPoolOptions (opts: PoolOptions): void { - if (isPlainObject(opts)) { - this.opts.startWorkers = opts.startWorkers ?? true - checkValidWorkerChoiceStrategy(opts.workerChoiceStrategy) - this.opts.workerChoiceStrategy = - opts.workerChoiceStrategy ?? WorkerChoiceStrategies.ROUND_ROBIN - this.checkValidWorkerChoiceStrategyOptions( - opts.workerChoiceStrategyOptions - ) - if (opts.workerChoiceStrategyOptions != null) { - this.opts.workerChoiceStrategyOptions = opts.workerChoiceStrategyOptions + /** + * Method hooked up after a worker node has been newly created. + * Can be overridden. + * @param workerNodeKey - The newly created worker node key. + */ + protected afterWorkerNodeSetup (workerNodeKey: number): void { + // Listen to worker messages. + this.registerWorkerMessageListener( + workerNodeKey, + this.workerMessageListener + ) + // Send the startup message to worker. + this.sendStartupMessageToWorker(workerNodeKey) + // Send the statistics message to worker. + this.sendStatisticsMessageToWorker(workerNodeKey) + if (this.opts.enableTasksQueue === true) { + if (this.opts.tasksQueueOptions?.taskStealing === true) { + this.workerNodes[workerNodeKey].on( + 'idle', + this.handleWorkerNodeIdleEvent + ) } - this.opts.restartWorkerOnError = opts.restartWorkerOnError ?? true - this.opts.enableEvents = opts.enableEvents ?? true - this.opts.enableTasksQueue = opts.enableTasksQueue ?? false - if (this.opts.enableTasksQueue) { - checkValidTasksQueueOptions(opts.tasksQueueOptions) - this.opts.tasksQueueOptions = this.buildTasksQueueOptions( - opts.tasksQueueOptions + if (this.opts.tasksQueueOptions?.tasksStealingOnBackPressure === true) { + this.workerNodes[workerNodeKey].on( + 'backPressure', + this.handleWorkerNodeBackPressureEvent ) } - } else { - throw new TypeError('Invalid pool options: must be a plain object') } } - private checkPoolType (): void { - if (this.type === PoolTypes.fixed && this.maximumNumberOfWorkers != null) { - throw new Error( - 'Cannot instantiate a fixed pool with a maximum number of workers specified at initialization' + /** + * Hook executed before the worker task execution. + * Can be overridden. + * @param workerNodeKey - The worker node key. + * @param task - The task to execute. + */ + protected beforeTaskExecutionHook ( + workerNodeKey: number, + task: Task + ): void { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (this.workerNodes[workerNodeKey]?.usage != null) { + const workerUsage = this.workerNodes[workerNodeKey].usage + ++workerUsage.tasks.executing + updateWaitTimeWorkerUsage( + this.workerChoiceStrategiesContext, + workerUsage, + task ) } - } - - private checkValidWorkerChoiceStrategyOptions ( - workerChoiceStrategyOptions: undefined | WorkerChoiceStrategyOptions - ): void { if ( - workerChoiceStrategyOptions != null && - !isPlainObject(workerChoiceStrategyOptions) + this.shallUpdateTaskFunctionWorkerUsage(workerNodeKey) && + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.workerNodes[workerNodeKey].getTaskFunctionWorkerUsage(task.name!) != + null ) { - throw new TypeError( - 'Invalid worker choice strategy options: must be a plain object' + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const taskFunctionWorkerUsage = this.workerNodes[ + workerNodeKey + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ].getTaskFunctionWorkerUsage(task.name!)! + ++taskFunctionWorkerUsage.tasks.executing + updateWaitTimeWorkerUsage( + this.workerChoiceStrategiesContext, + taskFunctionWorkerUsage, + task ) } - if ( - workerChoiceStrategyOptions?.weights != null && - Object.keys(workerChoiceStrategyOptions.weights).length !== - (this.maximumNumberOfWorkers ?? this.minimumNumberOfWorkers) - ) { - throw new Error( - 'Invalid worker choice strategy options: must have a weight for each worker node' + } + + /** + * Emits dynamic worker creation events. + */ + protected abstract checkAndEmitDynamicWorkerCreationEvents (): void + + /** + * Emits dynamic worker destruction events. + */ + protected abstract checkAndEmitDynamicWorkerDestructionEvents (): void + + /** + * Creates a new, completely set up dynamic worker node. + * @returns New, completely set up dynamic worker node key. + */ + protected createAndSetupDynamicWorkerNode (): number { + const workerNodeKey = this.createAndSetupWorkerNode() + this.registerWorkerMessageListener(workerNodeKey, message => { + this.checkMessageWorkerId(message) + const localWorkerNodeKey = this.getWorkerNodeKeyByWorkerId( + message.workerId ) + // Kill message received from worker + if ( + isKillBehavior(KillBehaviors.HARD, message.kill) || + (isKillBehavior(KillBehaviors.SOFT, message.kill) && + this.isWorkerNodeIdle(localWorkerNodeKey) && + !this.isWorkerNodeStealing(localWorkerNodeKey)) + ) { + // Flag the worker node as not ready immediately + this.flagWorkerNodeAsNotReady(localWorkerNodeKey) + this.destroyWorkerNode(localWorkerNodeKey).catch((error: unknown) => { + this.emitter?.emit(PoolEvents.error, error) + }) + } + }) + this.sendToWorker(workerNodeKey, { + checkActive: true, + }) + if (this.taskFunctions.size > 0) { + for (const [taskFunctionName, taskFunctionObject] of this.taskFunctions) { + this.sendTaskFunctionOperationToWorker(workerNodeKey, { + taskFunction: taskFunctionObject.taskFunction.toString(), + taskFunctionOperation: 'add', + taskFunctionProperties: buildTaskFunctionProperties( + taskFunctionName, + taskFunctionObject + ), + }).catch((error: unknown) => { + this.emitter?.emit(PoolEvents.error, error) + }) + } } + const workerNode = this.workerNodes[workerNodeKey] + workerNode.info.dynamic = true if ( - workerChoiceStrategyOptions?.measurement != null && - !Object.values(Measurements).includes( - workerChoiceStrategyOptions.measurement - ) + this.workerChoiceStrategiesContext?.getPolicy().dynamicWorkerReady === + true ) { - throw new Error( - `Invalid worker choice strategy options: invalid measurement '${workerChoiceStrategyOptions.measurement}'` - ) + workerNode.info.ready = true } + this.initWorkerNodeUsage(workerNode) + this.checkAndEmitDynamicWorkerCreationEvents() + return workerNodeKey } /** - * Chooses a worker node for the next task. - * @param name - The task function name. - * @returns The chosen worker node key. + * Creates a new, completely set up worker node. + * @returns New, completely set up worker node key. */ - private chooseWorkerNode (name?: string): number { - if (this.shallCreateDynamicWorker()) { - const workerNodeKey = this.createAndSetupDynamicWorkerNode() + protected createAndSetupWorkerNode (): number { + const workerNode = this.createWorkerNode() + workerNode.registerWorkerEventHandler( + 'online', + this.opts.onlineHandler ?? EMPTY_FUNCTION + ) + workerNode.registerWorkerEventHandler( + 'message', + this.opts.messageHandler ?? EMPTY_FUNCTION + ) + workerNode.registerWorkerEventHandler( + 'error', + this.opts.errorHandler ?? EMPTY_FUNCTION + ) + workerNode.registerOnceWorkerEventHandler('error', (error: Error) => { + workerNode.info.ready = false + this.emitter?.emit(PoolEvents.error, error) if ( - this.workerChoiceStrategiesContext?.getPolicy().dynamicWorkerUsage === - true + this.started && + !this.destroying && + this.opts.restartWorkerOnError === true ) { - return workerNodeKey + if (workerNode.info.dynamic) { + this.createAndSetupDynamicWorkerNode() + } else if (!this.startingMinimumNumberOfWorkers) { + this.startMinimumNumberOfWorkers(true) + } } - } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return this.workerChoiceStrategiesContext!.execute( - this.getTaskFunctionWorkerChoiceStrategy(name) - ) - } - - /** - * Creates a worker node. - * @returns The created worker node. - */ - private createWorkerNode (): IWorkerNode { - const workerNode = new WorkerNode( - this.worker, - this.filePath, - { - env: this.opts.env, - tasksQueueBackPressureSize: - this.opts.tasksQueueOptions?.size ?? - getDefaultTasksQueueOptions( - this.maximumNumberOfWorkers ?? this.minimumNumberOfWorkers - ).size, - tasksQueueBucketSize: defaultBucketSize, - tasksQueuePriority: this.getTasksQueuePriority(), - workerOptions: this.opts.workerOptions, + if ( + this.started && + !this.destroying && + this.opts.enableTasksQueue === true + ) { + this.redistributeQueuedTasks(this.workerNodes.indexOf(workerNode)) } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, promise/no-promise-in-callback + workerNode?.terminate().catch((error: unknown) => { + this.emitter?.emit(PoolEvents.error, error) + }) + }) + workerNode.registerWorkerEventHandler( + 'exit', + this.opts.exitHandler ?? EMPTY_FUNCTION ) - // Flag the worker node as ready at pool startup. - if (this.starting) { - workerNode.info.ready = true - } - return workerNode - } - - private dequeueTask (workerNodeKey: number): Task | undefined { - const task = this.workerNodes[workerNodeKey].dequeueTask() - this.checkAndEmitTaskDequeuingEvents() - return task - } - - private enqueueTask (workerNodeKey: number, task: Task): number { - const tasksQueueSize = this.workerNodes[workerNodeKey].enqueueTask(task) - this.checkAndEmitTaskQueuingEvents() - return tasksQueueSize + workerNode.registerOnceWorkerEventHandler('exit', () => { + this.removeWorkerNode(workerNode) + if ( + this.started && + !this.startingMinimumNumberOfWorkers && + !this.destroying + ) { + this.startMinimumNumberOfWorkers(true) + } + }) + const workerNodeKey = this.addWorkerNode(workerNode) + this.afterWorkerNodeSetup(workerNodeKey) + return workerNodeKey } /** - * Executes the given task on the worker given its worker node key. + * Deregisters a listener callback on the worker given its worker node key. * @param workerNodeKey - The worker node key. - * @param task - The task to execute. + * @param listener - The message listener callback. */ - private executeTask (workerNodeKey: number, task: Task): void { - this.beforeTaskExecutionHook(workerNodeKey, task) - this.sendToWorker(workerNodeKey, task, task.transferList) - this.checkAndEmitTaskExecutionEvents() - } - - private flushTasksQueues (): void { - for (const workerNodeKey of this.workerNodes.keys()) { - this.flushTasksQueue(workerNodeKey) - } - } - - private getTasksQueuePriority (): boolean { - return this.listTaskFunctionsProperties().some( - taskFunctionProperties => taskFunctionProperties.priority != null - ) - } + protected abstract deregisterWorkerMessageListener< + Message extends Data | Response + >( + workerNodeKey: number, + listener: (message: MessageValue) => void + ): void /** - * Gets the worker node key given its worker id. - * @param workerId - The worker id. - * @returns The worker node key if the worker id is found in the pool worker nodes, `-1` otherwise. + * Terminates the worker node given its worker node key. + * @param workerNodeKey - The worker node key. */ - private getWorkerNodeKeyByWorkerId (workerId: number | undefined): number { - return this.workerNodes.findIndex( - workerNode => workerNode.info.id === workerId + protected async destroyWorkerNode (workerNodeKey: number): Promise { + this.flagWorkerNodeAsNotReady(workerNodeKey) + const flushedTasks = this.flushTasksQueue(workerNodeKey) + const workerNode = this.workerNodes[workerNodeKey] + await waitWorkerNodeEvents( + workerNode, + 'taskFinished', + flushedTasks, + this.opts.tasksQueueOptions?.tasksFinishedTimeout ?? + getDefaultTasksQueueOptions( + this.maximumNumberOfWorkers ?? this.minimumNumberOfWorkers + ).tasksFinishedTimeout ) + await this.sendKillMessageToWorker(workerNodeKey) + await workerNode.terminate() } - private handleTask (workerNodeKey: number, task: Task): void { - if (this.shallExecuteTask(workerNodeKey)) { - this.executeTask(workerNodeKey, task) - } else { - this.enqueueTask(workerNodeKey, task) - } - } - - private handleTaskExecutionResponse (message: MessageValue): void { - const { data, taskId, workerError } = message - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const promiseResponse = this.promiseResponseMap.get(taskId!) - if (promiseResponse != null) { - const { asyncResource, reject, resolve, workerNodeKey } = promiseResponse - const workerNode = this.workerNodes[workerNodeKey] - if (workerError != null) { - this.emitter?.emit(PoolEvents.taskError, workerError) - const error = this.handleWorkerError(workerError) - asyncResource != null - ? asyncResource.runInAsyncScope(reject, this.emitter, error) - : reject(error) - } else { - asyncResource != null - ? asyncResource.runInAsyncScope(resolve, this.emitter, data) - : resolve(data as Response) - } - asyncResource?.emitDestroy() - this.afterTaskExecutionHook(workerNodeKey, message) - queueMicrotask(() => { - this.checkAndEmitTaskExecutionFinishedEvents() - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - workerNode?.emit('taskFinished', taskId) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.promiseResponseMap.delete(taskId!) - if (this.opts.enableTasksQueue === true && !this.destroying) { - if ( - !this.isWorkerNodeBusy(workerNodeKey) && - this.tasksQueueSize(workerNodeKey) > 0 - ) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.executeTask(workerNodeKey, this.dequeueTask(workerNodeKey)!) - } - if (this.isWorkerNodeIdle(workerNodeKey)) { - workerNode.emit('idle', { - workerNodeKey, - }) - } - } - if (this.shallCreateDynamicWorker()) { - this.createAndSetupDynamicWorkerNode() - } - }) + protected flagWorkerNodeAsNotReady (workerNodeKey: number): void { + const workerInfo = this.getWorkerInfo(workerNodeKey) + if (workerInfo != null) { + workerInfo.ready = false } } - private handleWorkerError (workerError: WorkerError): Error { - if (workerError.error != null) { - return workerError.error + protected flushTasksQueue (workerNodeKey: number): number { + let flushedTasks = 0 + while (this.tasksQueueSize(workerNodeKey) > 0) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.executeTask(workerNodeKey, this.dequeueTask(workerNodeKey)!) + ++flushedTasks } - const error = new Error(workerError.message) - error.stack = workerError.stack - return error + this.workerNodes[workerNodeKey].clearTasksQueue() + return flushedTasks } - private handleWorkerReadyResponse (message: MessageValue): void { - const { ready, taskFunctionsProperties, workerId } = message - if (ready == null || !ready) { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - throw new Error(`Worker ${workerId?.toString()} failed to initialize`) - } - const workerNodeKey = this.getWorkerNodeKeyByWorkerId(workerId) - const workerNode = this.workerNodes[workerNodeKey] - workerNode.info.ready = ready - workerNode.info.taskFunctionsProperties = taskFunctionsProperties - this.sendStatisticsMessageToWorker(workerNodeKey) - this.setTasksQueuePriority(workerNodeKey) - this.checkAndEmitReadyEvent() + /** + * Gets the worker information given its worker node key. + * @param workerNodeKey - The worker node key. + * @returns The worker information. + */ + protected getWorkerInfo (workerNodeKey: number): undefined | WorkerInfo { + return this.workerNodes[workerNodeKey]?.info } - private initEventEmitter (): void { - this.emitter = new EventEmitterAsyncResource({ - name: `poolifier:${this.type}-${this.worker}-pool`, - }) + /** + * Whether the worker nodes are back pressured or not. + * @returns Worker nodes back pressure boolean status. + */ + protected internalBackPressure (): boolean { + return ( + this.workerNodes.reduce( + (accumulator, _, workerNodeKey) => + this.isWorkerNodeBackPressured(workerNodeKey) + ? accumulator + 1 + : accumulator, + 0 + ) === this.workerNodes.length + ) } /** - * Initializes the worker node usage with sensible default values gathered during runtime. - * @param workerNode - The worker node. + * Whether worker nodes are executing concurrently their tasks quota or not. + * @returns Worker nodes busyness boolean status. */ - private initWorkerNodeUsage (workerNode: IWorkerNode): void { - if ( - this.workerChoiceStrategiesContext?.getTaskStatisticsRequirements() - .runTime.aggregate === true - ) { - workerNode.usage.runTime.aggregate = min( - ...this.workerNodes.map( - workerNode => - workerNode.usage.runTime.aggregate ?? Number.POSITIVE_INFINITY - ) - ) - } - if ( - this.workerChoiceStrategiesContext?.getTaskStatisticsRequirements() - .waitTime.aggregate === true - ) { - workerNode.usage.waitTime.aggregate = min( - ...this.workerNodes.map( - workerNode => - workerNode.usage.waitTime.aggregate ?? Number.POSITIVE_INFINITY - ) - ) - } - if ( - this.workerChoiceStrategiesContext?.getTaskStatisticsRequirements().elu - .aggregate === true - ) { - workerNode.usage.elu.active.aggregate = min( - ...this.workerNodes.map( - workerNode => - workerNode.usage.elu.active.aggregate ?? Number.POSITIVE_INFINITY - ) - ) - } + protected internalBusy (): boolean { + return ( + this.workerNodes.reduce( + (accumulator, _, workerNodeKey) => + this.isWorkerNodeBusy(workerNodeKey) ? accumulator + 1 : accumulator, + 0 + ) === this.workerNodes.length + ) } - private async internalExecute ( - data?: Data, - name?: string, + /** + * Returns whether the worker is the main worker or not. + * @returns `true` if the worker is the main worker, `false` otherwise. + */ + protected abstract isMain (): boolean + + /** + * 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 + + /** + * Registers 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 registerWorkerMessageListener< + Message extends Data | Response + >( + workerNodeKey: number, + listener: (message: MessageValue) => void + ): void + + /** + * Sends the startup message to worker given its worker node key. + * @param workerNodeKey - The worker node key. + */ + protected abstract sendStartupMessageToWorker (workerNodeKey: number): void + + /** + * Sends a message to worker given its worker node key. + * @param workerNodeKey - The worker node key. + * @param message - The message. + * @param transferList - The optional array of transferable objects. + */ + protected abstract sendToWorker ( + workerNodeKey: number, + message: MessageValue, transferList?: readonly TransferListItem[] - ): Promise { - return await new Promise((resolve, reject) => { - const timestamp = performance.now() - const workerNodeKey = this.chooseWorkerNode(name) - const task: Task = { - data: data ?? ({} as Data), - name: name ?? DEFAULT_TASK_NAME, - priority: this.getWorkerNodeTaskFunctionPriority(workerNodeKey, name), - strategy: this.getWorkerNodeTaskFunctionWorkerChoiceStrategy( - workerNodeKey, - name - ), - taskId: randomUUID(), - timestamp, - transferList, - } - if ( - this.opts.enableTasksQueue === false || - (this.opts.enableTasksQueue === true && - this.shallExecuteTask(workerNodeKey)) - ) { - this.executeTask(workerNodeKey, task) - } else { - this.enqueueTask(workerNodeKey, task) + ): void + + /** + * Setup hook to execute code before worker nodes are created in the abstract constructor. + * Can be overridden. + */ + protected setupHook (): void { + /* Intentionally empty */ + } + + /** + * Conditions for dynamic worker creation. + * @returns Whether to create a dynamic worker or not. + */ + protected abstract shallCreateDynamicWorker (): boolean + + /** + * This method is the message listener registered on each worker. + * @param message - The message received from the worker. + */ + protected readonly workerMessageListener = ( + message: MessageValue + ): void => { + this.checkMessageWorkerId(message) + const { ready, taskFunctionsProperties, taskId, workerId } = message + if (ready != null && taskFunctionsProperties != null) { + // Worker ready response received from worker + this.handleWorkerReadyResponse(message) + } else if (taskFunctionsProperties != null) { + // Task function properties message received from worker + const workerNodeKey = this.getWorkerNodeKeyByWorkerId(workerId) + const workerInfo = this.getWorkerInfo(workerNodeKey) + if (workerInfo != null) { + workerInfo.taskFunctionsProperties = taskFunctionsProperties + this.sendStatisticsMessageToWorker(workerNodeKey) + this.setTasksQueuePriority(workerNodeKey) } - queueMicrotask(() => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.promiseResponseMap.set(task.taskId!, { - reject, - resolve, - workerNodeKey, - ...(this.emitter != null && { - asyncResource: new AsyncResource('poolifier:task', { - requireManualDestroy: true, - triggerAsyncId: this.emitter.asyncId, - }), - }), - }) - }) - }) + } else if (taskId != null) { + // Task execution response received from worker + this.handleTaskExecutionResponse(message) + } } - private isWorkerNodeBackPressured (workerNodeKey: number): boolean { - const workerNode = this.workerNodes[workerNodeKey] - return workerNode.info.ready && workerNode.info.backPressure + /** + * Adds the given worker node in the pool worker nodes. + * @param workerNode - The worker node. + * @returns The added worker node key. + * @throws {@link https://nodejs.org/api/errors.html#class-error} If the added worker node is not found. + */ + private addWorkerNode (workerNode: IWorkerNode): number { + this.workerNodes.push(workerNode) + const workerNodeKey = this.workerNodes.indexOf(workerNode) + if (workerNodeKey === -1) { + throw new Error('Worker added not found in worker nodes') + } + return workerNodeKey } - private isWorkerNodeBusy (workerNodeKey: number): boolean { - const workerNode = this.workerNodes[workerNodeKey] - if (this.opts.enableTasksQueue === true) { - return ( - workerNode.info.ready && - workerNode.usage.tasks.executing >= - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.opts.tasksQueueOptions!.concurrency! - ) + private buildTasksQueueOptions ( + tasksQueueOptions: TasksQueueOptions | undefined + ): TasksQueueOptions { + return { + ...getDefaultTasksQueueOptions( + this.maximumNumberOfWorkers ?? this.minimumNumberOfWorkers + ), + ...this.opts.tasksQueueOptions, + ...tasksQueueOptions, } - return workerNode.info.ready && workerNode.usage.tasks.executing > 0 } - private isWorkerNodeIdle (workerNodeKey: number): boolean { - const workerNode = this.workerNodes[workerNodeKey] - if (this.opts.enableTasksQueue === true) { - return ( - workerNode.info.ready && - workerNode.usage.tasks.executing === 0 && - this.tasksQueueSize(workerNodeKey) === 0 - ) - } - return workerNode.info.ready && workerNode.usage.tasks.executing === 0 + private cannotStealTask (): boolean { + return this.workerNodes.length <= 1 || this.info.queuedTasks === 0 } - private isWorkerNodeStealing (workerNodeKey: number): boolean { - const workerNode = this.workerNodes[workerNodeKey] - return ( - workerNode.info.ready && - (workerNode.info.continuousStealing || - workerNode.info.backPressureStealing) - ) + private checkAndEmitReadyEvent (): void { + if (this.emitter != null && !this.readyEventEmitted && this.ready) { + this.emitter.emit(PoolEvents.ready, this.info) + this.readyEventEmitted = true + } } - private redistributeQueuedTasks (sourceWorkerNodeKey: number): void { - if (sourceWorkerNodeKey === -1 || this.cannotStealTask()) { - return - } - while (this.tasksQueueSize(sourceWorkerNodeKey) > 0) { - const destinationWorkerNodeKey = this.workerNodes.reduce( - (minWorkerNodeKey, workerNode, workerNodeKey, workerNodes) => { - return sourceWorkerNodeKey !== workerNodeKey && - workerNode.info.ready && - workerNode.usage.tasks.queued < - workerNodes[minWorkerNodeKey].usage.tasks.queued - ? workerNodeKey - : minWorkerNodeKey - }, - 0 - ) - this.handleTask( - destinationWorkerNodeKey, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.dequeueTask(sourceWorkerNodeKey)! - ) + private checkAndEmitTaskDequeuingEvents (): void { + if ( + this.emitter != null && + this.backPressureEventEmitted && + !this.backPressure + ) { + this.emitter.emit(PoolEvents.backPressureEnd, this.info) + this.backPressureEventEmitted = false } } - /** - * Removes the worker node from the pool worker nodes. - * @param workerNode - The worker node. - */ - private removeWorkerNode (workerNode: IWorkerNode): void { - const workerNodeKey = this.workerNodes.indexOf(workerNode) - if (workerNodeKey !== -1) { - this.workerNodes.splice(workerNodeKey, 1) - this.workerChoiceStrategiesContext?.remove(workerNodeKey) - workerNode.info.dynamic && - this.checkAndEmitDynamicWorkerDestructionEvents() + private checkAndEmitTaskExecutionEvents (): void { + if (this.emitter != null && !this.busyEventEmitted && this.busy) { + this.emitter.emit(PoolEvents.busy, this.info) + this.busyEventEmitted = true } } - private resetTaskSequentiallyStolenStatisticsWorkerUsage ( - workerNodeKey: number, - taskName?: string - ): void { - const workerNode = this.workerNodes[workerNodeKey] - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (workerNode?.usage != null) { - workerNode.usage.tasks.sequentiallyStolen = 0 + private checkAndEmitTaskExecutionFinishedEvents (): void { + if (this.emitter != null && this.busyEventEmitted && !this.busy) { + this.emitter.emit(PoolEvents.busyEnd, this.info) + this.busyEventEmitted = false } + } + + private checkAndEmitTaskQueuingEvents (): void { if ( - taskName != null && - this.shallUpdateTaskFunctionWorkerUsage(workerNodeKey) && - workerNode.getTaskFunctionWorkerUsage(taskName) != null + this.emitter != null && + !this.backPressureEventEmitted && + this.backPressure ) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - workerNode.getTaskFunctionWorkerUsage( - taskName - )!.tasks.sequentiallyStolen = 0 + this.emitter.emit(PoolEvents.backPressure, this.info) + this.backPressureEventEmitted = true } } - private async sendKillMessageToWorker (workerNodeKey: number): Promise { - await new Promise((resolve, reject) => { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (this.workerNodes[workerNodeKey] == null) { - resolve() - return - } - const killMessageListener = (message: MessageValue): void => { - this.checkMessageWorkerId(message) - if (message.kill === 'success') { - resolve() - } else if (message.kill === 'failure') { - reject( - new Error( - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `Kill message handling failed on worker ${message.workerId?.toString()}` - ) - ) - } - } - // FIXME: should be registered only once - this.registerWorkerMessageListener(workerNodeKey, killMessageListener) - this.sendToWorker(workerNodeKey, { kill: true }) - }) - } - /** - * Sends the statistics message to worker given its worker node key. - * @param workerNodeKey - The worker node key. + * Checks if the worker id sent in the received message from a worker is valid. + * @param message - The received message. + * @throws {@link https://nodejs.org/api/errors.html#class-error} If the worker id is invalid. */ - private sendStatisticsMessageToWorker (workerNodeKey: number): void { - this.sendToWorker(workerNodeKey, { - statistics: { - elu: - this.workerChoiceStrategiesContext?.getTaskStatisticsRequirements() - .elu.aggregate ?? false, - runTime: - this.workerChoiceStrategiesContext?.getTaskStatisticsRequirements() - .runTime.aggregate ?? false, - }, - }) + private checkMessageWorkerId (message: MessageValue): void { + if (message.workerId == null) { + throw new Error('Worker message received without worker id') + } else if (this.getWorkerNodeKeyByWorkerId(message.workerId) === -1) { + throw new Error( + `Worker message received from unknown worker '${message.workerId.toString()}'` + ) + } } - 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 - if ( - message.taskFunctionOperationStatus != null && - message.workerId === workerId - ) { - if (message.taskFunctionOperationStatus) { - resolve(true) - } else { - reject( - new Error( - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `Task function operation '${message.taskFunctionOperation?.toString()}' failed on worker ${message.workerId?.toString()} with error: '${ - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - message.workerError?.message - }'` - ) - ) - } - this.deregisterWorkerMessageListener( - this.getWorkerNodeKeyByWorkerId(message.workerId), - taskFunctionOperationListener - ) - } - } - this.registerWorkerMessageListener( - workerNodeKey, - taskFunctionOperationListener + private checkMinimumNumberOfWorkers ( + minimumNumberOfWorkers: number | undefined + ): void { + if (minimumNumberOfWorkers == null) { + throw new Error( + 'Cannot instantiate a pool without specifying the number of workers' ) - this.sendToWorker(workerNodeKey, message) - }) + } else if (!Number.isSafeInteger(minimumNumberOfWorkers)) { + throw new TypeError( + 'Cannot instantiate a pool with a non safe integer number of workers' + ) + } else if (minimumNumberOfWorkers < 0) { + throw new RangeError( + 'Cannot instantiate a pool with a negative number of workers' + ) + } else if (this.type === PoolTypes.fixed && minimumNumberOfWorkers === 0) { + throw new RangeError('Cannot instantiate a fixed pool with zero worker') + } } - 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 - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - }' failed on worker ${errorResponse?.workerId?.toString()} with error: '${ - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - errorResponse?.workerError?.message - }'` - ) - ) - } - this.deregisterWorkerMessageListener( - this.getWorkerNodeKeyByWorkerId(message.workerId), - taskFunctionOperationsListener - ) - } - } + private checkPoolOptions (opts: PoolOptions): void { + if (isPlainObject(opts)) { + this.opts.startWorkers = opts.startWorkers ?? true + checkValidWorkerChoiceStrategy(opts.workerChoiceStrategy) + this.opts.workerChoiceStrategy = + opts.workerChoiceStrategy ?? WorkerChoiceStrategies.ROUND_ROBIN + this.checkValidWorkerChoiceStrategyOptions( + opts.workerChoiceStrategyOptions + ) + if (opts.workerChoiceStrategyOptions != null) { + this.opts.workerChoiceStrategyOptions = opts.workerChoiceStrategyOptions } - for (const workerNodeKey of this.workerNodes.keys()) { - this.registerWorkerMessageListener( - workerNodeKey, - taskFunctionOperationsListener + this.opts.restartWorkerOnError = opts.restartWorkerOnError ?? true + this.opts.enableEvents = opts.enableEvents ?? true + this.opts.enableTasksQueue = opts.enableTasksQueue ?? false + if (this.opts.enableTasksQueue) { + checkValidTasksQueueOptions(opts.tasksQueueOptions) + this.opts.tasksQueueOptions = this.buildTasksQueueOptions( + opts.tasksQueueOptions ) - this.sendToWorker(workerNodeKey, message) } - }) - } - - private setTasksQueuePriority (workerNodeKey: number): void { - this.workerNodes[workerNodeKey].setTasksQueuePriority( - this.getTasksQueuePriority() - ) - } - - private setTasksQueueSize (size: number): void { - for (const workerNode of this.workerNodes) { - workerNode.tasksQueueBackPressureSize = size + } else { + throw new TypeError('Invalid pool options: must be a plain object') } } - private setTasksStealingOnBackPressure (): void { - for (const workerNodeKey of this.workerNodes.keys()) { - this.workerNodes[workerNodeKey].on( - 'backPressure', - this.handleWorkerNodeBackPressureEvent + private checkPoolType (): void { + if (this.type === PoolTypes.fixed && this.maximumNumberOfWorkers != null) { + throw new Error( + 'Cannot instantiate a fixed pool with a maximum number of workers specified at initialization' ) } } - private setTaskStealing (): void { - for (const workerNodeKey of this.workerNodes.keys()) { - this.workerNodes[workerNodeKey].on('idle', this.handleWorkerNodeIdleEvent) + private checkValidWorkerChoiceStrategyOptions ( + workerChoiceStrategyOptions: undefined | WorkerChoiceStrategyOptions + ): void { + if ( + workerChoiceStrategyOptions != null && + !isPlainObject(workerChoiceStrategyOptions) + ) { + throw new TypeError( + 'Invalid worker choice strategy options: must be a plain object' + ) + } + if ( + workerChoiceStrategyOptions?.weights != null && + Object.keys(workerChoiceStrategyOptions.weights).length !== + (this.maximumNumberOfWorkers ?? this.minimumNumberOfWorkers) + ) { + throw new Error( + '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 shallExecuteTask (workerNodeKey: number): boolean { - return ( - this.tasksQueueSize(workerNodeKey) === 0 && - this.workerNodes[workerNodeKey].usage.tasks.executing < - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.opts.tasksQueueOptions!.concurrency! - ) } /** - * Whether the worker node shall update its task function worker usage or not. - * @param workerNodeKey - The worker node key. - * @returns `true` if the worker node shall update its task function worker usage, `false` otherwise. + * Chooses a worker node for the next task. + * @param name - The task function name. + * @returns The chosen worker node key. */ - private shallUpdateTaskFunctionWorkerUsage (workerNodeKey: number): boolean { - const workerInfo = this.getWorkerInfo(workerNodeKey) - return ( - workerInfo != null && - Array.isArray(workerInfo.taskFunctionsProperties) && - workerInfo.taskFunctionsProperties.length > 2 + private chooseWorkerNode (name?: string): number { + if (this.shallCreateDynamicWorker()) { + const workerNodeKey = this.createAndSetupDynamicWorkerNode() + if ( + this.workerChoiceStrategiesContext?.getPolicy().dynamicWorkerUsage === + true + ) { + return workerNodeKey + } + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return this.workerChoiceStrategiesContext!.execute( + this.getTaskFunctionWorkerChoiceStrategy(name) ) } /** - * Starts the minimum number of workers. - * @param initWorkerNodeUsage - Whether to initialize the worker node usage or not. @defaultValue false + * Creates a worker node. + * @returns The created worker node. */ - private startMinimumNumberOfWorkers (initWorkerNodeUsage = false): void { - if (this.minimumNumberOfWorkers === 0) { - return - } - this.startingMinimumNumberOfWorkers = true - while ( - this.workerNodes.reduce( - (accumulator, workerNode) => - !workerNode.info.dynamic ? accumulator + 1 : accumulator, - 0 - ) < this.minimumNumberOfWorkers - ) { - const workerNodeKey = this.createAndSetupWorkerNode() - initWorkerNodeUsage && - this.initWorkerNodeUsage(this.workerNodes[workerNodeKey]) + private createWorkerNode (): IWorkerNode { + const workerNode = new WorkerNode( + this.worker, + this.filePath, + { + env: this.opts.env, + tasksQueueBackPressureSize: + this.opts.tasksQueueOptions?.size ?? + getDefaultTasksQueueOptions( + this.maximumNumberOfWorkers ?? this.minimumNumberOfWorkers + ).size, + tasksQueueBucketSize: defaultBucketSize, + tasksQueuePriority: this.getTasksQueuePriority(), + workerOptions: this.opts.workerOptions, + } + ) + // Flag the worker node as ready at pool startup. + if (this.starting) { + workerNode.info.ready = true } - this.startingMinimumNumberOfWorkers = false + return workerNode } - private tasksQueueSize (workerNodeKey: number): number { - return this.workerNodes[workerNodeKey].tasksQueueSize() + private dequeueTask (workerNodeKey: number): Task | undefined { + const task = this.workerNodes[workerNodeKey].dequeueTask() + this.checkAndEmitTaskDequeuingEvents() + return task } - private unsetTasksStealingOnBackPressure (): void { - for (const workerNodeKey of this.workerNodes.keys()) { - this.workerNodes[workerNodeKey].off( - 'backPressure', - this.handleWorkerNodeBackPressureEvent - ) - } + private enqueueTask (workerNodeKey: number, task: Task): number { + const tasksQueueSize = this.workerNodes[workerNodeKey].enqueueTask(task) + this.checkAndEmitTaskQueuingEvents() + return tasksQueueSize } - private unsetTaskStealing (): void { + /** + * Executes the given task on the worker given its worker node key. + * @param workerNodeKey - The worker node key. + * @param task - The task to execute. + */ + private executeTask (workerNodeKey: number, task: Task): void { + this.beforeTaskExecutionHook(workerNodeKey, task) + this.sendToWorker(workerNodeKey, task, task.transferList) + this.checkAndEmitTaskExecutionEvents() + } + + private flushTasksQueues (): void { for (const workerNodeKey of this.workerNodes.keys()) { - this.workerNodes[workerNodeKey].off( - 'idle', - this.handleWorkerNodeIdleEvent - ) + this.flushTasksQueue(workerNodeKey) } } - private updateTaskSequentiallyStolenStatisticsWorkerUsage ( - workerNodeKey: number, - taskName?: string, - previousTaskName?: string - ): void { - const workerNode = this.workerNodes[workerNodeKey] - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (workerNode?.usage != null && taskName != null) { - ++workerNode.usage.tasks.sequentiallyStolen + /** + * Gets task function worker choice strategy, if any. + * @param name - The task function name. + * @returns The task function worker choice strategy if the task function worker choice strategy is defined, `undefined` otherwise. + */ + private readonly getTaskFunctionWorkerChoiceStrategy = ( + name?: string + ): undefined | WorkerChoiceStrategy => { + name = name ?? DEFAULT_TASK_NAME + const taskFunctionsProperties = this.listTaskFunctionsProperties() + if (name === DEFAULT_TASK_NAME) { + name = taskFunctionsProperties[1]?.name } - if ( - taskName != null && - this.shallUpdateTaskFunctionWorkerUsage(workerNodeKey) && - workerNode.getTaskFunctionWorkerUsage(taskName) != null - ) { - const taskFunctionWorkerUsage = + return taskFunctionsProperties.find( + (taskFunctionProperties: TaskFunctionProperties) => + taskFunctionProperties.name === name + )?.strategy + } + + private getTasksQueuePriority (): boolean { + return this.listTaskFunctionsProperties().some( + taskFunctionProperties => taskFunctionProperties.priority != null + ) + } + + /** + * Gets the worker choice strategies registered in this pool. + * @returns The worker choice strategies. + */ + private readonly getWorkerChoiceStrategies = + (): Set => { + return new Set([ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - workerNode.getTaskFunctionWorkerUsage(taskName)! - if ( - taskFunctionWorkerUsage.tasks.sequentiallyStolen === 0 || - (previousTaskName != null && - previousTaskName === taskName && - taskFunctionWorkerUsage.tasks.sequentiallyStolen > 0) - ) { - ++taskFunctionWorkerUsage.tasks.sequentiallyStolen - } else if (taskFunctionWorkerUsage.tasks.sequentiallyStolen > 0) { - taskFunctionWorkerUsage.tasks.sequentiallyStolen = 0 - } + this.opts.workerChoiceStrategy!, + ...this.listTaskFunctionsProperties() + .map( + (taskFunctionProperties: TaskFunctionProperties) => + taskFunctionProperties.strategy + ) + .filter( + (strategy: undefined | WorkerChoiceStrategy) => strategy != null + ), + ]) } + + /** + * Gets the worker node key given its worker id. + * @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 | undefined): number { + return this.workerNodes.findIndex( + workerNode => workerNode.info.id === workerId + ) } - private updateTaskStolenStatisticsWorkerUsage ( + /** + * Gets worker node task function priority, if any. + * @param workerNodeKey - The worker node key. + * @param name - The task function name. + * @returns The worker node task function priority if the worker node task function priority is defined, `undefined` otherwise. + */ + private readonly getWorkerNodeTaskFunctionPriority = ( workerNodeKey: number, - taskName: string - ): void { - const workerNode = this.workerNodes[workerNodeKey] - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (workerNode?.usage != null) { - ++workerNode.usage.tasks.stolen + name?: string + ): number | undefined => { + const workerInfo = this.getWorkerInfo(workerNodeKey) + if (workerInfo == null) { + return } - if ( - this.shallUpdateTaskFunctionWorkerUsage(workerNodeKey) && - workerNode.getTaskFunctionWorkerUsage(taskName) != null - ) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - ++workerNode.getTaskFunctionWorkerUsage(taskName)!.tasks.stolen + name = name ?? DEFAULT_TASK_NAME + if (name === DEFAULT_TASK_NAME) { + name = workerInfo.taskFunctionsProperties?.[1]?.name } + return workerInfo.taskFunctionsProperties?.find( + (taskFunctionProperties: TaskFunctionProperties) => + taskFunctionProperties.name === name + )?.priority } - /** @inheritDoc */ - public async addTaskFunction ( - name: string, - fn: TaskFunction | TaskFunctionObject - ): 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') { - fn = { taskFunction: fn } satisfies TaskFunctionObject - } - if (typeof fn.taskFunction !== 'function') { - throw new TypeError('taskFunction property must be a function') + /** + * Gets worker node task function worker choice strategy, if any. + * @param workerNodeKey - The worker node key. + * @param name - The task function name. + * @returns The worker node task function worker choice strategy if the worker node task function worker choice strategy is defined, `undefined` otherwise. + */ + private readonly getWorkerNodeTaskFunctionWorkerChoiceStrategy = ( + workerNodeKey: number, + name?: string + ): undefined | WorkerChoiceStrategy => { + const workerInfo = this.getWorkerInfo(workerNodeKey) + if (workerInfo == null) { + return } - checkValidPriority(fn.priority) - checkValidWorkerChoiceStrategy(fn.strategy) - const opResult = await this.sendTaskFunctionOperationToWorkers({ - taskFunction: fn.taskFunction.toString(), - taskFunctionOperation: 'add', - taskFunctionProperties: buildTaskFunctionProperties(name, fn), - }) - this.taskFunctions.set(name, fn) - this.workerChoiceStrategiesContext?.syncWorkerChoiceStrategies( - this.getWorkerChoiceStrategies() - ) - for (const workerNodeKey of this.workerNodes.keys()) { - this.sendStatisticsMessageToWorker(workerNodeKey) + name = name ?? DEFAULT_TASK_NAME + if (name === DEFAULT_TASK_NAME) { + name = workerInfo.taskFunctionsProperties?.[1]?.name } - return opResult + return workerInfo.taskFunctionsProperties?.find( + (taskFunctionProperties: TaskFunctionProperties) => + taskFunctionProperties.name === name + )?.strategy } - /** @inheritDoc */ - public async destroy (): Promise { - if (!this.started) { - throw new Error('Cannot destroy an already destroyed pool') - } - if (this.starting) { - throw new Error('Cannot destroy an starting pool') - } - if (this.destroying) { - throw new Error('Cannot destroy an already destroying pool') + private handleTask (workerNodeKey: number, task: Task): void { + if (this.shallExecuteTask(workerNodeKey)) { + this.executeTask(workerNodeKey, task) + } else { + this.enqueueTask(workerNodeKey, task) } - this.destroying = true - await Promise.all( - this.workerNodes.map(async (_, workerNodeKey) => { - await this.destroyWorkerNode(workerNodeKey) + } + + private handleTaskExecutionResponse (message: MessageValue): void { + const { data, taskId, workerError } = message + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const promiseResponse = this.promiseResponseMap.get(taskId!) + if (promiseResponse != null) { + const { asyncResource, reject, resolve, workerNodeKey } = promiseResponse + const workerNode = this.workerNodes[workerNodeKey] + if (workerError != null) { + this.emitter?.emit(PoolEvents.taskError, workerError) + const error = this.handleWorkerError(workerError) + asyncResource != null + ? asyncResource.runInAsyncScope(reject, this.emitter, error) + : reject(error) + } else { + asyncResource != null + ? asyncResource.runInAsyncScope(resolve, this.emitter, data) + : resolve(data as Response) + } + asyncResource?.emitDestroy() + this.afterTaskExecutionHook(workerNodeKey, message) + queueMicrotask(() => { + this.checkAndEmitTaskExecutionFinishedEvents() + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + workerNode?.emit('taskFinished', taskId) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.promiseResponseMap.delete(taskId!) + if (this.opts.enableTasksQueue === true && !this.destroying) { + if ( + !this.isWorkerNodeBusy(workerNodeKey) && + this.tasksQueueSize(workerNodeKey) > 0 + ) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.executeTask(workerNodeKey, this.dequeueTask(workerNodeKey)!) + } + if (this.isWorkerNodeIdle(workerNodeKey)) { + workerNode.emit('idle', { + workerNodeKey, + }) + } + } + if (this.shallCreateDynamicWorker()) { + this.createAndSetupDynamicWorkerNode() + } }) - ) - if (this.emitter != null) { - this.emitter.emit(PoolEvents.destroy, this.info) - this.emitter.emitDestroy() - this.readyEventEmitted = false } - delete this.startTimestamp - this.destroying = false - this.started = false } - /** @inheritDoc */ - public enableTasksQueue ( - enable: boolean, - tasksQueueOptions?: TasksQueueOptions - ): void { - if (this.opts.enableTasksQueue === true && !enable) { - this.unsetTaskStealing() - this.unsetTasksStealingOnBackPressure() - this.flushTasksQueues() + private handleWorkerError (workerError: WorkerError): Error { + if (workerError.error != null) { + return workerError.error } - this.opts.enableTasksQueue = enable - this.setTasksQueueOptions(tasksQueueOptions) + const error = new Error(workerError.message) + error.stack = workerError.stack + return error } - /** @inheritDoc */ - public async execute ( - data?: Data, - name?: string, - transferList?: readonly TransferListItem[] - ): Promise { - if (!this.started) { - throw new Error('Cannot execute a task on not started pool') + private readonly handleWorkerNodeBackPressureEvent = ( + eventDetail: WorkerNodeEventDetail + ): void => { + if ( + this.cannotStealTask() || + this.backPressure || + this.isStealingRatioReached() + ) { + return } - if (this.destroying) { - throw new Error('Cannot execute a task on destroying pool') + const sizeOffset = 1 + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + if (this.opts.tasksQueueOptions!.size! <= sizeOffset) { + return } - if (name != null && typeof name !== 'string') { - throw new TypeError('name argument must be a string') + const { workerId } = eventDetail + const sourceWorkerNode = + this.workerNodes[this.getWorkerNodeKeyByWorkerId(workerId)] + const workerNodes = this.workerNodes + .slice() + .sort( + (workerNodeA, workerNodeB) => + workerNodeA.usage.tasks.queued - workerNodeB.usage.tasks.queued + ) + for (const [workerNodeKey, workerNode] of workerNodes.entries()) { + if (sourceWorkerNode.usage.tasks.queued === 0) { + break + } + if ( + workerNode.info.id !== workerId && + !workerNode.info.backPressureStealing && + workerNode.usage.tasks.queued < + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.opts.tasksQueueOptions!.size! - sizeOffset + ) { + workerNode.info.backPressureStealing = true + this.stealTask(sourceWorkerNode, workerNodeKey) + workerNode.info.backPressureStealing = false + } } - if (name != null && typeof name === 'string' && name.trim().length === 0) { - throw new TypeError('name argument must not be an empty string') + } + + private readonly handleWorkerNodeIdleEvent = ( + eventDetail: WorkerNodeEventDetail, + previousStolenTask?: Task + ): void => { + const { workerNodeKey } = eventDetail + if (workerNodeKey == null) { + throw new Error( + "WorkerNode event detail 'workerNodeKey' property must be defined" + ) } - if (transferList != null && !Array.isArray(transferList)) { - throw new TypeError('transferList argument must be an array') + const workerNode = this.workerNodes[workerNodeKey] + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (workerNode == null) { + return + } + if ( + !workerNode.info.continuousStealing && + (this.cannotStealTask() || this.isStealingRatioReached()) + ) { + return + } + const workerNodeTasksUsage = workerNode.usage.tasks + if ( + workerNode.info.continuousStealing && + !this.isWorkerNodeIdle(workerNodeKey) + ) { + workerNode.info.continuousStealing = false + if (workerNodeTasksUsage.sequentiallyStolen > 0) { + this.resetTaskSequentiallyStolenStatisticsWorkerUsage( + workerNodeKey, + previousStolenTask?.name + ) + } + return } - return await this.internalExecute(data, name, transferList) - } - - /** @inheritDoc */ - public hasTaskFunction (name: string): boolean { - return this.listTaskFunctionsProperties().some( - taskFunctionProperties => taskFunctionProperties.name === name + workerNode.info.continuousStealing = true + const stolenTask = this.workerNodeStealTask(workerNodeKey) + this.updateTaskSequentiallyStolenStatisticsWorkerUsage( + workerNodeKey, + stolenTask?.name, + previousStolenTask?.name ) + sleep(exponentialDelay(workerNodeTasksUsage.sequentiallyStolen)) + .then(() => { + this.handleWorkerNodeIdleEvent(eventDetail, stolenTask) + return undefined + }) + .catch((error: unknown) => { + this.emitter?.emit(PoolEvents.error, error) + }) } - /** @inheritDoc */ - public listTaskFunctionsProperties (): TaskFunctionProperties[] { - for (const workerNode of this.workerNodes) { - if ( - Array.isArray(workerNode.info.taskFunctionsProperties) && - workerNode.info.taskFunctionsProperties.length > 0 - ) { - return workerNode.info.taskFunctionsProperties - } + private handleWorkerReadyResponse (message: MessageValue): void { + const { ready, taskFunctionsProperties, workerId } = message + if (ready == null || !ready) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new Error(`Worker ${workerId?.toString()} failed to initialize`) } - return [] + const workerNodeKey = this.getWorkerNodeKeyByWorkerId(workerId) + const workerNode = this.workerNodes[workerNodeKey] + workerNode.info.ready = ready + workerNode.info.taskFunctionsProperties = taskFunctionsProperties + this.sendStatisticsMessageToWorker(workerNodeKey) + this.setTasksQueuePriority(workerNodeKey) + this.checkAndEmitReadyEvent() } - /** @inheritDoc */ - public async mapExecute ( - data: Iterable, - name?: string, - transferList?: readonly TransferListItem[] - ): Promise { - if (!this.started) { - throw new Error('Cannot execute task(s) on not started pool') - } - if (this.destroying) { - throw new Error('Cannot execute task(s) on destroying pool') - } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (data == null) { - throw new TypeError('data argument must be a defined iterable') - } - if (typeof data[Symbol.iterator] !== 'function') { - throw new TypeError('data argument must be an iterable') - } - if (name != null && typeof name !== 'string') { - throw new TypeError('name argument must be a string') - } - if (name != null && typeof name === 'string' && name.trim().length === 0) { - throw new TypeError('name argument must not be an empty string') - } - if (transferList != null && !Array.isArray(transferList)) { - throw new TypeError('transferList argument must be an array') - } - if (!Array.isArray(data)) { - data = [...data] - } - return await Promise.all( - (data as Data[]).map(data => - this.internalExecute(data, name, transferList) - ) - ) + private initEventEmitter (): void { + this.emitter = new EventEmitterAsyncResource({ + name: `poolifier:${this.type}-${this.worker}-pool`, + }) } - /** @inheritDoc */ - 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' + /** + * Initializes the worker node usage with sensible default values gathered during runtime. + * @param workerNode - The worker node. + */ + private initWorkerNodeUsage (workerNode: IWorkerNode): void { + if ( + this.workerChoiceStrategiesContext?.getTaskStatisticsRequirements() + .runTime.aggregate === true + ) { + workerNode.usage.runTime.aggregate = min( + ...this.workerNodes.map( + workerNode => + workerNode.usage.runTime.aggregate ?? Number.POSITIVE_INFINITY + ) ) } - const opResult = await this.sendTaskFunctionOperationToWorkers({ - taskFunctionOperation: 'remove', - taskFunctionProperties: buildTaskFunctionProperties( - name, - this.taskFunctions.get(name) - ), - }) - for (const workerNode of this.workerNodes) { - workerNode.deleteTaskFunctionWorkerUsage(name) + if ( + this.workerChoiceStrategiesContext?.getTaskStatisticsRequirements() + .waitTime.aggregate === true + ) { + workerNode.usage.waitTime.aggregate = min( + ...this.workerNodes.map( + workerNode => + workerNode.usage.waitTime.aggregate ?? Number.POSITIVE_INFINITY + ) + ) } - this.taskFunctions.delete(name) - this.workerChoiceStrategiesContext?.syncWorkerChoiceStrategies( - this.getWorkerChoiceStrategies() - ) - for (const workerNodeKey of this.workerNodes.keys()) { - this.sendStatisticsMessageToWorker(workerNodeKey) + if ( + this.workerChoiceStrategiesContext?.getTaskStatisticsRequirements().elu + .aggregate === true + ) { + workerNode.usage.elu.active.aggregate = min( + ...this.workerNodes.map( + workerNode => + workerNode.usage.elu.active.aggregate ?? Number.POSITIVE_INFINITY + ) + ) } - return opResult - } - - /** @inheritDoc */ - public async setDefaultTaskFunction (name: string): Promise { - return await this.sendTaskFunctionOperationToWorkers({ - taskFunctionOperation: 'default', - taskFunctionProperties: buildTaskFunctionProperties( - name, - this.taskFunctions.get(name) - ), - }) } - /** @inheritDoc */ - public setTasksQueueOptions ( - tasksQueueOptions: TasksQueueOptions | undefined - ): void { - if (this.opts.enableTasksQueue === true) { - checkValidTasksQueueOptions(tasksQueueOptions) - this.opts.tasksQueueOptions = - this.buildTasksQueueOptions(tasksQueueOptions) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.setTasksQueueSize(this.opts.tasksQueueOptions.size!) - if (this.opts.tasksQueueOptions.taskStealing === true) { - this.unsetTaskStealing() - this.setTaskStealing() - } else { - this.unsetTaskStealing() + private async internalExecute ( + data?: Data, + name?: string, + transferList?: readonly TransferListItem[] + ): Promise { + return await new Promise((resolve, reject) => { + const timestamp = performance.now() + const workerNodeKey = this.chooseWorkerNode(name) + const task: Task = { + data: data ?? ({} as Data), + name: name ?? DEFAULT_TASK_NAME, + priority: this.getWorkerNodeTaskFunctionPriority(workerNodeKey, name), + strategy: this.getWorkerNodeTaskFunctionWorkerChoiceStrategy( + workerNodeKey, + name + ), + taskId: randomUUID(), + timestamp, + transferList, } - if (this.opts.tasksQueueOptions.tasksStealingOnBackPressure === true) { - this.unsetTasksStealingOnBackPressure() - this.setTasksStealingOnBackPressure() + if ( + this.opts.enableTasksQueue === false || + (this.opts.enableTasksQueue === true && + this.shallExecuteTask(workerNodeKey)) + ) { + this.executeTask(workerNodeKey, task) } else { - this.unsetTasksStealingOnBackPressure() + this.enqueueTask(workerNodeKey, task) } - } else if (this.opts.tasksQueueOptions != null) { - delete this.opts.tasksQueueOptions - } + queueMicrotask(() => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.promiseResponseMap.set(task.taskId!, { + reject, + resolve, + workerNodeKey, + ...(this.emitter != null && { + asyncResource: new AsyncResource('poolifier:task', { + requireManualDestroy: true, + triggerAsyncId: this.emitter.asyncId, + }), + }), + }) + }) + }) } - /** @inheritDoc */ - public setWorkerChoiceStrategy ( - workerChoiceStrategy: WorkerChoiceStrategy, - workerChoiceStrategyOptions?: WorkerChoiceStrategyOptions - ): void { - let requireSync = false - checkValidWorkerChoiceStrategy(workerChoiceStrategy) - if (workerChoiceStrategyOptions != null) { - requireSync = !this.setWorkerChoiceStrategyOptions( - workerChoiceStrategyOptions - ) - } - if (workerChoiceStrategy !== this.opts.workerChoiceStrategy) { - this.opts.workerChoiceStrategy = workerChoiceStrategy - this.workerChoiceStrategiesContext?.setDefaultWorkerChoiceStrategy( - this.opts.workerChoiceStrategy, - this.opts.workerChoiceStrategyOptions - ) - requireSync = true - } - if (requireSync) { - this.workerChoiceStrategiesContext?.syncWorkerChoiceStrategies( - this.getWorkerChoiceStrategies(), - this.opts.workerChoiceStrategyOptions + private readonly isStealingRatioReached = (): boolean => { + return ( + this.opts.tasksQueueOptions?.tasksStealingRatio === 0 || + (this.info.stealingWorkerNodes ?? 0) > + Math.ceil( + this.workerNodes.length * + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.opts.tasksQueueOptions!.tasksStealingRatio! + ) + ) + } + + private isWorkerNodeBackPressured (workerNodeKey: number): boolean { + const workerNode = this.workerNodes[workerNodeKey] + return workerNode.info.ready && workerNode.info.backPressure + } + + private isWorkerNodeBusy (workerNodeKey: number): boolean { + const workerNode = this.workerNodes[workerNodeKey] + if (this.opts.enableTasksQueue === true) { + return ( + workerNode.info.ready && + workerNode.usage.tasks.executing >= + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.opts.tasksQueueOptions!.concurrency! ) - for (const workerNodeKey of this.workerNodes.keys()) { - this.sendStatisticsMessageToWorker(workerNodeKey) - } } + return workerNode.info.ready && workerNode.usage.tasks.executing > 0 } - /** @inheritDoc */ - public setWorkerChoiceStrategyOptions ( - workerChoiceStrategyOptions: undefined | WorkerChoiceStrategyOptions - ): boolean { - this.checkValidWorkerChoiceStrategyOptions(workerChoiceStrategyOptions) - if (workerChoiceStrategyOptions != null) { - this.opts.workerChoiceStrategyOptions = { - ...this.opts.workerChoiceStrategyOptions, - ...workerChoiceStrategyOptions, - } - this.workerChoiceStrategiesContext?.setOptions( - this.opts.workerChoiceStrategyOptions - ) - this.workerChoiceStrategiesContext?.syncWorkerChoiceStrategies( - this.getWorkerChoiceStrategies(), - this.opts.workerChoiceStrategyOptions + private isWorkerNodeIdle (workerNodeKey: number): boolean { + const workerNode = this.workerNodes[workerNodeKey] + if (this.opts.enableTasksQueue === true) { + return ( + workerNode.info.ready && + workerNode.usage.tasks.executing === 0 && + this.tasksQueueSize(workerNodeKey) === 0 ) - for (const workerNodeKey of this.workerNodes.keys()) { - this.sendStatisticsMessageToWorker(workerNodeKey) - } - return true } - return false + return workerNode.info.ready && workerNode.usage.tasks.executing === 0 } - /** @inheritdoc */ - public start (): void { - if (this.started) { - throw new Error('Cannot start an already started pool') - } - if (this.starting) { - throw new Error('Cannot start an already starting pool') + private isWorkerNodeStealing (workerNodeKey: number): boolean { + const workerNode = this.workerNodes[workerNodeKey] + return ( + workerNode.info.ready && + (workerNode.info.continuousStealing || + workerNode.info.backPressureStealing) + ) + } + + private redistributeQueuedTasks (sourceWorkerNodeKey: number): void { + if (sourceWorkerNodeKey === -1 || this.cannotStealTask()) { + return } - if (this.destroying) { - throw new Error('Cannot start a destroying pool') + while (this.tasksQueueSize(sourceWorkerNodeKey) > 0) { + const destinationWorkerNodeKey = this.workerNodes.reduce( + (minWorkerNodeKey, workerNode, workerNodeKey, workerNodes) => { + return sourceWorkerNodeKey !== workerNodeKey && + workerNode.info.ready && + workerNode.usage.tasks.queued < + workerNodes[minWorkerNodeKey].usage.tasks.queued + ? workerNodeKey + : minWorkerNodeKey + }, + 0 + ) + this.handleTask( + destinationWorkerNodeKey, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.dequeueTask(sourceWorkerNodeKey)! + ) } - this.starting = true - this.startMinimumNumberOfWorkers() - this.startTimestamp = performance.now() - this.starting = false - this.started = true } /** - * Whether the pool is back pressured or not. - * @returns The pool back pressure boolean status. - */ - protected abstract get backPressure (): boolean - - /** - * Whether the pool is busy or not. - * @returns The pool busyness boolean status. + * Removes the worker node from the pool worker nodes. + * @param workerNode - The worker node. */ - protected abstract get busy (): boolean + private removeWorkerNode (workerNode: IWorkerNode): void { + const workerNodeKey = this.workerNodes.indexOf(workerNode) + if (workerNodeKey !== -1) { + this.workerNodes.splice(workerNodeKey, 1) + this.workerChoiceStrategiesContext?.remove(workerNodeKey) + workerNode.info.dynamic && + this.checkAndEmitDynamicWorkerDestructionEvents() + } + } - /** @inheritDoc */ - public get info (): PoolInfo { - return { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - defaultStrategy: this.opts.workerChoiceStrategy!, - maxSize: this.maximumNumberOfWorkers ?? this.minimumNumberOfWorkers, - minSize: this.minimumNumberOfWorkers, - ready: this.ready, - started: this.started, - strategyRetries: this.workerChoiceStrategiesContext?.retriesCount ?? 0, - type: this.type, - version, - worker: this.worker, - ...(this.workerChoiceStrategiesContext?.getTaskStatisticsRequirements() - .runTime.aggregate === true && - this.workerChoiceStrategiesContext.getTaskStatisticsRequirements() - .waitTime.aggregate && { - utilization: round(this.utilization), - }), - busyWorkerNodes: this.workerNodes.reduce( - (accumulator, _, workerNodeKey) => - this.isWorkerNodeBusy(workerNodeKey) ? accumulator + 1 : accumulator, - 0 - ), - executedTasks: this.workerNodes.reduce( - (accumulator, workerNode) => - accumulator + workerNode.usage.tasks.executed, - 0 - ), - executingTasks: this.workerNodes.reduce( - (accumulator, workerNode) => - accumulator + workerNode.usage.tasks.executing, - 0 - ), - failedTasks: this.workerNodes.reduce( - (accumulator, workerNode) => - accumulator + workerNode.usage.tasks.failed, - 0 - ), - idleWorkerNodes: this.workerNodes.reduce( - (accumulator, _, workerNodeKey) => - this.isWorkerNodeIdle(workerNodeKey) ? accumulator + 1 : accumulator, - 0 - ), - workerNodes: this.workerNodes.length, - ...(this.type === PoolTypes.dynamic && { - dynamicWorkerNodes: this.workerNodes.reduce( - (accumulator, workerNode) => - workerNode.info.dynamic ? accumulator + 1 : accumulator, - 0 - ), - }), - ...(this.opts.enableTasksQueue === true && { - backPressure: this.backPressure, - backPressureWorkerNodes: this.workerNodes.reduce( - (accumulator, _, workerNodeKey) => - this.isWorkerNodeBackPressured(workerNodeKey) - ? accumulator + 1 - : accumulator, - 0 - ), - maxQueuedTasks: this.workerNodes.reduce( - (accumulator, workerNode) => - accumulator + (workerNode.usage.tasks.maxQueued ?? 0), - 0 - ), - queuedTasks: this.workerNodes.reduce( - (accumulator, workerNode) => - accumulator + workerNode.usage.tasks.queued, - 0 - ), - stealingWorkerNodes: this.workerNodes.reduce( - (accumulator, _, workerNodeKey) => - this.isWorkerNodeStealing(workerNodeKey) - ? accumulator + 1 - : accumulator, - 0 - ), - stolenTasks: this.workerNodes.reduce( - (accumulator, workerNode) => - accumulator + workerNode.usage.tasks.stolen, - 0 - ), - }), - ...(this.workerChoiceStrategiesContext?.getTaskStatisticsRequirements() - .runTime.aggregate === true && { - runTime: { - maximum: round( - max( - ...this.workerNodes.map( - workerNode => - workerNode.usage.runTime.maximum ?? Number.NEGATIVE_INFINITY - ) - ) - ), - minimum: round( - min( - ...this.workerNodes.map( - workerNode => - workerNode.usage.runTime.minimum ?? Number.POSITIVE_INFINITY - ) - ) - ), - ...(this.workerChoiceStrategiesContext.getTaskStatisticsRequirements() - .runTime.average && { - average: round( - average( - this.workerNodes.reduce( - (accumulator, workerNode) => - accumulator.concat( - workerNode.usage.runTime.history.toArray() - ), - [] - ) - ) - ), - }), - ...(this.workerChoiceStrategiesContext.getTaskStatisticsRequirements() - .runTime.median && { - median: round( - median( - this.workerNodes.reduce( - (accumulator, workerNode) => - accumulator.concat( - workerNode.usage.runTime.history.toArray() - ), - [] - ) - ) - ), - }), - }, - }), - ...(this.workerChoiceStrategiesContext?.getTaskStatisticsRequirements() - .waitTime.aggregate === true && { - waitTime: { - maximum: round( - max( - ...this.workerNodes.map( - workerNode => - workerNode.usage.waitTime.maximum ?? Number.NEGATIVE_INFINITY - ) + private resetTaskSequentiallyStolenStatisticsWorkerUsage ( + workerNodeKey: number, + taskName?: string + ): void { + const workerNode = this.workerNodes[workerNodeKey] + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (workerNode?.usage != null) { + workerNode.usage.tasks.sequentiallyStolen = 0 + } + if ( + taskName != null && + this.shallUpdateTaskFunctionWorkerUsage(workerNodeKey) && + workerNode.getTaskFunctionWorkerUsage(taskName) != null + ) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + workerNode.getTaskFunctionWorkerUsage( + taskName + )!.tasks.sequentiallyStolen = 0 + } + } + + private async sendKillMessageToWorker (workerNodeKey: number): Promise { + await new Promise((resolve, reject) => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (this.workerNodes[workerNodeKey] == null) { + resolve() + return + } + const killMessageListener = (message: MessageValue): void => { + this.checkMessageWorkerId(message) + if (message.kill === 'success') { + resolve() + } else if (message.kill === 'failure') { + reject( + new Error( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Kill message handling failed on worker ${message.workerId?.toString()}` ) - ), - minimum: round( - min( - ...this.workerNodes.map( - workerNode => - workerNode.usage.waitTime.minimum ?? Number.POSITIVE_INFINITY + ) + } + } + // FIXME: should be registered only once + this.registerWorkerMessageListener(workerNodeKey, killMessageListener) + this.sendToWorker(workerNodeKey, { kill: true }) + }) + } + + /** + * Sends the statistics message to worker given its worker node key. + * @param workerNodeKey - The worker node key. + */ + private sendStatisticsMessageToWorker (workerNodeKey: number): void { + this.sendToWorker(workerNodeKey, { + statistics: { + elu: + this.workerChoiceStrategiesContext?.getTaskStatisticsRequirements() + .elu.aggregate ?? false, + runTime: + this.workerChoiceStrategiesContext?.getTaskStatisticsRequirements() + .runTime.aggregate ?? false, + }, + }) + } + + 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 + if ( + message.taskFunctionOperationStatus != null && + message.workerId === workerId + ) { + if (message.taskFunctionOperationStatus) { + resolve(true) + } else { + reject( + new Error( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Task function operation '${message.taskFunctionOperation?.toString()}' failed on worker ${message.workerId?.toString()} with error: '${ + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + message.workerError?.message + }'` ) ) - ), - ...(this.workerChoiceStrategiesContext.getTaskStatisticsRequirements() - .waitTime.average && { - average: round( - average( - this.workerNodes.reduce( - (accumulator, workerNode) => - accumulator.concat( - workerNode.usage.waitTime.history.toArray() - ), - [] - ) - ) - ), - }), - ...(this.workerChoiceStrategiesContext.getTaskStatisticsRequirements() - .waitTime.median && { - median: round( - median( - this.workerNodes.reduce( - (accumulator, workerNode) => - accumulator.concat( - workerNode.usage.waitTime.history.toArray() - ), - [] - ) - ) - ), - }), - }, - }), - ...(this.workerChoiceStrategiesContext?.getTaskStatisticsRequirements() - .elu.aggregate === true && { - elu: { - active: { - maximum: round( - max( - ...this.workerNodes.map( - workerNode => - workerNode.usage.elu.active.maximum ?? - Number.NEGATIVE_INFINITY - ) - ) - ), - minimum: round( - min( - ...this.workerNodes.map( - workerNode => - workerNode.usage.elu.active.minimum ?? - Number.POSITIVE_INFINITY - ) - ) - ), - ...(this.workerChoiceStrategiesContext.getTaskStatisticsRequirements() - .elu.average && { - average: round( - average( - this.workerNodes.reduce( - (accumulator, workerNode) => - accumulator.concat( - workerNode.usage.elu.active.history.toArray() - ), - [] - ) - ) - ), - }), - ...(this.workerChoiceStrategiesContext.getTaskStatisticsRequirements() - .elu.median && { - median: round( - median( - this.workerNodes.reduce( - (accumulator, workerNode) => - accumulator.concat( - workerNode.usage.elu.active.history.toArray() - ), - [] - ) - ) - ), - }), - }, - idle: { - maximum: round( - max( - ...this.workerNodes.map( - workerNode => - workerNode.usage.elu.idle.maximum ?? - Number.NEGATIVE_INFINITY - ) + } + 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 ) - ), - minimum: round( - min( - ...this.workerNodes.map( - workerNode => - workerNode.usage.elu.idle.minimum ?? - Number.POSITIVE_INFINITY - ) + ) { + resolve(true) + } else if ( + responsesReceived.some( + message => message.taskFunctionOperationStatus === false ) - ), - ...(this.workerChoiceStrategiesContext.getTaskStatisticsRequirements() - .elu.average && { - average: round( - average( - this.workerNodes.reduce( - (accumulator, workerNode) => - accumulator.concat( - workerNode.usage.elu.idle.history.toArray() - ), - [] - ) - ) - ), - }), - ...(this.workerChoiceStrategiesContext.getTaskStatisticsRequirements() - .elu.median && { - median: round( - median( - this.workerNodes.reduce( - (accumulator, workerNode) => - accumulator.concat( - workerNode.usage.elu.idle.history.toArray() - ), - [] - ) - ) - ), - }), - }, - utilization: { - average: round( - average( - this.workerNodes.map( - workerNode => workerNode.usage.elu.utilization ?? 0 - ) + ) { + const errorResponse = responsesReceived.find( + response => response.taskFunctionOperationStatus === false ) - ), - median: round( - median( - this.workerNodes.map( - workerNode => workerNode.usage.elu.utilization ?? 0 + reject( + new Error( + `Task function operation '${ + message.taskFunctionOperation as string + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + }' failed on worker ${errorResponse?.workerId?.toString()} with error: '${ + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + errorResponse?.workerError?.message + }'` ) ) - ), - }, - }, - }), + } + this.deregisterWorkerMessageListener( + this.getWorkerNodeKeyByWorkerId(message.workerId), + taskFunctionOperationsListener + ) + } + } + } + for (const workerNodeKey of this.workerNodes.keys()) { + this.registerWorkerMessageListener( + workerNodeKey, + taskFunctionOperationsListener + ) + this.sendToWorker(workerNodeKey, message) + } + }) + } + + private setTasksQueuePriority (workerNodeKey: number): void { + this.workerNodes[workerNodeKey].setTasksQueuePriority( + this.getTasksQueuePriority() + ) + } + + private setTasksQueueSize (size: number): void { + for (const workerNode of this.workerNodes) { + workerNode.tasksQueueBackPressureSize = size } } - /** - * Whether the pool is ready or not. - * @returns The pool readiness boolean status. - */ - private get ready (): boolean { - if (!this.started) { - return false + private setTasksStealingOnBackPressure (): void { + for (const workerNodeKey of this.workerNodes.keys()) { + this.workerNodes[workerNodeKey].on( + 'backPressure', + this.handleWorkerNodeBackPressureEvent + ) + } + } + + private setTaskStealing (): void { + for (const workerNodeKey of this.workerNodes.keys()) { + this.workerNodes[workerNodeKey].on('idle', this.handleWorkerNodeIdleEvent) } + } + + private shallExecuteTask (workerNodeKey: number): boolean { return ( - this.workerNodes.reduce( - (accumulator, workerNode) => - !workerNode.info.dynamic && workerNode.info.ready - ? accumulator + 1 - : accumulator, - 0 - ) >= this.minimumNumberOfWorkers + this.tasksQueueSize(workerNodeKey) === 0 && + this.workerNodes[workerNodeKey].usage.tasks.executing < + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.opts.tasksQueueOptions!.concurrency! ) } /** - * The pool type. - * - * If it is `'dynamic'`, it provides the `max` property. + * Whether the worker node shall update its task function worker usage or not. + * @param workerNodeKey - The worker node key. + * @returns `true` if the worker node shall update its task function worker usage, `false` otherwise. */ - protected abstract get type (): PoolType + private shallUpdateTaskFunctionWorkerUsage (workerNodeKey: number): boolean { + const workerInfo = this.getWorkerInfo(workerNodeKey) + return ( + workerInfo != null && + Array.isArray(workerInfo.taskFunctionsProperties) && + workerInfo.taskFunctionsProperties.length > 2 + ) + } /** - * The approximate pool utilization. - * @returns The pool utilization. + * Starts the minimum number of workers. + * @param initWorkerNodeUsage - Whether to initialize the worker node usage or not. @defaultValue false */ - private get utilization (): number { - if (this.startTimestamp == null) { - return 0 + private startMinimumNumberOfWorkers (initWorkerNodeUsage = false): void { + if (this.minimumNumberOfWorkers === 0) { + return } - const poolTimeCapacity = - (performance.now() - this.startTimestamp) * - (this.maximumNumberOfWorkers ?? this.minimumNumberOfWorkers) - const totalTasksRunTime = this.workerNodes.reduce( - (accumulator, workerNode) => - accumulator + (workerNode.usage.runTime.aggregate ?? 0), - 0 - ) - const totalTasksWaitTime = this.workerNodes.reduce( - (accumulator, workerNode) => - accumulator + (workerNode.usage.waitTime.aggregate ?? 0), - 0 + this.startingMinimumNumberOfWorkers = true + while ( + this.workerNodes.reduce( + (accumulator, workerNode) => + !workerNode.info.dynamic ? accumulator + 1 : accumulator, + 0 + ) < this.minimumNumberOfWorkers + ) { + const workerNodeKey = this.createAndSetupWorkerNode() + initWorkerNodeUsage && + this.initWorkerNodeUsage(this.workerNodes[workerNodeKey]) + } + this.startingMinimumNumberOfWorkers = false + } + + private readonly stealTask = ( + sourceWorkerNode: IWorkerNode, + destinationWorkerNodeKey: number + ): Task | undefined => { + const destinationWorkerNode = this.workerNodes[destinationWorkerNodeKey] + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (destinationWorkerNode == null) { + return + } + // Avoid cross and cascade task stealing. Could be smarter by checking stealing/stolen worker ids pair. + if ( + !sourceWorkerNode.info.ready || + sourceWorkerNode.info.stolen || + sourceWorkerNode.info.stealing || + !destinationWorkerNode.info.ready || + destinationWorkerNode.info.stolen || + destinationWorkerNode.info.stealing + ) { + return + } + destinationWorkerNode.info.stealing = true + sourceWorkerNode.info.stolen = true + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const stolenTask = sourceWorkerNode.dequeueLastPrioritizedTask()! + sourceWorkerNode.info.stolen = false + destinationWorkerNode.info.stealing = false + this.handleTask(destinationWorkerNodeKey, stolenTask) + this.updateTaskStolenStatisticsWorkerUsage( + destinationWorkerNodeKey, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + stolenTask.name! ) - return (totalTasksRunTime + totalTasksWaitTime) / poolTimeCapacity + return stolenTask } - /** - * The worker type. - */ - protected abstract get worker (): WorkerType + private tasksQueueSize (workerNodeKey: number): number { + return this.workerNodes[workerNodeKey].tasksQueueSize() + } + + private unsetTasksStealingOnBackPressure (): void { + for (const workerNodeKey of this.workerNodes.keys()) { + this.workerNodes[workerNodeKey].off( + 'backPressure', + this.handleWorkerNodeBackPressureEvent + ) + } + } + + private unsetTaskStealing (): void { + for (const workerNodeKey of this.workerNodes.keys()) { + this.workerNodes[workerNodeKey].off( + 'idle', + this.handleWorkerNodeIdleEvent + ) + } + } + + private updateTaskSequentiallyStolenStatisticsWorkerUsage ( + workerNodeKey: number, + taskName?: string, + previousTaskName?: string + ): void { + const workerNode = this.workerNodes[workerNodeKey] + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (workerNode?.usage != null && taskName != null) { + ++workerNode.usage.tasks.sequentiallyStolen + } + if ( + taskName != null && + this.shallUpdateTaskFunctionWorkerUsage(workerNodeKey) && + workerNode.getTaskFunctionWorkerUsage(taskName) != null + ) { + const taskFunctionWorkerUsage = + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + workerNode.getTaskFunctionWorkerUsage(taskName)! + if ( + taskFunctionWorkerUsage.tasks.sequentiallyStolen === 0 || + (previousTaskName != null && + previousTaskName === taskName && + taskFunctionWorkerUsage.tasks.sequentiallyStolen > 0) + ) { + ++taskFunctionWorkerUsage.tasks.sequentiallyStolen + } else if (taskFunctionWorkerUsage.tasks.sequentiallyStolen > 0) { + taskFunctionWorkerUsage.tasks.sequentiallyStolen = 0 + } + } + } + + private updateTaskStolenStatisticsWorkerUsage ( + workerNodeKey: number, + taskName: string + ): void { + const workerNode = this.workerNodes[workerNodeKey] + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (workerNode?.usage != null) { + ++workerNode.usage.tasks.stolen + } + if ( + this.shallUpdateTaskFunctionWorkerUsage(workerNodeKey) && + workerNode.getTaskFunctionWorkerUsage(taskName) != null + ) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ++workerNode.getTaskFunctionWorkerUsage(taskName)!.tasks.stolen + } + } + + private readonly workerNodeStealTask = ( + workerNodeKey: number + ): Task | undefined => { + const workerNodes = this.workerNodes + .slice() + .sort( + (workerNodeA, workerNodeB) => + workerNodeB.usage.tasks.queued - workerNodeA.usage.tasks.queued + ) + const sourceWorkerNode = workerNodes.find( + (sourceWorkerNode, sourceWorkerNodeKey) => + sourceWorkerNodeKey !== workerNodeKey && + sourceWorkerNode.usage.tasks.queued > 0 + ) + if (sourceWorkerNode != null) { + return this.stealTask(sourceWorkerNode, workerNodeKey) + } + } } diff --git a/src/pools/cluster/dynamic.ts b/src/pools/cluster/dynamic.ts index 014e3e80..067ef3b8 100644 --- a/src/pools/cluster/dynamic.ts +++ b/src/pools/cluster/dynamic.ts @@ -16,6 +16,21 @@ export class DynamicClusterPool< Data = unknown, Response = unknown > extends FixedClusterPool { + /** @inheritDoc */ + protected override get backPressure (): boolean { + return this.full && this.internalBackPressure() + } + + /** @inheritDoc */ + protected override get busy (): boolean { + return this.full && this.internalBusy() + } + + /** @inheritDoc */ + protected override get type (): PoolType { + return PoolTypes.dynamic + } + /** * Whether the pool empty event has been emitted or not */ @@ -26,6 +41,28 @@ export class DynamicClusterPool< */ private fullEventEmitted: boolean + /** + * Whether the pool is empty or not. + * @returns The pool emptiness boolean status. + */ + private get empty (): boolean { + return ( + this.minimumNumberOfWorkers === 0 && + this.workerNodes.length === this.minimumNumberOfWorkers + ) + } + + /** + * Whether the pool is full or not. + * @returns The pool fullness boolean status. + */ + private get full (): boolean { + return ( + this.workerNodes.length >= + (this.maximumNumberOfWorkers ?? this.minimumNumberOfWorkers) + ) + } + /** * Constructs a new poolifier dynamic cluster pool. * @param min - Minimum number of workers which are always active. @@ -79,41 +116,4 @@ export class DynamicClusterPool< protected override shallCreateDynamicWorker (): boolean { return (!this.full && this.internalBusy()) || this.empty } - - /** @inheritDoc */ - protected override get backPressure (): boolean { - return this.full && this.internalBackPressure() - } - - /** @inheritDoc */ - protected override get busy (): boolean { - return this.full && this.internalBusy() - } - - /** - * Whether the pool is empty or not. - * @returns The pool emptiness boolean status. - */ - private get empty (): boolean { - return ( - this.minimumNumberOfWorkers === 0 && - this.workerNodes.length === this.minimumNumberOfWorkers - ) - } - - /** - * Whether the pool is full or not. - * @returns The pool fullness boolean status. - */ - private get full (): boolean { - return ( - this.workerNodes.length >= - (this.maximumNumberOfWorkers ?? this.minimumNumberOfWorkers) - ) - } - - /** @inheritDoc */ - protected override get type (): PoolType { - return PoolTypes.dynamic - } } diff --git a/src/pools/cluster/fixed.ts b/src/pools/cluster/fixed.ts index 17b64c30..0023aeb8 100644 --- a/src/pools/cluster/fixed.ts +++ b/src/pools/cluster/fixed.ts @@ -22,6 +22,26 @@ export class FixedClusterPool< Data = unknown, Response = unknown > extends AbstractPool { + /** @inheritDoc */ + protected get backPressure (): boolean { + return this.internalBackPressure() + } + + /** @inheritDoc */ + protected get busy (): boolean { + return this.internalBusy() + } + + /** @inheritDoc */ + protected get type (): PoolType { + return PoolTypes.fixed + } + + /** @inheritDoc */ + protected get worker (): WorkerType { + return WorkerTypes.cluster + } + /** * Constructs a new poolifier fixed cluster pool. * @param numberOfWorkers - Number of workers for this pool. @@ -104,24 +124,4 @@ export class FixedClusterPool< protected shallCreateDynamicWorker (): boolean { return false } - - /** @inheritDoc */ - protected get backPressure (): boolean { - return this.internalBackPressure() - } - - /** @inheritDoc */ - protected get busy (): boolean { - return this.internalBusy() - } - - /** @inheritDoc */ - protected get type (): PoolType { - return PoolTypes.fixed - } - - /** @inheritDoc */ - protected get worker (): WorkerType { - return WorkerTypes.cluster - } } diff --git a/src/pools/pool.ts b/src/pools/pool.ts index 74ceb0e3..2318c72e 100644 --- a/src/pools/pool.ts +++ b/src/pools/pool.ts @@ -72,6 +72,146 @@ export const PoolEvents: Readonly<{ taskError: 'taskError', } as const) +/** + * Contract definition for a poolifier pool. + * @typeParam Worker - Type of worker which manages this pool. + * @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 interface IPool< + Worker extends IWorker, + Data = unknown, + Response = unknown +> { + /** + * 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 fn - The task function. + * @returns `true` if the task function was added, `false` otherwise. + * @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-typeerror} If the `fn` parameter is not a function or task function object. + */ + readonly addTaskFunction: ( + name: string, + fn: TaskFunction | TaskFunctionObject + ) => Promise + /** + * Terminates all workers in this pool. + */ + readonly destroy: () => Promise + /** + * Pool event emitter integrated with async resource. + * The async tracking tooling identifier is `poolifier:--pool`. + * + * Events that can currently be listened to: + * + * - `'ready'`: Emitted when the number of workers created in the pool has reached the minimum size expected and are ready. If the pool is dynamic with a minimum number of workers set to zero, this event is emitted when the pool is started. + * - `'busy'`: Emitted when the number of workers created in the pool has reached the maximum size expected and are executing concurrently their tasks quota. + * - `'busyEnd'`: Emitted when the number of workers created in the pool has reached the maximum size expected and are no longer executing concurrently their tasks quota. + * - `'full'`: Emitted when the pool is dynamic and the number of workers created has reached the maximum size expected. + * - `'fullEnd'`: Emitted when the pool is dynamic and the number of workers created has no longer reached the maximum size expected. + * - `'empty'`: Emitted when the pool is dynamic with a minimum number of workers set to zero and the number of workers has reached the minimum size expected. + * - `'destroy'`: Emitted when the pool is destroyed. + * - `'error'`: Emitted when an uncaught error occurs. + * - `'taskError'`: Emitted when an error occurs while executing a task. + * - `'backPressure'`: Emitted when the number of workers created in the pool has reached the maximum size expected and are back pressured (i.e. their tasks queue is full: queue size \>= maximum queue size). + * - `'backPressureEnd'`: Emitted when the number of workers created in the pool has reached the maximum size expected and are no longer back pressured (i.e. their tasks queue is no longer full: queue size \< maximum queue size). + */ + readonly emitter?: EventEmitterAsyncResource + /** + * Enables/disables the worker node tasks queue in this pool. + * @param enable - Whether to enable or disable the worker node tasks queue. + * @param tasksQueueOptions - The worker node tasks queue options. + */ + readonly enableTasksQueue: ( + enable: boolean, + tasksQueueOptions?: TasksQueueOptions + ) => void + /** + * Executes the specified function in the worker constructor with the task data input parameter. + * @param data - The optional task input data for the specified task function. This can only be structured-cloneable data. + * @param name - The optional name of the task function to execute. If not specified, the default task function will be executed. + * @param transferList - An optional array of transferable objects to transfer ownership of. Ownership of the transferred objects is given to the chosen pool's worker_threads worker and they should not be used in the main thread afterwards. + * @returns Promise with a task function response that will be fulfilled when the task is completed. + */ + readonly execute: ( + data?: Data, + name?: string, + transferList?: readonly TransferListItem[] + ) => 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 + /** + * Pool information. + */ + readonly info: PoolInfo + /** + * Lists the properties of task functions available in this pool. + * @returns The properties of task functions available in this pool. + */ + readonly listTaskFunctionsProperties: () => TaskFunctionProperties[] + /** + * Executes the specified function in the worker constructor with the tasks data iterable input parameter. + * @param data - The tasks iterable input data for the specified task function. This can only be an iterable of structured-cloneable data. + * @param name - The optional name of the task function to execute. If not specified, the default task function will be executed. + * @param transferList - An optional array of transferable objects to transfer ownership of. Ownership of the transferred objects is given to the chosen pool's worker_threads worker and they should not be used in the main thread afterwards. + * @returns Promise with an array of task function responses that will be fulfilled when the tasks are completed. + */ + readonly mapExecute: ( + data: Iterable, + name?: string, + transferList?: readonly TransferListItem[] + ) => Promise + /** + * 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) => Promise + /** + * 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) => Promise + /** + * Sets the worker node tasks queue options in this pool. + * @param tasksQueueOptions - The worker node tasks queue options. + */ + readonly setTasksQueueOptions: (tasksQueueOptions: TasksQueueOptions) => void + /** + * Sets the default worker choice strategy in this pool. + * @param workerChoiceStrategy - The default worker choice strategy. + * @param workerChoiceStrategyOptions - The worker choice strategy options. + */ + readonly setWorkerChoiceStrategy: ( + workerChoiceStrategy: WorkerChoiceStrategy, + workerChoiceStrategyOptions?: WorkerChoiceStrategyOptions + ) => void + /** + * Sets the worker choice strategy options in this pool. + * @param workerChoiceStrategyOptions - The worker choice strategy options. + * @returns `true` if the worker choice strategy options were set, `false` otherwise. + */ + readonly setWorkerChoiceStrategyOptions: ( + workerChoiceStrategyOptions: WorkerChoiceStrategyOptions + ) => boolean + /** + * Starts the minimum number of workers in this pool. + */ + readonly start: () => void + /** + * Pool worker nodes. + * @internal + */ + readonly workerNodes: IWorkerNode[] +} + /** * Pool event. */ @@ -143,42 +283,6 @@ export interface PoolInfo { readonly workerNodes: number } -/** - * Worker node tasks queue options. - */ -export interface TasksQueueOptions { - /** - * Maximum number of tasks that can be executed concurrently on a worker node. - * @defaultValue 1 - */ - readonly concurrency?: number - /** - * Maximum tasks queue size per worker node flagging it as back pressured. - * @defaultValue (pool maximum size)^2 - */ - readonly size?: number - /** - * Queued tasks finished timeout in milliseconds at worker node termination. - * @defaultValue 2000 - */ - readonly tasksFinishedTimeout?: number - /** - * Whether to enable tasks stealing under back pressure. - * @defaultValue true - */ - readonly tasksStealingOnBackPressure?: boolean - /** - * Ratio of worker nodes that can steal tasks from another worker node. - * @defaultValue 0.6 - */ - readonly tasksStealingRatio?: number - /** - * Whether to enable task stealing on idle. - * @defaultValue true - */ - readonly taskStealing?: boolean -} - /** * Options for a poolifier pool. * @typeParam Worker - Type of worker. @@ -254,141 +358,37 @@ export interface PoolOptions { } /** - * Contract definition for a poolifier pool. - * @typeParam Worker - Type of worker which manages this pool. - * @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. + * Worker node tasks queue options. */ -export interface IPool< - Worker extends IWorker, - Data = unknown, - Response = unknown -> { - /** - * 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 fn - The task function. - * @returns `true` if the task function was added, `false` otherwise. - * @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-typeerror} If the `fn` parameter is not a function or task function object. - */ - readonly addTaskFunction: ( - name: string, - fn: TaskFunction | TaskFunctionObject - ) => Promise - /** - * Terminates all workers in this pool. - */ - readonly destroy: () => Promise - /** - * Pool event emitter integrated with async resource. - * The async tracking tooling identifier is `poolifier:--pool`. - * - * Events that can currently be listened to: - * - * - `'ready'`: Emitted when the number of workers created in the pool has reached the minimum size expected and are ready. If the pool is dynamic with a minimum number of workers set to zero, this event is emitted when the pool is started. - * - `'busy'`: Emitted when the number of workers created in the pool has reached the maximum size expected and are executing concurrently their tasks quota. - * - `'busyEnd'`: Emitted when the number of workers created in the pool has reached the maximum size expected and are no longer executing concurrently their tasks quota. - * - `'full'`: Emitted when the pool is dynamic and the number of workers created has reached the maximum size expected. - * - `'fullEnd'`: Emitted when the pool is dynamic and the number of workers created has no longer reached the maximum size expected. - * - `'empty'`: Emitted when the pool is dynamic with a minimum number of workers set to zero and the number of workers has reached the minimum size expected. - * - `'destroy'`: Emitted when the pool is destroyed. - * - `'error'`: Emitted when an uncaught error occurs. - * - `'taskError'`: Emitted when an error occurs while executing a task. - * - `'backPressure'`: Emitted when the number of workers created in the pool has reached the maximum size expected and are back pressured (i.e. their tasks queue is full: queue size \>= maximum queue size). - * - `'backPressureEnd'`: Emitted when the number of workers created in the pool has reached the maximum size expected and are no longer back pressured (i.e. their tasks queue is no longer full: queue size \< maximum queue size). - */ - readonly emitter?: EventEmitterAsyncResource - /** - * Enables/disables the worker node tasks queue in this pool. - * @param enable - Whether to enable or disable the worker node tasks queue. - * @param tasksQueueOptions - The worker node tasks queue options. - */ - readonly enableTasksQueue: ( - enable: boolean, - tasksQueueOptions?: TasksQueueOptions - ) => void - /** - * Executes the specified function in the worker constructor with the task data input parameter. - * @param data - The optional task input data for the specified task function. This can only be structured-cloneable data. - * @param name - The optional name of the task function to execute. If not specified, the default task function will be executed. - * @param transferList - An optional array of transferable objects to transfer ownership of. Ownership of the transferred objects is given to the chosen pool's worker_threads worker and they should not be used in the main thread afterwards. - * @returns Promise with a task function response that will be fulfilled when the task is completed. - */ - readonly execute: ( - data?: Data, - name?: string, - transferList?: readonly TransferListItem[] - ) => 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 - /** - * Pool information. - */ - readonly info: PoolInfo - /** - * Lists the properties of task functions available in this pool. - * @returns The properties of task functions available in this pool. - */ - readonly listTaskFunctionsProperties: () => TaskFunctionProperties[] - /** - * Executes the specified function in the worker constructor with the tasks data iterable input parameter. - * @param data - The tasks iterable input data for the specified task function. This can only be an iterable of structured-cloneable data. - * @param name - The optional name of the task function to execute. If not specified, the default task function will be executed. - * @param transferList - An optional array of transferable objects to transfer ownership of. Ownership of the transferred objects is given to the chosen pool's worker_threads worker and they should not be used in the main thread afterwards. - * @returns Promise with an array of task function responses that will be fulfilled when the tasks are completed. - */ - readonly mapExecute: ( - data: Iterable, - name?: string, - transferList?: readonly TransferListItem[] - ) => Promise - /** - * 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) => Promise +export interface TasksQueueOptions { /** - * 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. + * Maximum number of tasks that can be executed concurrently on a worker node. + * @defaultValue 1 */ - readonly setDefaultTaskFunction: (name: string) => Promise + readonly concurrency?: number /** - * Sets the worker node tasks queue options in this pool. - * @param tasksQueueOptions - The worker node tasks queue options. + * Maximum tasks queue size per worker node flagging it as back pressured. + * @defaultValue (pool maximum size)^2 */ - readonly setTasksQueueOptions: (tasksQueueOptions: TasksQueueOptions) => void + readonly size?: number /** - * Sets the default worker choice strategy in this pool. - * @param workerChoiceStrategy - The default worker choice strategy. - * @param workerChoiceStrategyOptions - The worker choice strategy options. + * Queued tasks finished timeout in milliseconds at worker node termination. + * @defaultValue 2000 */ - readonly setWorkerChoiceStrategy: ( - workerChoiceStrategy: WorkerChoiceStrategy, - workerChoiceStrategyOptions?: WorkerChoiceStrategyOptions - ) => void + readonly tasksFinishedTimeout?: number /** - * Sets the worker choice strategy options in this pool. - * @param workerChoiceStrategyOptions - The worker choice strategy options. - * @returns `true` if the worker choice strategy options were set, `false` otherwise. + * Whether to enable tasks stealing under back pressure. + * @defaultValue true */ - readonly setWorkerChoiceStrategyOptions: ( - workerChoiceStrategyOptions: WorkerChoiceStrategyOptions - ) => boolean + readonly tasksStealingOnBackPressure?: boolean /** - * Starts the minimum number of workers in this pool. + * Ratio of worker nodes that can steal tasks from another worker node. + * @defaultValue 0.6 */ - readonly start: () => void + readonly tasksStealingRatio?: number /** - * Pool worker nodes. - * @internal + * Whether to enable task stealing on idle. + * @defaultValue true */ - readonly workerNodes: IWorkerNode[] + readonly taskStealing?: boolean } diff --git a/src/pools/selection-strategies/abstract-worker-choice-strategy.ts b/src/pools/selection-strategies/abstract-worker-choice-strategy.ts index 9d1a62e0..70a00342 100644 --- a/src/pools/selection-strategies/abstract-worker-choice-strategy.ts +++ b/src/pools/selection-strategies/abstract-worker-choice-strategy.ts @@ -24,16 +24,6 @@ export abstract class AbstractWorkerChoiceStrategy< Data = unknown, Response = unknown > implements IWorkerChoiceStrategy { - /** - * The next worker node key. - */ - protected nextWorkerNodeKey: number | undefined = 0 - - /** - * The previous worker node key. - */ - protected previousWorkerNodeKey = 0 - /** @inheritDoc */ public readonly strategyPolicy: StrategyPolicy = { dynamicWorkerReady: true, @@ -48,6 +38,16 @@ export abstract class AbstractWorkerChoiceStrategy< waitTime: DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS, }) + /** + * The next worker node key. + */ + protected nextWorkerNodeKey: number | undefined = 0 + + /** + * The previous worker node key. + */ + protected previousWorkerNodeKey = 0 + /** * Constructs a worker choice strategy bound to the pool. * @param pool - The pool instance. @@ -61,6 +61,27 @@ export abstract class AbstractWorkerChoiceStrategy< this.setOptions(this.opts) } + /** @inheritDoc */ + public abstract choose (): number | undefined + + /** @inheritDoc */ + public abstract remove (workerNodeKey: number): boolean + + /** @inheritDoc */ + public abstract reset (): boolean + + /** @inheritDoc */ + public setOptions (opts: undefined | WorkerChoiceStrategyOptions): void { + this.opts = buildWorkerChoiceStrategyOptions( + this.pool, + opts + ) + this.setTaskStatisticsRequirements(this.opts) + } + + /** @inheritDoc */ + public abstract update (workerNodeKey: number): boolean + /** * Check the next worker node key. */ @@ -157,25 +178,4 @@ export abstract class AbstractWorkerChoiceStrategy< opts!.elu!.median ) } - - /** @inheritDoc */ - public abstract choose (): number | undefined - - /** @inheritDoc */ - public abstract remove (workerNodeKey: number): boolean - - /** @inheritDoc */ - public abstract reset (): boolean - - /** @inheritDoc */ - public setOptions (opts: undefined | WorkerChoiceStrategyOptions): void { - this.opts = buildWorkerChoiceStrategyOptions( - this.pool, - opts - ) - this.setTaskStatisticsRequirements(this.opts) - } - - /** @inheritDoc */ - public abstract update (workerNodeKey: number): boolean } diff --git a/src/pools/selection-strategies/fair-share-worker-choice-strategy.ts b/src/pools/selection-strategies/fair-share-worker-choice-strategy.ts index fc0dcc3b..44f39b57 100644 --- a/src/pools/selection-strategies/fair-share-worker-choice-strategy.ts +++ b/src/pools/selection-strategies/fair-share-worker-choice-strategy.ts @@ -52,6 +52,35 @@ export class FairShareWorkerChoiceStrategy< this.setTaskStatisticsRequirements(this.opts) } + /** @inheritDoc */ + public choose (): number | undefined { + this.setPreviousWorkerNodeKey(this.nextWorkerNodeKey) + this.nextWorkerNodeKey = this.fairShareNextWorkerNodeKey() + return this.nextWorkerNodeKey + } + + /** @inheritDoc */ + public remove (): boolean { + return true + } + + /** @inheritDoc */ + public reset (): boolean { + for (const workerNode of this.pool.workerNodes) { + delete workerNode.strategyData?.virtualTaskEndTimestamp + } + return true + } + + /** @inheritDoc */ + public update (workerNodeKey: number): boolean { + this.pool.workerNodes[workerNodeKey].strategyData = { + virtualTaskEndTimestamp: + this.computeWorkerNodeVirtualTaskEndTimestamp(workerNodeKey), + } + return true + } + /** * Computes the worker node key virtual task end timestamp. * @param workerNodeKey - The worker node key. @@ -112,33 +141,4 @@ export class FairShareWorkerChoiceStrategy< virtualTaskEndTimestamp! : now } - - /** @inheritDoc */ - public choose (): number | undefined { - this.setPreviousWorkerNodeKey(this.nextWorkerNodeKey) - this.nextWorkerNodeKey = this.fairShareNextWorkerNodeKey() - return this.nextWorkerNodeKey - } - - /** @inheritDoc */ - public remove (): boolean { - return true - } - - /** @inheritDoc */ - public reset (): boolean { - for (const workerNode of this.pool.workerNodes) { - delete workerNode.strategyData?.virtualTaskEndTimestamp - } - return true - } - - /** @inheritDoc */ - public update (workerNodeKey: number): boolean { - this.pool.workerNodes[workerNodeKey].strategyData = { - virtualTaskEndTimestamp: - this.computeWorkerNodeVirtualTaskEndTimestamp(workerNodeKey), - } - return true - } } diff --git a/src/pools/selection-strategies/interleaved-weighted-round-robin-worker-choice-strategy.ts b/src/pools/selection-strategies/interleaved-weighted-round-robin-worker-choice-strategy.ts index fbc79718..5a3310e8 100644 --- a/src/pools/selection-strategies/interleaved-weighted-round-robin-worker-choice-strategy.ts +++ b/src/pools/selection-strategies/interleaved-weighted-round-robin-worker-choice-strategy.ts @@ -22,11 +22,26 @@ export class InterleavedWeightedRoundRobinWorkerChoiceStrategy< > extends AbstractWorkerChoiceStrategy implements IWorkerChoiceStrategy { + /** @inheritDoc */ + public override readonly taskStatisticsRequirements: TaskStatisticsRequirements = + Object.freeze({ + elu: DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS, + runTime: { + aggregate: true, + average: true, + median: false, + }, + waitTime: { + aggregate: true, + average: true, + median: false, + }, + }) + /** * Round id. */ private roundId = 0 - /** * Round weights. */ @@ -39,21 +54,6 @@ export class InterleavedWeightedRoundRobinWorkerChoiceStrategy< * Worker node virtual execution time. */ private workerNodeVirtualTaskExecutionTime = 0 - /** @inheritDoc */ - public override readonly taskStatisticsRequirements: TaskStatisticsRequirements = - Object.freeze({ - elu: DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS, - runTime: { - aggregate: true, - average: true, - median: false, - }, - waitTime: { - aggregate: true, - average: true, - median: false, - }, - }) /** @inheritDoc */ public constructor ( @@ -65,34 +65,6 @@ export class InterleavedWeightedRoundRobinWorkerChoiceStrategy< this.roundWeights = this.getRoundWeights() } - private getRoundWeights (): number[] { - return [ - ...new Set( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - Object.values(this.opts!.weights!) - .slice() - .sort((a, b) => a - b) - ), - ] - } - - private interleavedWeightedRoundRobinNextWorkerNodeId (): void { - if (this.pool.workerNodes.length === 0) { - this.workerNodeId = 0 - } else if ( - this.roundId === this.roundWeights.length - 1 && - this.workerNodeId === this.pool.workerNodes.length - 1 - ) { - this.roundId = 0 - this.workerNodeId = 0 - } else if (this.workerNodeId === this.pool.workerNodes.length - 1) { - this.roundId = this.roundId + 1 - this.workerNodeId = 0 - } else { - this.workerNodeId = this.workerNodeId + 1 - } - } - /** @inheritDoc */ public choose (): number | undefined { for ( @@ -176,4 +148,32 @@ export class InterleavedWeightedRoundRobinWorkerChoiceStrategy< public update (): boolean { return true } + + private getRoundWeights (): number[] { + return [ + ...new Set( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + Object.values(this.opts!.weights!) + .slice() + .sort((a, b) => a - b) + ), + ] + } + + private interleavedWeightedRoundRobinNextWorkerNodeId (): void { + if (this.pool.workerNodes.length === 0) { + this.workerNodeId = 0 + } else if ( + this.roundId === this.roundWeights.length - 1 && + this.workerNodeId === this.pool.workerNodes.length - 1 + ) { + this.roundId = 0 + this.workerNodeId = 0 + } else if (this.workerNodeId === this.pool.workerNodes.length - 1) { + this.roundId = this.roundId + 1 + this.workerNodeId = 0 + } else { + this.workerNodeId = this.workerNodeId + 1 + } + } } diff --git a/src/pools/selection-strategies/least-busy-worker-choice-strategy.ts b/src/pools/selection-strategies/least-busy-worker-choice-strategy.ts index b6ad7075..ef61c72a 100644 --- a/src/pools/selection-strategies/least-busy-worker-choice-strategy.ts +++ b/src/pools/selection-strategies/least-busy-worker-choice-strategy.ts @@ -47,21 +47,6 @@ export class LeastBusyWorkerChoiceStrategy< this.setTaskStatisticsRequirements(this.opts) } - private leastBusyNextWorkerNodeKey (): number | undefined { - return this.pool.workerNodes.reduce( - (minWorkerNodeKey, workerNode, workerNodeKey, workerNodes) => { - return this.isWorkerNodeReady(workerNodeKey) && - (workerNode.usage.waitTime.aggregate ?? 0) + - (workerNode.usage.runTime.aggregate ?? 0) < - (workerNodes[minWorkerNodeKey].usage.waitTime.aggregate ?? 0) + - (workerNodes[minWorkerNodeKey].usage.runTime.aggregate ?? 0) - ? workerNodeKey - : minWorkerNodeKey - }, - 0 - ) - } - /** @inheritDoc */ public choose (): number | undefined { this.setPreviousWorkerNodeKey(this.nextWorkerNodeKey) @@ -83,4 +68,19 @@ export class LeastBusyWorkerChoiceStrategy< public update (): boolean { return true } + + private leastBusyNextWorkerNodeKey (): number | undefined { + return this.pool.workerNodes.reduce( + (minWorkerNodeKey, workerNode, workerNodeKey, workerNodes) => { + return this.isWorkerNodeReady(workerNodeKey) && + (workerNode.usage.waitTime.aggregate ?? 0) + + (workerNode.usage.runTime.aggregate ?? 0) < + (workerNodes[minWorkerNodeKey].usage.waitTime.aggregate ?? 0) + + (workerNodes[minWorkerNodeKey].usage.runTime.aggregate ?? 0) + ? workerNodeKey + : minWorkerNodeKey + }, + 0 + ) + } } diff --git a/src/pools/selection-strategies/least-elu-worker-choice-strategy.ts b/src/pools/selection-strategies/least-elu-worker-choice-strategy.ts index 37193a1c..7ac70513 100644 --- a/src/pools/selection-strategies/least-elu-worker-choice-strategy.ts +++ b/src/pools/selection-strategies/least-elu-worker-choice-strategy.ts @@ -43,19 +43,6 @@ export class LeastEluWorkerChoiceStrategy< this.setTaskStatisticsRequirements(this.opts) } - private leastEluNextWorkerNodeKey (): number | undefined { - return this.pool.workerNodes.reduce( - (minWorkerNodeKey, workerNode, workerNodeKey, workerNodes) => { - return this.isWorkerNodeReady(workerNodeKey) && - (workerNode.usage.elu.active.aggregate ?? 0) < - (workerNodes[minWorkerNodeKey].usage.elu.active.aggregate ?? 0) - ? workerNodeKey - : minWorkerNodeKey - }, - 0 - ) - } - /** @inheritDoc */ public choose (): number | undefined { this.setPreviousWorkerNodeKey(this.nextWorkerNodeKey) @@ -77,4 +64,17 @@ export class LeastEluWorkerChoiceStrategy< public update (): boolean { return true } + + private leastEluNextWorkerNodeKey (): number | undefined { + return this.pool.workerNodes.reduce( + (minWorkerNodeKey, workerNode, workerNodeKey, workerNodes) => { + return this.isWorkerNodeReady(workerNodeKey) && + (workerNode.usage.elu.active.aggregate ?? 0) < + (workerNodes[minWorkerNodeKey].usage.elu.active.aggregate ?? 0) + ? workerNodeKey + : minWorkerNodeKey + }, + 0 + ) + } } diff --git a/src/pools/selection-strategies/least-used-worker-choice-strategy.ts b/src/pools/selection-strategies/least-used-worker-choice-strategy.ts index 067e1238..5cdcb3b7 100644 --- a/src/pools/selection-strategies/least-used-worker-choice-strategy.ts +++ b/src/pools/selection-strategies/least-used-worker-choice-strategy.ts @@ -28,20 +28,6 @@ export class LeastUsedWorkerChoiceStrategy< super(pool, opts) } - private leastUsedNextWorkerNodeKey (): number | undefined { - return this.pool.workerNodes.reduce( - (minWorkerNodeKey, workerNode, workerNodeKey, workerNodes) => { - return this.isWorkerNodeReady(workerNodeKey) && - workerNode.usage.tasks.executing + workerNode.usage.tasks.queued < - workerNodes[minWorkerNodeKey].usage.tasks.executing + - workerNodes[minWorkerNodeKey].usage.tasks.queued - ? workerNodeKey - : minWorkerNodeKey - }, - 0 - ) - } - /** @inheritDoc */ public choose (): number | undefined { this.setPreviousWorkerNodeKey(this.nextWorkerNodeKey) @@ -63,4 +49,18 @@ export class LeastUsedWorkerChoiceStrategy< public update (): boolean { return true } + + private leastUsedNextWorkerNodeKey (): number | undefined { + return this.pool.workerNodes.reduce( + (minWorkerNodeKey, workerNode, workerNodeKey, workerNodes) => { + return this.isWorkerNodeReady(workerNodeKey) && + workerNode.usage.tasks.executing + workerNode.usage.tasks.queued < + workerNodes[minWorkerNodeKey].usage.tasks.executing + + workerNodes[minWorkerNodeKey].usage.tasks.queued + ? workerNodeKey + : minWorkerNodeKey + }, + 0 + ) + } } diff --git a/src/pools/selection-strategies/round-robin-worker-choice-strategy.ts b/src/pools/selection-strategies/round-robin-worker-choice-strategy.ts index 39cb3a08..a70b72e9 100644 --- a/src/pools/selection-strategies/round-robin-worker-choice-strategy.ts +++ b/src/pools/selection-strategies/round-robin-worker-choice-strategy.ts @@ -28,14 +28,6 @@ export class RoundRobinWorkerChoiceStrategy< super(pool, opts) } - private roundRobinNextWorkerNodeKey (): number | undefined { - this.nextWorkerNodeKey = - this.nextWorkerNodeKey === this.pool.workerNodes.length - 1 - ? 0 - : (this.nextWorkerNodeKey ?? this.previousWorkerNodeKey) + 1 - return this.nextWorkerNodeKey - } - /** @inheritDoc */ public choose (): number | undefined { const chosenWorkerNodeKey = this.nextWorkerNodeKey @@ -76,4 +68,12 @@ export class RoundRobinWorkerChoiceStrategy< public update (): boolean { return true } + + private roundRobinNextWorkerNodeKey (): number | undefined { + this.nextWorkerNodeKey = + this.nextWorkerNodeKey === this.pool.workerNodes.length - 1 + ? 0 + : (this.nextWorkerNodeKey ?? this.previousWorkerNodeKey) + 1 + return this.nextWorkerNodeKey + } } diff --git a/src/pools/selection-strategies/selection-strategies-types.ts b/src/pools/selection-strategies/selection-strategies-types.ts index 236a3adf..393b7098 100644 --- a/src/pools/selection-strategies/selection-strategies-types.ts +++ b/src/pools/selection-strategies/selection-strategies-types.ts @@ -60,49 +60,62 @@ export const Measurements: Readonly<{ } as const) /** - * Measurement. - */ -export type Measurement = keyof typeof Measurements - -/** - * Measurement options. + * Worker choice strategy interface. + * @internal */ -export interface MeasurementOptions { +export interface IWorkerChoiceStrategy { /** - * Set measurement median. + * Chooses a worker node in the pool and returns its key. + * If no worker nodes are not eligible, `undefined` is returned. + * If `undefined` is returned, the caller retry. + * @returns The worker node key or `undefined`. */ - readonly median: boolean -} - -/** - * Worker choice strategy options. - */ -export interface WorkerChoiceStrategyOptions { + readonly choose: () => number | undefined /** - * Event loop utilization options. - * @defaultValue \{ median: false \} + * Removes the worker node key from strategy internals. + * @param workerNodeKey - The worker node key. + * @returns `true` if the worker node key is removed, `false` otherwise. */ - readonly elu?: MeasurementOptions + readonly remove: (workerNodeKey: number) => boolean /** - * Measurement to use in worker choice strategy supporting it. + * Resets strategy internals. + * @returns `true` if the reset is successful, `false` otherwise. */ - readonly measurement?: Measurement + readonly reset: () => boolean /** - * Runtime options. - * @defaultValue \{ median: false \} + * Sets the worker choice strategy options. + * @param opts - The worker choice strategy options. */ - readonly runTime?: MeasurementOptions + readonly setOptions: (opts: undefined | WorkerChoiceStrategyOptions) => void /** - * Wait time options. - * @defaultValue \{ median: false \} + * Strategy policy. */ - readonly waitTime?: MeasurementOptions + readonly strategyPolicy: StrategyPolicy /** - * Worker weights to use for weighted round robin worker selection strategies. - * A weight is tasks maximum execution time in milliseconds for a worker node. - * @defaultValue Weights computed automatically given the CPU performance. + * Tasks statistics requirements. */ - weights?: Record + readonly taskStatisticsRequirements: TaskStatisticsRequirements + /** + * Updates the worker node key strategy internals. + * This is called after a task has been executed on a worker node. + * @returns `true` if the update is successful, `false` otherwise. + */ + readonly update: (workerNodeKey: number) => boolean +} + +/** + * Measurement. + */ +export type Measurement = keyof typeof Measurements + +/** + * Measurement options. + */ +export interface MeasurementOptions { + /** + * Set measurement median. + */ + readonly median: boolean } /** @@ -124,25 +137,6 @@ export interface MeasurementStatisticsRequirements { median: boolean } -/** - * Pool worker node worker usage statistics requirements. - * @internal - */ -export interface TaskStatisticsRequirements { - /** - * Tasks event loop utilization requirements. - */ - readonly elu: MeasurementStatisticsRequirements - /** - * Tasks runtime requirements. - */ - readonly runTime: MeasurementStatisticsRequirements - /** - * Tasks wait time requirements. - */ - readonly waitTime: MeasurementStatisticsRequirements -} - /** * Strategy policy. * @internal @@ -159,45 +153,51 @@ export interface StrategyPolicy { } /** - * Worker choice strategy interface. + * Pool worker node worker usage statistics requirements. * @internal */ -export interface IWorkerChoiceStrategy { +export interface TaskStatisticsRequirements { /** - * Chooses a worker node in the pool and returns its key. - * If no worker nodes are not eligible, `undefined` is returned. - * If `undefined` is returned, the caller retry. - * @returns The worker node key or `undefined`. + * Tasks event loop utilization requirements. */ - readonly choose: () => number | undefined + readonly elu: MeasurementStatisticsRequirements /** - * Removes the worker node key from strategy internals. - * @param workerNodeKey - The worker node key. - * @returns `true` if the worker node key is removed, `false` otherwise. + * Tasks runtime requirements. */ - readonly remove: (workerNodeKey: number) => boolean + readonly runTime: MeasurementStatisticsRequirements /** - * Resets strategy internals. - * @returns `true` if the reset is successful, `false` otherwise. + * Tasks wait time requirements. */ - readonly reset: () => boolean + readonly waitTime: MeasurementStatisticsRequirements +} + +/** + * Worker choice strategy options. + */ +export interface WorkerChoiceStrategyOptions { /** - * Sets the worker choice strategy options. - * @param opts - The worker choice strategy options. + * Event loop utilization options. + * @defaultValue \{ median: false \} */ - readonly setOptions: (opts: undefined | WorkerChoiceStrategyOptions) => void + readonly elu?: MeasurementOptions /** - * Strategy policy. + * Measurement to use in worker choice strategy supporting it. */ - readonly strategyPolicy: StrategyPolicy + readonly measurement?: Measurement /** - * Tasks statistics requirements. + * Runtime options. + * @defaultValue \{ median: false \} */ - readonly taskStatisticsRequirements: TaskStatisticsRequirements + readonly runTime?: MeasurementOptions /** - * Updates the worker node key strategy internals. - * This is called after a task has been executed on a worker node. - * @returns `true` if the update is successful, `false` otherwise. + * Wait time options. + * @defaultValue \{ median: false \} */ - readonly update: (workerNodeKey: number) => boolean + readonly waitTime?: MeasurementOptions + /** + * Worker weights to use for weighted round robin worker selection strategies. + * A weight is tasks maximum execution time in milliseconds for a worker node. + * @defaultValue Weights computed automatically given the CPU performance. + */ + weights?: Record } diff --git a/src/pools/selection-strategies/weighted-round-robin-worker-choice-strategy.ts b/src/pools/selection-strategies/weighted-round-robin-worker-choice-strategy.ts index a62103a9..edc54cd5 100644 --- a/src/pools/selection-strategies/weighted-round-robin-worker-choice-strategy.ts +++ b/src/pools/selection-strategies/weighted-round-robin-worker-choice-strategy.ts @@ -23,11 +23,6 @@ export class WeightedRoundRobinWorkerChoiceStrategy< > extends AbstractWorkerChoiceStrategy implements IWorkerChoiceStrategy { - /** - * Worker node virtual execution time. - */ - private workerNodeVirtualTaskExecutionTime = 0 - /** @inheritDoc */ public override readonly taskStatisticsRequirements: TaskStatisticsRequirements = Object.freeze({ @@ -44,6 +39,11 @@ export class WeightedRoundRobinWorkerChoiceStrategy< }, }) + /** + * Worker node virtual execution time. + */ + private workerNodeVirtualTaskExecutionTime = 0 + /** @inheritDoc */ public constructor ( pool: IPool, @@ -53,28 +53,6 @@ export class WeightedRoundRobinWorkerChoiceStrategy< this.setTaskStatisticsRequirements(this.opts) } - private weightedRoundRobinNextWorkerNodeKey (): number | undefined { - const workerWeight = - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.opts!.weights![this.nextWorkerNodeKey ?? this.previousWorkerNodeKey] - if (this.workerNodeVirtualTaskExecutionTime < workerWeight) { - this.workerNodeVirtualTaskExecutionTime += - this.getWorkerNodeTaskWaitTime( - this.nextWorkerNodeKey ?? this.previousWorkerNodeKey - ) + - this.getWorkerNodeTaskRunTime( - this.nextWorkerNodeKey ?? this.previousWorkerNodeKey - ) - } else { - this.nextWorkerNodeKey = - this.nextWorkerNodeKey === this.pool.workerNodes.length - 1 - ? 0 - : (this.nextWorkerNodeKey ?? this.previousWorkerNodeKey) + 1 - this.workerNodeVirtualTaskExecutionTime = 0 - } - return this.nextWorkerNodeKey - } - /** @inheritDoc */ public choose (): number | undefined { this.setPreviousWorkerNodeKey(this.nextWorkerNodeKey) @@ -115,4 +93,26 @@ export class WeightedRoundRobinWorkerChoiceStrategy< public update (): boolean { return true } + + private weightedRoundRobinNextWorkerNodeKey (): number | undefined { + const workerWeight = + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.opts!.weights![this.nextWorkerNodeKey ?? this.previousWorkerNodeKey] + if (this.workerNodeVirtualTaskExecutionTime < workerWeight) { + this.workerNodeVirtualTaskExecutionTime += + this.getWorkerNodeTaskWaitTime( + this.nextWorkerNodeKey ?? this.previousWorkerNodeKey + ) + + this.getWorkerNodeTaskRunTime( + this.nextWorkerNodeKey ?? this.previousWorkerNodeKey + ) + } else { + this.nextWorkerNodeKey = + this.nextWorkerNodeKey === this.pool.workerNodes.length - 1 + ? 0 + : (this.nextWorkerNodeKey ?? this.previousWorkerNodeKey) + 1 + this.workerNodeVirtualTaskExecutionTime = 0 + } + return this.nextWorkerNodeKey + } } diff --git a/src/pools/selection-strategies/worker-choice-strategies-context.ts b/src/pools/selection-strategies/worker-choice-strategies-context.ts index a1ccb4e9..0e5085be 100644 --- a/src/pools/selection-strategies/worker-choice-strategies-context.ts +++ b/src/pools/selection-strategies/worker-choice-strategies-context.ts @@ -28,6 +28,11 @@ export class WorkerChoiceStrategiesContext< Data = unknown, Response = unknown > { + /** + * The number of worker choice strategies execution retries. + */ + public retriesCount: number + /** * The default worker choice strategy in the context. */ @@ -56,11 +61,6 @@ export class WorkerChoiceStrategiesContext< */ private workerChoiceStrategiesTaskStatisticsRequirements: TaskStatisticsRequirements - /** - * The number of worker choice strategies execution retries. - */ - public retriesCount: number - /** * Worker choice strategies context constructor. * @param pool - The pool instance. @@ -97,69 +97,6 @@ export class WorkerChoiceStrategiesContext< ) } - /** - * Adds a worker choice strategy to the context. - * @param workerChoiceStrategy - The worker choice strategy to add. - * @param pool - The pool instance. - * @param opts - The worker choice strategy options. - * @returns The worker choice strategies. - */ - private addWorkerChoiceStrategy ( - workerChoiceStrategy: WorkerChoiceStrategy, - pool: IPool, - opts?: WorkerChoiceStrategyOptions - ): Map { - if (!this.workerChoiceStrategies.has(workerChoiceStrategy)) { - return this.workerChoiceStrategies.set( - workerChoiceStrategy, - getWorkerChoiceStrategy( - workerChoiceStrategy, - pool, - this, - opts - ) - ) - } - return this.workerChoiceStrategies - } - - /** - * Executes the given worker choice strategy. - * @param workerChoiceStrategy - The worker choice strategy. - * @returns The key of the worker node. - * @throws {@link https://nodejs.org/api/errors.html#class-error} If after computed retries the worker node key is null or undefined. - */ - private executeStrategy (workerChoiceStrategy: IWorkerChoiceStrategy): number { - let workerNodeKey: number | undefined - let chooseCount = 0 - let retriesCount = 0 - do { - workerNodeKey = workerChoiceStrategy.choose() - if (workerNodeKey == null && chooseCount > 0) { - ++retriesCount - ++this.retriesCount - } - ++chooseCount - } while (workerNodeKey == null && retriesCount < this.retries) - if (workerNodeKey == null) { - throw new Error( - `Worker node key chosen is null or undefined after ${retriesCount.toString()} retries` - ) - } - return workerNodeKey - } - - /** - * Removes a worker choice strategy from the context. - * @param workerChoiceStrategy - The worker choice strategy to remove. - * @returns `true` if the worker choice strategy is removed, `false` otherwise. - */ - private removeWorkerChoiceStrategy ( - workerChoiceStrategy: WorkerChoiceStrategy - ): boolean { - return this.workerChoiceStrategies.delete(workerChoiceStrategy) - } - /** * Executes the given worker choice strategy in the context algorithm. * @param workerChoiceStrategy - The worker choice strategy algorithm to execute. @defaultValue this.defaultWorkerChoiceStrategy @@ -268,4 +205,67 @@ export class WorkerChoiceStrategiesContext< ([_, workerChoiceStrategy]) => workerChoiceStrategy.update(workerNodeKey) ).every(r => r) } + + /** + * Adds a worker choice strategy to the context. + * @param workerChoiceStrategy - The worker choice strategy to add. + * @param pool - The pool instance. + * @param opts - The worker choice strategy options. + * @returns The worker choice strategies. + */ + private addWorkerChoiceStrategy ( + workerChoiceStrategy: WorkerChoiceStrategy, + pool: IPool, + opts?: WorkerChoiceStrategyOptions + ): Map { + if (!this.workerChoiceStrategies.has(workerChoiceStrategy)) { + return this.workerChoiceStrategies.set( + workerChoiceStrategy, + getWorkerChoiceStrategy( + workerChoiceStrategy, + pool, + this, + opts + ) + ) + } + return this.workerChoiceStrategies + } + + /** + * Executes the given worker choice strategy. + * @param workerChoiceStrategy - The worker choice strategy. + * @returns The key of the worker node. + * @throws {@link https://nodejs.org/api/errors.html#class-error} If after computed retries the worker node key is null or undefined. + */ + private executeStrategy (workerChoiceStrategy: IWorkerChoiceStrategy): number { + let workerNodeKey: number | undefined + let chooseCount = 0 + let retriesCount = 0 + do { + workerNodeKey = workerChoiceStrategy.choose() + if (workerNodeKey == null && chooseCount > 0) { + ++retriesCount + ++this.retriesCount + } + ++chooseCount + } while (workerNodeKey == null && retriesCount < this.retries) + if (workerNodeKey == null) { + throw new Error( + `Worker node key chosen is null or undefined after ${retriesCount.toString()} retries` + ) + } + return workerNodeKey + } + + /** + * Removes a worker choice strategy from the context. + * @param workerChoiceStrategy - The worker choice strategy to remove. + * @returns `true` if the worker choice strategy is removed, `false` otherwise. + */ + private removeWorkerChoiceStrategy ( + workerChoiceStrategy: WorkerChoiceStrategy + ): boolean { + return this.workerChoiceStrategies.delete(workerChoiceStrategy) + } } diff --git a/src/pools/thread/dynamic.ts b/src/pools/thread/dynamic.ts index fdf1486e..b64f79a6 100644 --- a/src/pools/thread/dynamic.ts +++ b/src/pools/thread/dynamic.ts @@ -16,6 +16,21 @@ export class DynamicThreadPool< Data = unknown, Response = unknown > extends FixedThreadPool { + /** @inheritDoc */ + protected override get backPressure (): boolean { + return this.full && this.internalBackPressure() + } + + /** @inheritDoc */ + protected override get busy (): boolean { + return this.full && this.internalBusy() + } + + /** @inheritDoc */ + protected override get type (): PoolType { + return PoolTypes.dynamic + } + /** * Whether the pool empty event has been emitted or not */ @@ -26,6 +41,28 @@ export class DynamicThreadPool< */ private fullEventEmitted: boolean + /** + * Whether the pool is empty or not. + * @returns The pool emptiness boolean status. + */ + private get empty (): boolean { + return ( + this.minimumNumberOfWorkers === 0 && + this.workerNodes.length === this.minimumNumberOfWorkers + ) + } + + /** + * Whether the pool is full or not. + * @returns The pool fullness boolean status. + */ + private get full (): boolean { + return ( + this.workerNodes.length >= + (this.maximumNumberOfWorkers ?? this.minimumNumberOfWorkers) + ) + } + /** * Constructs a new poolifier dynamic thread pool. * @param min - Minimum number of threads which are always active. @@ -79,41 +116,4 @@ export class DynamicThreadPool< protected override shallCreateDynamicWorker (): boolean { return (!this.full && this.internalBusy()) || this.empty } - - /** @inheritDoc */ - protected override get backPressure (): boolean { - return this.full && this.internalBackPressure() - } - - /** @inheritDoc */ - protected override get busy (): boolean { - return this.full && this.internalBusy() - } - - /** - * Whether the pool is empty or not. - * @returns The pool emptiness boolean status. - */ - private get empty (): boolean { - return ( - this.minimumNumberOfWorkers === 0 && - this.workerNodes.length === this.minimumNumberOfWorkers - ) - } - - /** - * Whether the pool is full or not. - * @returns The pool fullness boolean status. - */ - private get full (): boolean { - return ( - this.workerNodes.length >= - (this.maximumNumberOfWorkers ?? this.minimumNumberOfWorkers) - ) - } - - /** @inheritDoc */ - protected override get type (): PoolType { - return PoolTypes.dynamic - } } diff --git a/src/pools/thread/fixed.ts b/src/pools/thread/fixed.ts index 612a9edf..90b7661c 100644 --- a/src/pools/thread/fixed.ts +++ b/src/pools/thread/fixed.ts @@ -26,6 +26,26 @@ export class FixedThreadPool< Data = unknown, Response = unknown > extends AbstractPool { + /** @inheritDoc */ + protected get backPressure (): boolean { + return this.internalBackPressure() + } + + /** @inheritDoc */ + protected get busy (): boolean { + return this.internalBusy() + } + + /** @inheritDoc */ + protected get type (): PoolType { + return PoolTypes.fixed + } + + /** @inheritDoc */ + protected get worker (): WorkerType { + return WorkerTypes.thread + } + /** * Constructs a new poolifier fixed thread pool. * @param numberOfThreads - Number of threads for this pool. @@ -124,24 +144,4 @@ export class FixedThreadPool< protected shallCreateDynamicWorker (): boolean { return false } - - /** @inheritDoc */ - protected get backPressure (): boolean { - return this.internalBackPressure() - } - - /** @inheritDoc */ - protected get busy (): boolean { - return this.internalBusy() - } - - /** @inheritDoc */ - protected get type (): PoolType { - return PoolTypes.fixed - } - - /** @inheritDoc */ - protected get worker (): WorkerType { - return WorkerTypes.thread - } } diff --git a/src/pools/worker-node.ts b/src/pools/worker-node.ts index 73160a25..93ecb073 100644 --- a/src/pools/worker-node.ts +++ b/src/pools/worker-node.ts @@ -33,9 +33,6 @@ import { export class WorkerNode extends EventEmitter implements IWorkerNode { - private setBackPressureFlag: boolean - private readonly taskFunctionsUsage: Map - private readonly tasksQueue: PriorityQueue> /** @inheritdoc */ public readonly info: WorkerInfo /** @inheritdoc */ @@ -48,6 +45,9 @@ export class WorkerNode public usage: WorkerUsage /** @inheritdoc */ public readonly worker: Worker + private setBackPressureFlag: boolean + private readonly taskFunctionsUsage: Map + private readonly tasksQueue: PriorityQueue> /** * Constructs a new worker node. @@ -77,120 +77,6 @@ export class WorkerNode this.taskFunctionsUsage = new Map() } - private closeMessageChannel (): void { - if (this.messageChannel != null) { - this.messageChannel.port1.unref() - this.messageChannel.port2.unref() - this.messageChannel.port1.close() - this.messageChannel.port2.close() - delete this.messageChannel - } - } - - /** - * Whether the worker node is back pressured or not. - * @returns `true` if the worker node is back pressured, `false` otherwise. - */ - private hasBackPressure (): boolean { - return this.tasksQueue.size >= this.tasksQueueBackPressureSize - } - - private initTaskFunctionWorkerUsage (name: string): WorkerUsage { - const getTaskFunctionQueueSize = (): number => { - let taskFunctionQueueSize = 0 - for (const task of this.tasksQueue) { - if ( - (task.name === DEFAULT_TASK_NAME && - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - name === this.info.taskFunctionsProperties![1].name) || - (task.name !== DEFAULT_TASK_NAME && name === task.name) - ) { - ++taskFunctionQueueSize - } - } - return taskFunctionQueueSize - } - return { - elu: { - active: { - history: new CircularBuffer(MeasurementHistorySize), - }, - idle: { - history: new CircularBuffer(MeasurementHistorySize), - }, - }, - runTime: { - history: new CircularBuffer(MeasurementHistorySize), - }, - tasks: { - executed: 0, - executing: 0, - failed: 0, - get queued (): number { - return getTaskFunctionQueueSize() - }, - sequentiallyStolen: 0, - stolen: 0, - }, - waitTime: { - history: new CircularBuffer(MeasurementHistorySize), - }, - } - } - - private initWorkerInfo (worker: Worker): WorkerInfo { - return { - backPressure: false, - backPressureStealing: false, - continuousStealing: false, - dynamic: false, - id: getWorkerId(worker), - ready: false, - stealing: false, - stolen: false, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - type: getWorkerType(worker)!, - } - } - - private initWorkerUsage (): WorkerUsage { - const getTasksQueueSize = (): number => { - return this.tasksQueue.size - } - const getTasksQueueMaxSize = (): number => { - return this.tasksQueue.maxSize - } - return { - elu: { - active: { - history: new CircularBuffer(MeasurementHistorySize), - }, - idle: { - history: new CircularBuffer(MeasurementHistorySize), - }, - }, - runTime: { - history: new CircularBuffer(MeasurementHistorySize), - }, - tasks: { - executed: 0, - executing: 0, - failed: 0, - get maxQueued (): number { - return getTasksQueueMaxSize() - }, - get queued (): number { - return getTasksQueueSize() - }, - sequentiallyStolen: 0, - stolen: 0, - }, - waitTime: { - history: new CircularBuffer(MeasurementHistorySize), - }, - } - } - /** @inheritdoc */ public clearTasksQueue (): void { this.tasksQueue.clear() @@ -311,4 +197,118 @@ export class WorkerNode } await waitWorkerExit } + + private closeMessageChannel (): void { + if (this.messageChannel != null) { + this.messageChannel.port1.unref() + this.messageChannel.port2.unref() + this.messageChannel.port1.close() + this.messageChannel.port2.close() + delete this.messageChannel + } + } + + /** + * Whether the worker node is back pressured or not. + * @returns `true` if the worker node is back pressured, `false` otherwise. + */ + private hasBackPressure (): boolean { + return this.tasksQueue.size >= this.tasksQueueBackPressureSize + } + + private initTaskFunctionWorkerUsage (name: string): WorkerUsage { + const getTaskFunctionQueueSize = (): number => { + let taskFunctionQueueSize = 0 + for (const task of this.tasksQueue) { + if ( + (task.name === DEFAULT_TASK_NAME && + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + name === this.info.taskFunctionsProperties![1].name) || + (task.name !== DEFAULT_TASK_NAME && name === task.name) + ) { + ++taskFunctionQueueSize + } + } + return taskFunctionQueueSize + } + return { + elu: { + active: { + history: new CircularBuffer(MeasurementHistorySize), + }, + idle: { + history: new CircularBuffer(MeasurementHistorySize), + }, + }, + runTime: { + history: new CircularBuffer(MeasurementHistorySize), + }, + tasks: { + executed: 0, + executing: 0, + failed: 0, + get queued (): number { + return getTaskFunctionQueueSize() + }, + sequentiallyStolen: 0, + stolen: 0, + }, + waitTime: { + history: new CircularBuffer(MeasurementHistorySize), + }, + } + } + + private initWorkerInfo (worker: Worker): WorkerInfo { + return { + backPressure: false, + backPressureStealing: false, + continuousStealing: false, + dynamic: false, + id: getWorkerId(worker), + ready: false, + stealing: false, + stolen: false, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + type: getWorkerType(worker)!, + } + } + + private initWorkerUsage (): WorkerUsage { + const getTasksQueueSize = (): number => { + return this.tasksQueue.size + } + const getTasksQueueMaxSize = (): number => { + return this.tasksQueue.maxSize + } + return { + elu: { + active: { + history: new CircularBuffer(MeasurementHistorySize), + }, + idle: { + history: new CircularBuffer(MeasurementHistorySize), + }, + }, + runTime: { + history: new CircularBuffer(MeasurementHistorySize), + }, + tasks: { + executed: 0, + executing: 0, + failed: 0, + get maxQueued (): number { + return getTasksQueueMaxSize() + }, + get queued (): number { + return getTasksQueueSize() + }, + sequentiallyStolen: 0, + stolen: 0, + }, + waitTime: { + history: new CircularBuffer(MeasurementHistorySize), + }, + } + } } diff --git a/src/pools/worker.ts b/src/pools/worker.ts index ff8aa620..ff3a5c82 100644 --- a/src/pools/worker.ts +++ b/src/pools/worker.ts @@ -5,53 +5,63 @@ import type { CircularBuffer } from '../circular-buffer.js' import type { Task, TaskFunctionProperties } from '../utility-types.js' /** - * Callback invoked when the worker has started successfully. + * Callback invoked if the worker raised an error. * @typeParam Worker - Type of worker. */ -export type OnlineHandler = (this: Worker) => void +export type ErrorHandler = ( + this: Worker, + error: Error +) => void /** - * Callback invoked if the worker has received a message. + * Worker event handler. * @typeParam Worker - Type of worker. */ -export type MessageHandler = ( - this: Worker, - message: unknown -) => void +export type EventHandler = + | ErrorHandler + | ExitHandler + | MessageHandler + | OnlineHandler /** - * Callback invoked if the worker raised an error. + * Callback invoked when the worker exits successfully. * @typeParam Worker - Type of worker. */ -export type ErrorHandler = ( +export type ExitHandler = ( this: Worker, - error: Error + exitCode: number ) => void /** - * Callback invoked when the worker exits successfully. + * Callback invoked if the worker has received a message. * @typeParam Worker - Type of worker. */ -export type ExitHandler = ( +export type MessageHandler = ( this: Worker, - exitCode: number + message: unknown ) => void /** - * Worker event handler. + * Callback invoked when the worker has started successfully. * @typeParam Worker - Type of worker. */ -export type EventHandler = - | ErrorHandler - | ExitHandler - | MessageHandler - | OnlineHandler +export type OnlineHandler = (this: Worker) => void /** * Measurement history size. */ export const MeasurementHistorySize = 386 +/** + * Event loop utilization measurement statistics. + * @internal + */ +export interface EventLoopUtilizationMeasurementStatistics { + readonly active: MeasurementStatistics + readonly idle: MeasurementStatistics + utilization?: number +} + /** * Measurement statistics. * @internal @@ -83,16 +93,6 @@ export interface MeasurementStatistics { minimum?: number } -/** - * Event loop utilization measurement statistics. - * @internal - */ -export interface EventLoopUtilizationMeasurementStatistics { - readonly active: MeasurementStatistics - readonly idle: MeasurementStatistics - utilization?: number -} - /** * Task statistics. * @internal @@ -137,94 +137,6 @@ export const WorkerTypes: Readonly<{ cluster: 'cluster'; thread: 'thread' }> = thread: 'thread', } as const) -/** - * Worker type. - */ -export type WorkerType = keyof typeof WorkerTypes - -/** - * Worker information. - * @internal - */ -export interface WorkerInfo { - /** - * Back pressure flag. - * This flag is set to `true` when worker node tasks queue is back pressured. - */ - backPressure: boolean - /** - * Back pressure stealing flag. - * This flag is set to `true` when worker node is stealing one task from another back pressured worker node. - */ - backPressureStealing: boolean - /** - * Continuous stealing flag. - * This flag is set to `true` when worker node is continuously stealing tasks from other worker nodes. - */ - continuousStealing: boolean - /** - * Dynamic flag. - */ - dynamic: boolean - /** - * Worker id. - */ - readonly id: number | undefined - /** - * Ready flag. - */ - ready: boolean - /** - * Stealing flag. - * This flag is set to `true` when worker node is stealing one task from another worker node. - */ - stealing: boolean - /** - * Stolen flag. - * This flag is set to `true` when worker node has one task stolen from another worker node. - */ - stolen: boolean - /** - * Task functions properties. - */ - taskFunctionsProperties?: TaskFunctionProperties[] - /** - * Worker type. - */ - readonly type: WorkerType -} - -/** - * Worker usage statistics. - * @internal - */ -export interface WorkerUsage { - /** - * Tasks event loop utilization statistics. - */ - readonly elu: EventLoopUtilizationMeasurementStatistics - /** - * Tasks runtime statistics. - */ - readonly runTime: MeasurementStatistics - /** - * Tasks statistics. - */ - readonly tasks: TaskStatistics - /** - * Tasks wait time statistics. - */ - readonly waitTime: MeasurementStatistics -} - -/** - * Worker choice strategy data. - * @internal - */ -export interface StrategyData { - virtualTaskEndTimestamp?: number -} - /** * Worker interface. */ @@ -270,18 +182,6 @@ export interface IWorker extends EventEmitter { readonly unref?: () => void } -/** - * Worker node options. - * @internal - */ -export interface WorkerNodeOptions { - env?: Record - tasksQueueBackPressureSize: number | undefined - tasksQueueBucketSize: number | undefined - tasksQueuePriority: boolean | undefined - workerOptions?: WorkerOptions -} - /** * Worker node interface. * @typeParam Worker - Type of worker. @@ -383,6 +283,66 @@ export interface IWorkerNode readonly worker: Worker } +/** + * Worker choice strategy data. + * @internal + */ +export interface StrategyData { + virtualTaskEndTimestamp?: number +} + +/** + * Worker information. + * @internal + */ +export interface WorkerInfo { + /** + * Back pressure flag. + * This flag is set to `true` when worker node tasks queue is back pressured. + */ + backPressure: boolean + /** + * Back pressure stealing flag. + * This flag is set to `true` when worker node is stealing one task from another back pressured worker node. + */ + backPressureStealing: boolean + /** + * Continuous stealing flag. + * This flag is set to `true` when worker node is continuously stealing tasks from other worker nodes. + */ + continuousStealing: boolean + /** + * Dynamic flag. + */ + dynamic: boolean + /** + * Worker id. + */ + readonly id: number | undefined + /** + * Ready flag. + */ + ready: boolean + /** + * Stealing flag. + * This flag is set to `true` when worker node is stealing one task from another worker node. + */ + stealing: boolean + /** + * Stolen flag. + * This flag is set to `true` when worker node has one task stolen from another worker node. + */ + stolen: boolean + /** + * Task functions properties. + */ + taskFunctionsProperties?: TaskFunctionProperties[] + /** + * Worker type. + */ + readonly type: WorkerType +} + /** * Worker node event detail. * @internal @@ -391,3 +351,43 @@ export interface WorkerNodeEventDetail { workerId?: number workerNodeKey?: number } + +/** + * Worker node options. + * @internal + */ +export interface WorkerNodeOptions { + env?: Record + tasksQueueBackPressureSize: number | undefined + tasksQueueBucketSize: number | undefined + tasksQueuePriority: boolean | undefined + workerOptions?: WorkerOptions +} + +/** + * Worker type. + */ +export type WorkerType = keyof typeof WorkerTypes + +/** + * Worker usage statistics. + * @internal + */ +export interface WorkerUsage { + /** + * Tasks event loop utilization statistics. + */ + readonly elu: EventLoopUtilizationMeasurementStatistics + /** + * Tasks runtime statistics. + */ + readonly runTime: MeasurementStatistics + /** + * Tasks statistics. + */ + readonly tasks: TaskStatistics + /** + * Tasks wait time statistics. + */ + readonly waitTime: MeasurementStatistics +} diff --git a/src/queues/abstract-fixed-queue.ts b/src/queues/abstract-fixed-queue.ts index 00d4ddda..b3090adf 100644 --- a/src/queues/abstract-fixed-queue.ts +++ b/src/queues/abstract-fixed-queue.ts @@ -10,13 +10,13 @@ import { * @internal */ export abstract class AbstractFixedQueue implements IFixedQueue { - protected start!: number /** @inheritdoc */ public readonly capacity: number /** @inheritdoc */ public nodeArray: FixedQueueNode[] /** @inheritdoc */ public size!: number + protected start!: number /** * Constructs a fixed queue. @@ -30,21 +30,6 @@ export abstract class AbstractFixedQueue implements IFixedQueue { this.clear() } - /** - * Checks the fixed queue size. - * @param size - Queue size. - */ - private checkSize (size: number): void { - if (!Number.isSafeInteger(size)) { - throw new TypeError( - `Invalid fixed queue size: '${size.toString()}' is not an integer` - ) - } - if (size < 0) { - throw new RangeError(`Invalid fixed queue size: ${size.toString()} < 0`) - } - } - /** @inheritdoc */ public clear (): void { this.start = 0 @@ -115,4 +100,19 @@ export abstract class AbstractFixedQueue implements IFixedQueue { }, } } + + /** + * Checks the fixed queue size. + * @param size - Queue size. + */ + private checkSize (size: number): void { + if (!Number.isSafeInteger(size)) { + throw new TypeError( + `Invalid fixed queue size: '${size.toString()}' is not an integer` + ) + } + if (size < 0) { + throw new RangeError(`Invalid fixed queue size: ${size.toString()} < 0`) + } + } } diff --git a/src/queues/priority-queue.ts b/src/queues/priority-queue.ts index c8cb84f0..51be4266 100644 --- a/src/queues/priority-queue.ts +++ b/src/queues/priority-queue.ts @@ -15,12 +15,78 @@ import { * @internal */ export class PriorityQueue { + /** The priority queue maximum size. */ + public maxSize!: number + + /** + * The number of filled prioritized buckets. + * @returns The number of filled prioritized buckets. + */ + public get buckets (): number { + return Math.trunc(this.size / this.bucketSize) + } + + /** + * Whether priority is enabled. + * @returns Whether priority is enabled. + */ + public get enablePriority (): boolean { + return this.priorityEnabled + } + + /** + * Enables/disables priority. + * @param enablePriority - Whether to enable priority. + */ + public set enablePriority (enablePriority: boolean) { + if (this.priorityEnabled === enablePriority) { + return + } + this.priorityEnabled = enablePriority + let head: PriorityQueueNode + let tail: PriorityQueueNode + let prev: PriorityQueueNode | undefined + let node: PriorityQueueNode | undefined = this.tail + let buckets = 0 + while (node != null) { + const currentNode = this.getPriorityQueueNode(node.nodeArray) + if (buckets === 0) { + tail = currentNode + } + if (prev != null) { + prev.next = currentNode + } + prev = currentNode + if (node.next == null) { + head = currentNode + } + ++buckets + node = node.next + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.head = head! + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.tail = tail! + } + + /** + * The priority queue size. + * @returns The priority queue size. + */ + public get size (): number { + let node: PriorityQueueNode | undefined = this.tail + let size = 0 + while (node != null) { + size += node.size + node = node.next + } + return size + } + private readonly bucketSize: number private head!: PriorityQueueNode private priorityEnabled: boolean private tail!: PriorityQueueNode - /** The priority queue maximum size. */ - public maxSize!: number /** * Constructs a priority queue. @@ -45,21 +111,6 @@ export class PriorityQueue { this.clear() } - private getPriorityQueueNode ( - nodeArray?: FixedQueueNode[] - ): PriorityQueueNode { - let fixedQueue: IFixedQueue - if (this.priorityEnabled) { - fixedQueue = new FixedPriorityQueue(this.bucketSize) - } else { - fixedQueue = new FixedQueue(this.bucketSize) - } - if (nodeArray != null) { - fixedQueue.nodeArray = nodeArray - } - return fixedQueue - } - /** * Clears the priority queue. */ @@ -168,68 +219,18 @@ export class PriorityQueue { } } - /** - * The number of filled prioritized buckets. - * @returns The number of filled prioritized buckets. - */ - public get buckets (): number { - return Math.trunc(this.size / this.bucketSize) - } - - /** - * Whether priority is enabled. - * @returns Whether priority is enabled. - */ - public get enablePriority (): boolean { - return this.priorityEnabled - } - - /** - * Enables/disables priority. - * @param enablePriority - Whether to enable priority. - */ - public set enablePriority (enablePriority: boolean) { - if (this.priorityEnabled === enablePriority) { - return - } - this.priorityEnabled = enablePriority - let head: PriorityQueueNode - let tail: PriorityQueueNode - let prev: PriorityQueueNode | undefined - let node: PriorityQueueNode | undefined = this.tail - let buckets = 0 - while (node != null) { - const currentNode = this.getPriorityQueueNode(node.nodeArray) - if (buckets === 0) { - tail = currentNode - } - if (prev != null) { - prev.next = currentNode - } - prev = currentNode - if (node.next == null) { - head = currentNode - } - ++buckets - node = node.next + private getPriorityQueueNode ( + nodeArray?: FixedQueueNode[] + ): PriorityQueueNode { + let fixedQueue: IFixedQueue + if (this.priorityEnabled) { + fixedQueue = new FixedPriorityQueue(this.bucketSize) + } else { + fixedQueue = new FixedQueue(this.bucketSize) } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.head = head! - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.tail = tail! - } - - /** - * The priority queue size. - * @returns The priority queue size. - */ - public get size (): number { - let node: PriorityQueueNode | undefined = this.tail - let size = 0 - while (node != null) { - size += node.size - node = node.next + if (nodeArray != null) { + fixedQueue.nodeArray = nodeArray } - return size + return fixedQueue } } diff --git a/src/utility-types.ts b/src/utility-types.ts index fdc3a056..0a75e2b3 100644 --- a/src/utility-types.ts +++ b/src/utility-types.ts @@ -6,86 +6,92 @@ import type { WorkerChoiceStrategy } from './pools/selection-strategies/selectio import type { KillBehavior } from './worker/worker-options.js' /** - * Worker error. - * @typeParam Data - Type of data sent to the worker triggering an error. This can only be structured-cloneable data. + * Message object that is passed between main worker and worker. + * @typeParam Data - Type of data sent to the worker or execution response. This can only be structured-cloneable data. + * @typeParam ErrorData - Type of data sent to the worker triggering an error. This can only be structured-cloneable data. + * @internal */ -export interface WorkerError { +export interface MessageValue + extends Task { /** - * Data triggering the error. + * Whether the worker starts or stops its activity check. */ - readonly data?: Data + readonly checkActive?: boolean /** - * Error object. + * Kill code. */ - readonly error?: Error + readonly kill?: 'failure' | 'success' | KillBehavior | true /** - * Error message. + * Message port. */ - readonly message: string + readonly port?: MessagePort /** - * Task function name triggering the error. + * Whether the worker is ready or not. */ - readonly name?: string + readonly ready?: boolean /** - * Error stack trace. + * Whether the worker computes the given statistics or not. */ - readonly stack?: string -} - -/** - * Task performance. - * @internal - */ -export interface TaskPerformance { + readonly statistics?: WorkerStatistics /** - * Task event loop utilization. + * Task function serialized to string. */ - readonly elu?: EventLoopUtilization + readonly taskFunction?: string /** - * Task name. + * Task function operation: + * - `'add'` - Add a task function. + * - `'remove'` - Remove a task function. + * - `'default'` - Set a task function as default. */ - readonly name: string + readonly taskFunctionOperation?: 'add' | 'default' | 'remove' /** - * Task runtime. + * Whether the task function operation is successful or not. */ - readonly runTime?: number + readonly taskFunctionOperationStatus?: boolean /** - * Task performance timestamp. + * Task function properties. */ - readonly timestamp: number -} - -/** - * Worker task performance statistics computation settings. - * @internal - */ -export interface WorkerStatistics { + readonly taskFunctionProperties?: TaskFunctionProperties /** - * Whether the worker computes the task event loop utilization (ELU) or not. + * Task functions properties. */ - readonly elu: boolean + readonly taskFunctionsProperties?: TaskFunctionProperties[] /** - * Whether the worker computes the task runtime or not. + * Task performance. */ - readonly runTime: boolean + readonly taskPerformance?: TaskPerformance + /** + * Worker error. + */ + readonly workerError?: WorkerError + /** + * Worker id. + */ + readonly workerId?: number } /** - * Task function properties. + * An object holding the task execution response promise resolve/reject callbacks. + * @typeParam Response - Type of execution response. This can only be structured-cloneable data. + * @internal */ -export interface TaskFunctionProperties { +export interface PromiseResponseWrapper { /** - * Task function name. + * The asynchronous resource used to track the task execution. */ - readonly name: string + readonly asyncResource?: AsyncResource /** - * Task function priority. Lower values have higher priority. + * Reject callback to reject the promise. */ - readonly priority?: number + readonly reject: (reason?: unknown) => void /** - * Task function worker choice strategy. + * Resolve callback to fulfill the promise. */ - readonly strategy?: WorkerChoiceStrategy + readonly resolve: (value: PromiseLike | Response) => void + /** + * The worker node key executing the task. + */ + readonly workerNodeKey: number } /** @@ -126,92 +132,86 @@ export interface Task { } /** - * Message object that is passed between main worker and worker. - * @typeParam Data - Type of data sent to the worker or execution response. This can only be structured-cloneable data. - * @typeParam ErrorData - Type of data sent to the worker triggering an error. This can only be structured-cloneable data. - * @internal + * Task function properties. */ -export interface MessageValue - extends Task { - /** - * Whether the worker starts or stops its activity check. - */ - readonly checkActive?: boolean +export interface TaskFunctionProperties { /** - * Kill code. + * Task function name. */ - readonly kill?: 'failure' | 'success' | KillBehavior | true + readonly name: string /** - * Message port. + * Task function priority. Lower values have higher priority. */ - readonly port?: MessagePort + readonly priority?: number /** - * Whether the worker is ready or not. + * Task function worker choice strategy. */ - readonly ready?: boolean + readonly strategy?: WorkerChoiceStrategy +} + +/** + * Task performance. + * @internal + */ +export interface TaskPerformance { /** - * Whether the worker computes the given statistics or not. + * Task event loop utilization. */ - readonly statistics?: WorkerStatistics + readonly elu?: EventLoopUtilization /** - * Task function serialized to string. + * Task name. */ - readonly taskFunction?: string + readonly name: string /** - * Task function operation: - * - `'add'` - Add a task function. - * - `'remove'` - Remove a task function. - * - `'default'` - Set a task function as default. + * Task runtime. */ - readonly taskFunctionOperation?: 'add' | 'default' | 'remove' + readonly runTime?: number /** - * Whether the task function operation is successful or not. + * Task performance timestamp. */ - readonly taskFunctionOperationStatus?: boolean + readonly timestamp: number +} + +/** + * Worker error. + * @typeParam Data - Type of data sent to the worker triggering an error. This can only be structured-cloneable data. + */ +export interface WorkerError { /** - * Task function properties. + * Data triggering the error. */ - readonly taskFunctionProperties?: TaskFunctionProperties + readonly data?: Data /** - * Task functions properties. + * Error object. */ - readonly taskFunctionsProperties?: TaskFunctionProperties[] + readonly error?: Error /** - * Task performance. + * Error message. */ - readonly taskPerformance?: TaskPerformance + readonly message: string /** - * Worker error. + * Task function name triggering the error. */ - readonly workerError?: WorkerError + readonly name?: string /** - * Worker id. + * Error stack trace. */ - readonly workerId?: number + readonly stack?: string } /** - * An object holding the task execution response promise resolve/reject callbacks. - * @typeParam Response - Type of execution response. This can only be structured-cloneable data. + * Worker task performance statistics computation settings. * @internal */ -export interface PromiseResponseWrapper { - /** - * The asynchronous resource used to track the task execution. - */ - readonly asyncResource?: AsyncResource - /** - * Reject callback to reject the promise. - */ - readonly reject: (reason?: unknown) => void +export interface WorkerStatistics { /** - * Resolve callback to fulfill the promise. + * Whether the worker computes the task event loop utilization (ELU) or not. */ - readonly resolve: (value: PromiseLike | Response) => void + readonly elu: boolean /** - * The worker node key executing the task. + * Whether the worker computes the task runtime or not. */ - readonly workerNodeKey: number + readonly runTime: boolean } /** diff --git a/src/worker/abstract-worker.ts b/src/worker/abstract-worker.ts index 9549d18c..25bf144e 100644 --- a/src/worker/abstract-worker.ts +++ b/src/worker/abstract-worker.ts @@ -74,105 +74,6 @@ export abstract class AbstractWorker< */ protected lastTaskTimestamp!: number - /** - * Runs the given task. - * @param task - The task to execute. - */ - protected readonly run = (task: Task): void => { - const { data, name, taskId } = task - const taskFunctionName = name ?? DEFAULT_TASK_NAME - if (!this.taskFunctions.has(taskFunctionName)) { - this.sendToMainWorker({ - taskId, - workerError: { - data, - name, - ...this.handleError( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - new Error(`Task function '${name!}' not found`) - ), - }, - }) - return - } - const fn = this.taskFunctions.get(taskFunctionName)?.taskFunction - if (isAsyncFunction(fn)) { - this.runAsync(fn as TaskAsyncFunction, task) - } else { - this.runSync(fn as TaskSyncFunction, task) - } - } - - /** - * Runs the given task function asynchronously. - * @param fn - Task function that will be executed. - * @param task - Input data for the task function. - */ - protected readonly runAsync = ( - fn: TaskAsyncFunction, - task: Task - ): void => { - const { data, name, taskId } = task - let taskPerformance = this.beginTaskPerformance(name) - fn(data) - .then(res => { - taskPerformance = this.endTaskPerformance(taskPerformance) - this.sendToMainWorker({ - data: res, - taskId, - taskPerformance, - }) - return undefined - }) - .catch((error: unknown) => { - this.sendToMainWorker({ - taskId, - workerError: { - data, - name, - ...this.handleError(error as Error), - }, - }) - }) - .finally(() => { - this.updateLastTaskTimestamp() - }) - .catch(EMPTY_FUNCTION) - } - - /** - * Runs the given task function synchronously. - * @param fn - Task function that will be executed. - * @param task - Input data for the task function. - */ - protected readonly runSync = ( - fn: TaskSyncFunction, - task: Task - ): void => { - const { data, name, taskId } = task - try { - let taskPerformance = this.beginTaskPerformance(name) - const res = fn(data) - taskPerformance = this.endTaskPerformance(taskPerformance) - this.sendToMainWorker({ - data: res, - taskId, - taskPerformance, - }) - } catch (error) { - this.sendToMainWorker({ - taskId, - workerError: { - data, - name, - ...this.handleError(error as Error), - }, - }) - } finally { - this.updateLastTaskTimestamp() - } - } - /** * Performance statistics computation requirements. */ @@ -207,6 +108,148 @@ export abstract class AbstractWorker< } } + /** + * Adds a task function to the worker. + * If a task function with the same name already exists, it is replaced. + * @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. + */ + public addTaskFunction ( + name: string, + fn: TaskFunction | TaskFunctionObject + ): TaskFunctionOperationResult { + try { + checkTaskFunctionName(name) + if (name === DEFAULT_TASK_NAME) { + throw new Error( + 'Cannot add a task function with the default reserved name' + ) + } + if (typeof fn === 'function') { + fn = { taskFunction: fn } satisfies TaskFunctionObject + } + checkValidTaskFunctionObjectEntry(name, fn) + fn.taskFunction = fn.taskFunction.bind(this) + if ( + this.taskFunctions.get(name) === + this.taskFunctions.get(DEFAULT_TASK_NAME) + ) { + this.taskFunctions.set(DEFAULT_TASK_NAME, fn) + } + this.taskFunctions.set(name, fn) + this.sendTaskFunctionsPropertiesToMainWorker() + return { status: true } + } catch (error) { + return { error: error as Error, status: false } + } + } + + /** + * Checks if the worker has a task function with the given name. + * @param name - The name of the task function to check. + * @returns Whether the worker has a task function with the given name or not. + */ + public hasTaskFunction (name: string): TaskFunctionOperationResult { + try { + checkTaskFunctionName(name) + } catch (error) { + return { error: error as Error, status: false } + } + return { status: this.taskFunctions.has(name) } + } + + /** + * Lists the properties of the worker's task functions. + * @returns The properties of the worker's task functions. + */ + public listTaskFunctionsProperties (): TaskFunctionProperties[] { + let defaultTaskFunctionName = DEFAULT_TASK_NAME + for (const [name, fnObj] of this.taskFunctions) { + if ( + name !== DEFAULT_TASK_NAME && + fnObj === this.taskFunctions.get(DEFAULT_TASK_NAME) + ) { + defaultTaskFunctionName = name + break + } + } + const taskFunctionsProperties: TaskFunctionProperties[] = [] + for (const [name, fnObj] of this.taskFunctions) { + if (name === DEFAULT_TASK_NAME || name === defaultTaskFunctionName) { + continue + } + taskFunctionsProperties.push(buildTaskFunctionProperties(name, fnObj)) + } + return [ + buildTaskFunctionProperties( + DEFAULT_TASK_NAME, + this.taskFunctions.get(DEFAULT_TASK_NAME) + ), + buildTaskFunctionProperties( + defaultTaskFunctionName, + this.taskFunctions.get(defaultTaskFunctionName) + ), + ...taskFunctionsProperties, + ] + } + + /** + * Removes a task function from the worker. + * @param name - The name of the task function to remove. + * @returns Whether the task function existed and was removed or not. + */ + public removeTaskFunction (name: string): TaskFunctionOperationResult { + try { + 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.sendTaskFunctionsPropertiesToMainWorker() + return { status: deleteStatus } + } catch (error) { + return { error: error as Error, status: false } + } + } + + /** + * Sets the default task function to use in the worker. + * @param name - The name of the task function to use as default task function. + * @returns Whether the default task function was set or not. + */ + public setDefaultTaskFunction (name: string): TaskFunctionOperationResult { + try { + 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' + ) + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.taskFunctions.set(DEFAULT_TASK_NAME, this.taskFunctions.get(name)!) + this.sendTaskFunctionsPropertiesToMainWorker() + return { status: true } + } catch (error) { + return { error: error as Error, status: false } + } + } + /** * Returns the main worker. * @returns Reference to the main worker. @@ -349,6 +392,105 @@ export abstract class AbstractWorker< } } + /** + * Runs the given task. + * @param task - The task to execute. + */ + protected readonly run = (task: Task): void => { + const { data, name, taskId } = task + const taskFunctionName = name ?? DEFAULT_TASK_NAME + if (!this.taskFunctions.has(taskFunctionName)) { + this.sendToMainWorker({ + taskId, + workerError: { + data, + name, + ...this.handleError( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + new Error(`Task function '${name!}' not found`) + ), + }, + }) + return + } + const fn = this.taskFunctions.get(taskFunctionName)?.taskFunction + if (isAsyncFunction(fn)) { + this.runAsync(fn as TaskAsyncFunction, task) + } else { + this.runSync(fn as TaskSyncFunction, task) + } + } + + /** + * Runs the given task function asynchronously. + * @param fn - Task function that will be executed. + * @param task - Input data for the task function. + */ + protected readonly runAsync = ( + fn: TaskAsyncFunction, + task: Task + ): void => { + const { data, name, taskId } = task + let taskPerformance = this.beginTaskPerformance(name) + fn(data) + .then(res => { + taskPerformance = this.endTaskPerformance(taskPerformance) + this.sendToMainWorker({ + data: res, + taskId, + taskPerformance, + }) + return undefined + }) + .catch((error: unknown) => { + this.sendToMainWorker({ + taskId, + workerError: { + data, + name, + ...this.handleError(error as Error), + }, + }) + }) + .finally(() => { + this.updateLastTaskTimestamp() + }) + .catch(EMPTY_FUNCTION) + } + + /** + * Runs the given task function synchronously. + * @param fn - Task function that will be executed. + * @param task - Input data for the task function. + */ + protected readonly runSync = ( + fn: TaskSyncFunction, + task: Task + ): void => { + const { data, name, taskId } = task + try { + let taskPerformance = this.beginTaskPerformance(name) + const res = fn(data) + taskPerformance = this.endTaskPerformance(taskPerformance) + this.sendToMainWorker({ + data: res, + taskId, + taskPerformance, + }) + } catch (error) { + this.sendToMainWorker({ + taskId, + workerError: { + data, + name, + ...this.handleError(error as Error), + }, + }) + } finally { + this.updateLastTaskTimestamp() + } + } + /** * Sends task functions properties to the main worker. */ @@ -505,146 +647,4 @@ export abstract class AbstractWorker< this.lastTaskTimestamp = performance.now() } } - - /** - * Adds a task function to the worker. - * If a task function with the same name already exists, it is replaced. - * @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. - */ - public addTaskFunction ( - name: string, - fn: TaskFunction | TaskFunctionObject - ): TaskFunctionOperationResult { - try { - checkTaskFunctionName(name) - if (name === DEFAULT_TASK_NAME) { - throw new Error( - 'Cannot add a task function with the default reserved name' - ) - } - if (typeof fn === 'function') { - fn = { taskFunction: fn } satisfies TaskFunctionObject - } - checkValidTaskFunctionObjectEntry(name, fn) - fn.taskFunction = fn.taskFunction.bind(this) - if ( - this.taskFunctions.get(name) === - this.taskFunctions.get(DEFAULT_TASK_NAME) - ) { - this.taskFunctions.set(DEFAULT_TASK_NAME, fn) - } - this.taskFunctions.set(name, fn) - this.sendTaskFunctionsPropertiesToMainWorker() - return { status: true } - } catch (error) { - return { error: error as Error, status: false } - } - } - - /** - * Checks if the worker has a task function with the given name. - * @param name - The name of the task function to check. - * @returns Whether the worker has a task function with the given name or not. - */ - public hasTaskFunction (name: string): TaskFunctionOperationResult { - try { - checkTaskFunctionName(name) - } catch (error) { - return { error: error as Error, status: false } - } - return { status: this.taskFunctions.has(name) } - } - - /** - * Lists the properties of the worker's task functions. - * @returns The properties of the worker's task functions. - */ - public listTaskFunctionsProperties (): TaskFunctionProperties[] { - let defaultTaskFunctionName = DEFAULT_TASK_NAME - for (const [name, fnObj] of this.taskFunctions) { - if ( - name !== DEFAULT_TASK_NAME && - fnObj === this.taskFunctions.get(DEFAULT_TASK_NAME) - ) { - defaultTaskFunctionName = name - break - } - } - const taskFunctionsProperties: TaskFunctionProperties[] = [] - for (const [name, fnObj] of this.taskFunctions) { - if (name === DEFAULT_TASK_NAME || name === defaultTaskFunctionName) { - continue - } - taskFunctionsProperties.push(buildTaskFunctionProperties(name, fnObj)) - } - return [ - buildTaskFunctionProperties( - DEFAULT_TASK_NAME, - this.taskFunctions.get(DEFAULT_TASK_NAME) - ), - buildTaskFunctionProperties( - defaultTaskFunctionName, - this.taskFunctions.get(defaultTaskFunctionName) - ), - ...taskFunctionsProperties, - ] - } - - /** - * Removes a task function from the worker. - * @param name - The name of the task function to remove. - * @returns Whether the task function existed and was removed or not. - */ - public removeTaskFunction (name: string): TaskFunctionOperationResult { - try { - 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.sendTaskFunctionsPropertiesToMainWorker() - return { status: deleteStatus } - } catch (error) { - return { error: error as Error, status: false } - } - } - - /** - * Sets the default task function to use in the worker. - * @param name - The name of the task function to use as default task function. - * @returns Whether the default task function was set or not. - */ - public setDefaultTaskFunction (name: string): TaskFunctionOperationResult { - try { - 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' - ) - } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.taskFunctions.set(DEFAULT_TASK_NAME, this.taskFunctions.get(name)!) - this.sendTaskFunctionsPropertiesToMainWorker() - return { status: true } - } catch (error) { - return { error: error as Error, status: false } - } - } } diff --git a/src/worker/cluster-worker.ts b/src/worker/cluster-worker.ts index 5a611e80..394e3674 100644 --- a/src/worker/cluster-worker.ts +++ b/src/worker/cluster-worker.ts @@ -24,13 +24,8 @@ export class ClusterWorker< Response = unknown > extends AbstractWorker { /** @inheritDoc */ - protected readonly sendToMainWorker = ( - message: MessageValue - ): void => { - this.getMainWorker().send({ - ...message, - workerId: this.id, - } satisfies MessageValue) + protected get id (): number { + return this.getMainWorker().id } /** @@ -71,7 +66,12 @@ export class ClusterWorker< } /** @inheritDoc */ - protected get id (): number { - return this.getMainWorker().id + protected readonly sendToMainWorker = ( + message: MessageValue + ): void => { + this.getMainWorker().send({ + ...message, + workerId: this.id, + } satisfies MessageValue) } } diff --git a/src/worker/task-functions.ts b/src/worker/task-functions.ts index 9e406730..11099d3c 100644 --- a/src/worker/task-functions.ts +++ b/src/worker/task-functions.ts @@ -1,16 +1,5 @@ import type { WorkerChoiceStrategy } from '../pools/selection-strategies/selection-strategies-types.js' -/** - * Task synchronous function that can be executed. - * @param data - Data sent to the worker. - * @returns Execution response. - * @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 type TaskSyncFunction = ( - data?: Data -) => Response - /** * Task asynchronous function that can be executed. * This function must return a promise. @@ -53,6 +42,14 @@ export interface TaskFunctionObject { taskFunction: TaskFunction } +/** + * Task function operation result. + */ +export interface TaskFunctionOperationResult { + error?: Error + status: boolean +} + /** * Tasks functions that can be executed. * The key is the name of the task function or task function object. @@ -66,9 +63,12 @@ export type TaskFunctions = Record< > /** - * Task function operation result. + * Task synchronous function that can be executed. + * @param data - Data sent to the worker. + * @returns Execution response. + * @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 interface TaskFunctionOperationResult { - error?: Error - status: boolean -} +export type TaskSyncFunction = ( + data?: Data +) => Response diff --git a/src/worker/thread-worker.ts b/src/worker/thread-worker.ts index 513acbd5..9cbba3a5 100644 --- a/src/worker/thread-worker.ts +++ b/src/worker/thread-worker.ts @@ -29,13 +29,8 @@ export class ThreadWorker< Response = unknown > extends AbstractWorker { /** @inheritDoc */ - protected readonly sendToMainWorker = ( - message: MessageValue - ): void => { - this.port?.postMessage({ - ...message, - workerId: this.id, - } satisfies MessageValue) + protected get id (): number { + return threadId } /** @@ -97,7 +92,12 @@ export class ThreadWorker< } /** @inheritDoc */ - protected get id (): number { - return threadId + protected readonly sendToMainWorker = ( + message: MessageValue + ): void => { + this.port?.postMessage({ + ...message, + workerId: this.id, + } satisfies MessageValue) } } diff --git a/tests/pools/worker-node.test.mjs b/tests/pools/worker-node.test.mjs index d0f56367..753cb31d 100644 --- a/tests/pools/worker-node.test.mjs +++ b/tests/pools/worker-node.test.mjs @@ -4,8 +4,8 @@ import { MessageChannel, Worker as ThreadWorker } from 'node:worker_threads' import { CircularBuffer } from '../../lib/circular-buffer.cjs' import { WorkerTypes } from '../../lib/index.cjs' -import { MeasurementHistorySize } from '../../lib/pools/worker.cjs' import { WorkerNode } from '../../lib/pools/worker-node.cjs' +import { MeasurementHistorySize } from '../../lib/pools/worker.cjs' import { PriorityQueue } from '../../lib/queues/priority-queue.cjs' import { DEFAULT_TASK_NAME } from '../../lib/utils.cjs' diff --git a/tests/utils.test.mjs b/tests/utils.test.mjs index 2001ddcd..53360e85 100644 --- a/tests/utils.test.mjs +++ b/tests/utils.test.mjs @@ -181,10 +181,10 @@ describe('Utils test suite', () => { expect(isAsyncFunction(async function () {})).toBe(true) expect(isAsyncFunction(async function named () {})).toBe(true) class TestClass { - testArrowAsync = async () => {} - testArrowSync = () => {} static async testStaticAsync () {} static testStaticSync () {} + testArrowAsync = async () => {} + testArrowSync = () => {} async testAsync () {} testSync () {} }