From: Jérôme Benoit Date: Wed, 21 Aug 2024 19:41:24 +0000 (+0200) Subject: refactor: switch to eslint-plugin-perfectionist X-Git-Tag: v4.2.4~12 X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=972310863f23533360c1021be9c00f375230f81d;p=poolifier.git refactor: switch to eslint-plugin-perfectionist Signed-off-by: Jérôme Benoit --- diff --git a/.lintstagedrc.js b/.lintstagedrc.js index 8cdf685f..9dd36b64 100644 --- a/.lintstagedrc.js +++ b/.lintstagedrc.js @@ -1,8 +1,8 @@ export default { - '**/*.{ts,tsx,js,jsx,cjs,mjs}': [ - 'biome format --write', - 'eslint --cache --fix', - ], - '**/*.json': ['biome format --write'], '**/*.{md,yml,yaml}': ['prettier --cache --write'], + // '**/*.{ts,tsx,js,jsx,cjs,mjs}': [ + // 'biome format --write', + // 'eslint --cache --fix', + // ], + '**/*.json': ['biome format --write'], } diff --git a/benchmarks/benchmarks-types.cjs b/benchmarks/benchmarks-types.cjs index f99a1501..67fab6b3 100644 --- a/benchmarks/benchmarks-types.cjs +++ b/benchmarks/benchmarks-types.cjs @@ -1,7 +1,7 @@ const TaskFunctions = { - jsonIntegerSerialization: 'jsonIntegerSerialization', - fibonacci: 'fibonacci', factorial: 'factorial', + fibonacci: 'fibonacci', + jsonIntegerSerialization: 'jsonIntegerSerialization', readWriteFiles: 'readWriteFiles', } diff --git a/benchmarks/benchmarks-utils.cjs b/benchmarks/benchmarks-utils.cjs index f212c7b2..30eb1e2b 100644 --- a/benchmarks/benchmarks-utils.cjs +++ b/benchmarks/benchmarks-utils.cjs @@ -6,6 +6,7 @@ const { rmSync, writeFileSync, } = require('node:fs') + const { TaskFunctions } = require('./benchmarks-types.cjs') const jsonIntegerSerialization = n => { diff --git a/benchmarks/benchmarks-utils.mjs b/benchmarks/benchmarks-utils.mjs index 6c01efcb..7198beb0 100644 --- a/benchmarks/benchmarks-utils.mjs +++ b/benchmarks/benchmarks-utils.mjs @@ -1,5 +1,4 @@ import { strictEqual } from 'node:assert' - import { bench, clear, group, run } from 'tatami-ng' import { @@ -16,24 +15,15 @@ import { executeTaskFunction } from './benchmarks-utils.cjs' const buildPoolifierPool = (workerType, poolType, poolSize, poolOptions) => { switch (poolType) { - case PoolTypes.fixed: + case PoolTypes.dynamic: switch (workerType) { - case WorkerTypes.thread: - return new FixedThreadPool( - poolSize, - './benchmarks/internal/thread-worker.mjs', - poolOptions - ) case WorkerTypes.cluster: - return new FixedClusterPool( + return new DynamicClusterPool( + Math.floor(poolSize / 2), poolSize, './benchmarks/internal/cluster-worker.cjs', poolOptions ) - } - break - case PoolTypes.dynamic: - switch (workerType) { case WorkerTypes.thread: return new DynamicThreadPool( Math.floor(poolSize / 2), @@ -41,13 +31,22 @@ const buildPoolifierPool = (workerType, poolType, poolSize, poolOptions) => { './benchmarks/internal/thread-worker.mjs', poolOptions ) + } + break + case PoolTypes.fixed: + switch (workerType) { case WorkerTypes.cluster: - return new DynamicClusterPool( - Math.floor(poolSize / 2), + return new FixedClusterPool( poolSize, './benchmarks/internal/cluster-worker.cjs', poolOptions ) + case WorkerTypes.thread: + return new FixedThreadPool( + poolSize, + './benchmarks/internal/thread-worker.mjs', + poolOptions + ) } break } @@ -146,9 +145,9 @@ export const convertTatamiNgToBmf = report => { return { [name]: { latency: { - value: stats?.avg, lower_value: stats?.min, upper_value: stats?.max, + value: stats?.avg, }, throughput: { value: stats?.iter, diff --git a/benchmarks/internal/bench.mjs b/benchmarks/internal/bench.mjs index 8bae6aaf..78f22a84 100644 --- a/benchmarks/internal/bench.mjs +++ b/benchmarks/internal/bench.mjs @@ -25,15 +25,15 @@ let benchmarkReport switch ( parseArgs({ + allowPositionals: true, args: process.argv, options: { type: { - type: 'string', short: 't', + type: 'string', }, }, strict: true, - allowPositionals: true, }).values.type ) { case 'tatami-ng': diff --git a/benchmarks/internal/cluster-worker.cjs b/benchmarks/internal/cluster-worker.cjs index 40d614c7..757c6961 100644 --- a/benchmarks/internal/cluster-worker.cjs +++ b/benchmarks/internal/cluster-worker.cjs @@ -1,7 +1,8 @@ const { isPrimary } = require('node:cluster') + const { ClusterWorker } = require('../../lib/index.cjs') -const { executeTaskFunction } = require('../benchmarks-utils.cjs') const { TaskFunctions } = require('../benchmarks-types.cjs') +const { executeTaskFunction } = require('../benchmarks-utils.cjs') const taskFunction = data => { data = data || {} diff --git a/benchmarks/worker-selection/least.mjs b/benchmarks/worker-selection/least.mjs index 189a5e6c..09679b0c 100644 --- a/benchmarks/worker-selection/least.mjs +++ b/benchmarks/worker-selection/least.mjs @@ -1,5 +1,4 @@ import { randomInt } from 'node:crypto' - import { bench, group, run } from 'tatami-ng' /** diff --git a/eslint.config.js b/eslint.config.js index 517ded5d..eb8a8c33 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2,7 +2,7 @@ import cspellConfigs from '@cspell/eslint-plugin/configs' import js from '@eslint/js' import { defineFlatConfig } from 'eslint-define-config' import jsdoc from 'eslint-plugin-jsdoc' -import simpleImportSort from 'eslint-plugin-simple-import-sort' +import perfectionist from 'eslint-plugin-perfectionist' import globals from 'globals' import neostandard, { plugins } from 'neostandard' @@ -20,12 +20,13 @@ export default defineFlatConfig([ 'jsdoc/check-tag-names': [ 'warn', { - typed: true, definedTags: ['defaultValue', 'experimental', 'typeParam'], + typed: true, }, ], }, }, + ...plugins['typescript-eslint'].config( { extends: [ @@ -45,9 +46,6 @@ export default defineFlatConfig([ } ), { - plugins: { - 'simple-import-sort': simpleImportSort, - }, rules: { '@cspell/spellchecker': [ 'warn', @@ -66,15 +64,14 @@ export default defineFlatConfig([ }, }, ], - 'simple-import-sort/imports': 'error', - 'simple-import-sort/exports': 'error', }, }, + perfectionist.configs['recommended-natural'], ...neostandard({ - ts: true, globals: { ...globals.mocha, }, + ts: true, }), { files: [ @@ -88,27 +85,27 @@ export default defineFlatConfig([ { files: ['examples/**/*.ts'], rules: { + '@typescript-eslint/no-redundant-type-constituents': 'off', + '@typescript-eslint/no-unnecessary-type-assertion': 'off', '@typescript-eslint/no-unsafe-argument': 'off', - '@typescript-eslint/no-unsafe-call': 'off', - '@typescript-eslint/no-unsafe-return': 'off', '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-call': 'off', '@typescript-eslint/no-unsafe-member-access': 'off', - '@typescript-eslint/no-unnecessary-type-assertion': 'off', - '@typescript-eslint/no-redundant-type-constituents': 'off', - '@typescript-eslint/return-await': 'off', + '@typescript-eslint/no-unsafe-return': 'off', '@typescript-eslint/restrict-template-expressions': 'off', + '@typescript-eslint/return-await': 'off', }, }, { files: ['examples/**/*.js', 'examples/**/*.cjs'], rules: { + '@typescript-eslint/no-require-imports': 'off', 'n/no-missing-import': [ 'error', { allowModules: ['ws'], }, ], - '@typescript-eslint/no-require-imports': 'off', }, }, // benchmarks specific configuration @@ -122,8 +119,8 @@ export default defineFlatConfig([ { files: ['tests/**/*.js', 'tests/**/*.mjs', 'tests/**/*.cjs'], rules: { - '@typescript-eslint/no-require-imports': 'off', '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/no-require-imports': 'off', }, }, ]) diff --git a/examples/javascript/dynamicExample.cjs b/examples/javascript/dynamicExample.cjs index ac361d08..7a1b0a0d 100644 --- a/examples/javascript/dynamicExample.cjs +++ b/examples/javascript/dynamicExample.cjs @@ -1,8 +1,8 @@ 'use strict' const { + availableParallelism, DynamicThreadPool, PoolEvents, - availableParallelism, } = require('poolifier') const pool = new DynamicThreadPool( @@ -10,8 +10,8 @@ const pool = new DynamicThreadPool( availableParallelism(), './yourWorker.js', { - onlineHandler: () => console.info('worker is online'), errorHandler: e => console.error(e), + onlineHandler: () => console.info('worker is online'), } ) let poolFull = 0 diff --git a/examples/javascript/fixedExample.cjs b/examples/javascript/fixedExample.cjs index ac2c3bee..d7f12915 100644 --- a/examples/javascript/fixedExample.cjs +++ b/examples/javascript/fixedExample.cjs @@ -1,13 +1,13 @@ 'use strict' const { + availableParallelism, FixedThreadPool, PoolEvents, - availableParallelism, } = require('poolifier') const pool = new FixedThreadPool(availableParallelism(), './yourWorker.cjs', { - onlineHandler: () => console.info('worker is online'), errorHandler: e => console.error(e), + onlineHandler: () => console.info('worker is online'), }) let poolReady = 0 let poolBusy = 0 diff --git a/examples/javascript/multiFunctionExample.cjs b/examples/javascript/multiFunctionExample.cjs index 9f92c132..80aa4a6e 100644 --- a/examples/javascript/multiFunctionExample.cjs +++ b/examples/javascript/multiFunctionExample.cjs @@ -1,12 +1,12 @@ 'use strict' -const { FixedThreadPool, availableParallelism } = require('poolifier') +const { availableParallelism, FixedThreadPool } = require('poolifier') const pool = new FixedThreadPool( availableParallelism(), './multiFunctionWorker.cjs', { - onlineHandler: () => console.info('worker is online'), errorHandler: e => console.error(e), + onlineHandler: () => console.info('worker is online'), } ) diff --git a/examples/typescript/http-client-pool/src/main.ts b/examples/typescript/http-client-pool/src/main.ts index 696671ea..1103c4ef 100644 --- a/examples/typescript/http-client-pool/src/main.ts +++ b/examples/typescript/http-client-pool/src/main.ts @@ -1,8 +1,9 @@ import { availableParallelism } from 'poolifier' -import { httpClientPool } from './pool.js' import type { WorkerResponse } from './types.js' +import { httpClientPool } from './pool.js' + const parallelism = availableParallelism() * 2 const requestUrl = 'http://localhost:8080/' diff --git a/examples/typescript/http-client-pool/src/pool.ts b/examples/typescript/http-client-pool/src/pool.ts index 83bec2fd..e0d64173 100644 --- a/examples/typescript/http-client-pool/src/pool.ts +++ b/examples/typescript/http-client-pool/src/pool.ts @@ -1,6 +1,5 @@ import { dirname, extname, join } from 'node:path' import { fileURLToPath } from 'node:url' - import { availableParallelism, DynamicThreadPool } from 'poolifier' import type { WorkerData, WorkerResponse } from './types.js' @@ -16,11 +15,11 @@ export const httpClientPool = new DynamicThreadPool( workerFile, { enableTasksQueue: true, - tasksQueueOptions: { - concurrency: 8, - }, errorHandler: (e: Error) => { console.error('Thread worker error:', e) }, + tasksQueueOptions: { + concurrency: 8, + }, } ) diff --git a/examples/typescript/http-client-pool/src/types.ts b/examples/typescript/http-client-pool/src/types.ts index d21087dd..36df0cd3 100644 --- a/examples/typescript/http-client-pool/src/types.ts +++ b/examples/typescript/http-client-pool/src/types.ts @@ -1,15 +1,14 @@ -import type { URL } from 'node:url' - import type { AxiosRequestConfig } from 'axios' +import type { URL } from 'node:url' import type { RequestInfo as NodeFetchRequestInfo, RequestInit as NodeFetchRequestInit, } from 'node-fetch' export interface WorkerData { - input: URL | RequestInfo | NodeFetchRequestInfo - init?: RequestInit | NodeFetchRequestInit axiosRequestConfig?: AxiosRequestConfig + init?: NodeFetchRequestInit | RequestInit + input: NodeFetchRequestInfo | RequestInfo | URL } export interface WorkerResponse { diff --git a/examples/typescript/http-client-pool/src/worker.ts b/examples/typescript/http-client-pool/src/worker.ts index 95917d64..a4700326 100644 --- a/examples/typescript/http-client-pool/src/worker.ts +++ b/examples/typescript/http-client-pool/src/worker.ts @@ -10,21 +10,21 @@ import type { WorkerData, WorkerResponse } from './types.js' class HttpClientWorker extends ThreadWorker { public constructor () { super({ - node_fetch: async (workerData?: WorkerData) => { - const response = await nodeFetch( + axios: async (workerData?: WorkerData) => { + const response = await axios({ + method: 'get', // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - workerData!.input as URL | NodeFetchRequestInfo, - workerData?.init as NodeFetchRequestInit - ) - // The response is not structured-cloneable, so we return the response text body instead. + url: workerData!.input as string, + ...workerData?.axiosRequestConfig, + }) return { - text: await response.text(), + text: response.data, } }, fetch: async (workerData?: WorkerData) => { const response = await fetch( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - workerData!.input as URL | RequestInfo, + workerData!.input as RequestInfo | URL, workerData?.init as RequestInit ) // The response is not structured-cloneable, so we return the response text body instead. @@ -32,15 +32,15 @@ class HttpClientWorker extends ThreadWorker { text: await response.text(), } }, - axios: async (workerData?: WorkerData) => { - const response = await axios({ - method: 'get', + node_fetch: async (workerData?: WorkerData) => { + const response = await nodeFetch( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - url: workerData!.input as string, - ...workerData?.axiosRequestConfig, - }) + workerData!.input as NodeFetchRequestInfo | URL, + workerData?.init as NodeFetchRequestInit + ) + // The response is not structured-cloneable, so we return the response text body instead. return { - text: response.data, + text: await response.text(), } }, }) diff --git a/examples/typescript/http-server-pool/express-cluster/rollup.config.ts b/examples/typescript/http-server-pool/express-cluster/rollup.config.ts index 2fd37607..91383774 100644 --- a/examples/typescript/http-server-pool/express-cluster/rollup.config.ts +++ b/examples/typescript/http-server-pool/express-cluster/rollup.config.ts @@ -3,27 +3,27 @@ import { defineConfig } from 'rollup' import del from 'rollup-plugin-delete' export default defineConfig({ + external: ['express', /^node:*/, 'poolifier'], input: ['./src/main.ts', './src/worker.ts'], - strictDeprecations: true, output: [ { - format: 'cjs', + chunkFileNames: '[name]-[hash].cjs', dir: './dist', - sourcemap: true, entryFileNames: '[name].cjs', - chunkFileNames: '[name]-[hash].cjs', + format: 'cjs', + sourcemap: true, }, { - format: 'esm', dir: './dist', + format: 'esm', sourcemap: true, }, ], - external: ['express', /^node:*/, 'poolifier'], plugins: [ typescript(), del({ targets: ['./dist/*'], }), ], + strictDeprecations: true, }) diff --git a/examples/typescript/http-server-pool/express-cluster/src/main.ts b/examples/typescript/http-server-pool/express-cluster/src/main.ts index bde79170..355c5a6b 100644 --- a/examples/typescript/http-server-pool/express-cluster/src/main.ts +++ b/examples/typescript/http-server-pool/express-cluster/src/main.ts @@ -1,6 +1,5 @@ import { dirname, extname, join } from 'node:path' import { fileURLToPath } from 'node:url' - import { availableParallelism, FixedClusterPool } from 'poolifier' import type { WorkerData, WorkerResponse } from './types.js' @@ -15,6 +14,9 @@ const pool = new FixedClusterPool( workerFile, { enableEvents: false, + errorHandler: (e: Error) => { + console.error('Cluster worker error:', e) + }, onlineHandler: () => { pool .execute({ port: 8080 }) @@ -30,8 +32,5 @@ const pool = new FixedClusterPool( console.error('Express failed to start in cluster worker:', error) }) }, - errorHandler: (e: Error) => { - console.error('Cluster worker error:', e) - }, } ) diff --git a/examples/typescript/http-server-pool/express-cluster/src/types.ts b/examples/typescript/http-server-pool/express-cluster/src/types.ts index 7d6c174e..efaa31f8 100644 --- a/examples/typescript/http-server-pool/express-cluster/src/types.ts +++ b/examples/typescript/http-server-pool/express-cluster/src/types.ts @@ -3,6 +3,6 @@ export interface WorkerData { } export interface WorkerResponse { - status: boolean port?: number + status: boolean } 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 6c0aad8c..35a08240 100644 --- a/examples/typescript/http-server-pool/express-cluster/src/worker.ts +++ b/examples/typescript/http-server-pool/express-cluster/src/worker.ts @@ -7,9 +7,7 @@ import { ClusterWorker } from 'poolifier' import type { WorkerData, WorkerResponse } from './types.js' class ExpressWorker extends ClusterWorker { - private static server: Server - - private static readonly factorial = (n: number | bigint): bigint => { + private static readonly factorial = (n: bigint | number): bigint => { if (n === 0 || n === 1) { return 1n } else { @@ -22,6 +20,8 @@ class ExpressWorker extends ClusterWorker { } } + private static server: Server + private static readonly startExpress = ( workerData?: WorkerData ): WorkerResponse => { @@ -54,8 +54,8 @@ class ExpressWorker extends ClusterWorker { ) }) return { - status: true, port: listenerPort ?? port, + status: true, } } diff --git a/examples/typescript/http-server-pool/express-hybrid/rollup.config.ts b/examples/typescript/http-server-pool/express-hybrid/rollup.config.ts index cd2a4a98..06e95db7 100644 --- a/examples/typescript/http-server-pool/express-hybrid/rollup.config.ts +++ b/examples/typescript/http-server-pool/express-hybrid/rollup.config.ts @@ -3,31 +3,31 @@ import { defineConfig } from 'rollup' import del from 'rollup-plugin-delete' export default defineConfig({ + external: ['express', /^node:*/, 'poolifier'], input: [ './src/main.ts', './src/express-worker.ts', './src/request-handler-worker.ts', ], - strictDeprecations: true, output: [ { - format: 'cjs', + chunkFileNames: '[name]-[hash].cjs', dir: './dist', - sourcemap: true, entryFileNames: '[name].cjs', - chunkFileNames: '[name]-[hash].cjs', + format: 'cjs', + sourcemap: true, }, { - format: 'esm', dir: './dist', + format: 'esm', sourcemap: true, }, ], - external: ['express', /^node:*/, 'poolifier'], plugins: [ typescript(), del({ targets: ['./dist/*'], }), ], + strictDeprecations: true, }) 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 2089b3ef..053a25ab 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 @@ -24,16 +24,17 @@ class ExpressWorker extends ClusterWorker< ClusterWorkerData, ClusterWorkerResponse > { - private static server: Server private static requestHandlerPool: DynamicThreadPool< ThreadWorkerData, ThreadWorkerResponse > + private static server: Server + private static readonly startExpress = ( workerData?: ClusterWorkerData ): ClusterWorkerResponse => { - const { port, workerFile, minWorkers, maxWorkers, ...poolOptions } = + const { maxWorkers, minWorkers, port, workerFile, ...poolOptions } = // eslint-disable-next-line @typescript-eslint/no-non-null-assertion workerData! @@ -79,8 +80,8 @@ class ExpressWorker extends ClusterWorker< ) }) return { - status: true, port: listenerPort ?? port, + status: true, } } diff --git a/examples/typescript/http-server-pool/express-hybrid/src/main.ts b/examples/typescript/http-server-pool/express-hybrid/src/main.ts index 3abecad8..60c72488 100644 --- a/examples/typescript/http-server-pool/express-hybrid/src/main.ts +++ b/examples/typescript/http-server-pool/express-hybrid/src/main.ts @@ -1,6 +1,5 @@ import { dirname, extname, join } from 'node:path' import { fileURLToPath } from 'node:url' - import { availableParallelism, FixedClusterPool } from 'poolifier' import type { ClusterWorkerData, ClusterWorkerResponse } from './types.js' @@ -20,22 +19,25 @@ const pool = new FixedClusterPool( expressWorkerFile, { enableEvents: false, + errorHandler: (e: Error) => { + console.error('Cluster worker error:', e) + }, onlineHandler: () => { pool .execute({ - port: 8080, + enableTasksQueue: true, + errorHandler: (e: Error) => { + console.error('Thread worker error:', e) + }, maxWorkers: Math.round(availableParallelism() / 4) < 1 ? 1 : Math.round(availableParallelism() / 4), - workerFile: requestHandlerWorkerFile, - enableTasksQueue: true, + port: 8080, tasksQueueOptions: { concurrency: 8, }, - errorHandler: (e: Error) => { - console.error('Thread worker error:', e) - }, + workerFile: requestHandlerWorkerFile, }) .then(response => { if (response.status) { @@ -49,8 +51,5 @@ const pool = new FixedClusterPool( console.error('Express failed to start in cluster worker:', error) }) }, - errorHandler: (e: Error) => { - console.error('Cluster worker error:', e) - }, } ) 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 5297de12..aac0d8d0 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,7 +10,7 @@ class RequestHandlerWorker< Data extends ThreadWorkerData, Response extends ThreadWorkerResponse > extends ThreadWorker { - private static readonly factorial = (n: number | bigint): bigint => { + private static readonly factorial = (n: bigint | number): bigint => { if (n === 0 || n === 1) { return 1n } else { diff --git a/examples/typescript/http-server-pool/express-hybrid/src/types.ts b/examples/typescript/http-server-pool/express-hybrid/src/types.ts index 76a9133e..8b4e0f23 100644 --- a/examples/typescript/http-server-pool/express-hybrid/src/types.ts +++ b/examples/typescript/http-server-pool/express-hybrid/src/types.ts @@ -1,15 +1,15 @@ import type { ThreadPoolOptions } from 'poolifier' export interface ClusterWorkerData extends ThreadPoolOptions { + maxWorkers?: number + minWorkers?: number port: number workerFile: string - minWorkers?: number - maxWorkers?: number } export interface ClusterWorkerResponse { - status: boolean port?: number + status: boolean } export interface DataPayload { diff --git a/examples/typescript/http-server-pool/express-worker_threads/src/main.ts b/examples/typescript/http-server-pool/express-worker_threads/src/main.ts index 61514364..d1579793 100644 --- a/examples/typescript/http-server-pool/express-worker_threads/src/main.ts +++ b/examples/typescript/http-server-pool/express-worker_threads/src/main.ts @@ -1,6 +1,5 @@ -import { exit } from 'node:process' - import express, { type Express, type Request, type Response } from 'express' +import { exit } from 'node:process' import { requestHandlerPool } from './pool.js' diff --git a/examples/typescript/http-server-pool/express-worker_threads/src/pool.ts b/examples/typescript/http-server-pool/express-worker_threads/src/pool.ts index 29bf4ef7..f4502c43 100644 --- a/examples/typescript/http-server-pool/express-worker_threads/src/pool.ts +++ b/examples/typescript/http-server-pool/express-worker_threads/src/pool.ts @@ -1,6 +1,5 @@ import { dirname, extname, join } from 'node:path' import { fileURLToPath } from 'node:url' - import { availableParallelism, DynamicThreadPool } from 'poolifier' import type { BodyPayload, WorkerData, WorkerResponse } from './types.js' @@ -15,10 +14,10 @@ export const requestHandlerPool = new DynamicThreadPool< WorkerResponse >(1, availableParallelism(), workerFile, { enableTasksQueue: true, - tasksQueueOptions: { - concurrency: 8, - }, errorHandler: (e: Error) => { console.error('Thread worker error:', e) }, + tasksQueueOptions: { + concurrency: 8, + }, }) 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 c5819f83..3e8699bc 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,7 +6,7 @@ class RequestHandlerWorker< Data extends WorkerData, Response extends WorkerResponse > extends ThreadWorker { - private static readonly factorial: (n: number | bigint) => bigint = n => { + private static readonly factorial: (n: bigint | number) => bigint = n => { if (n === 0 || n === 1) { return 1n } else { diff --git a/examples/typescript/http-server-pool/fastify-cluster/rollup.config.ts b/examples/typescript/http-server-pool/fastify-cluster/rollup.config.ts index 03cbd852..93c1d29d 100644 --- a/examples/typescript/http-server-pool/fastify-cluster/rollup.config.ts +++ b/examples/typescript/http-server-pool/fastify-cluster/rollup.config.ts @@ -3,27 +3,27 @@ import { defineConfig } from 'rollup' import del from 'rollup-plugin-delete' export default defineConfig({ + external: ['fastify', /^node:*/, 'poolifier'], input: ['./src/main.ts', './src/worker.ts'], - strictDeprecations: true, output: [ { - format: 'cjs', + chunkFileNames: '[name]-[hash].cjs', dir: './dist', - sourcemap: true, entryFileNames: '[name].cjs', - chunkFileNames: '[name]-[hash].cjs', + format: 'cjs', + sourcemap: true, }, { - format: 'esm', dir: './dist', + format: 'esm', sourcemap: true, }, ], - external: ['fastify', /^node:*/, 'poolifier'], plugins: [ typescript(), del({ targets: ['./dist/*'], }), ], + strictDeprecations: true, }) diff --git a/examples/typescript/http-server-pool/fastify-cluster/src/main.ts b/examples/typescript/http-server-pool/fastify-cluster/src/main.ts index 2afc6538..d85b7369 100644 --- a/examples/typescript/http-server-pool/fastify-cluster/src/main.ts +++ b/examples/typescript/http-server-pool/fastify-cluster/src/main.ts @@ -1,6 +1,5 @@ import { dirname, extname, join } from 'node:path' import { fileURLToPath } from 'node:url' - import { availableParallelism, FixedClusterPool } from 'poolifier' import type { WorkerData, WorkerResponse } from './types.js' @@ -15,6 +14,9 @@ const pool = new FixedClusterPool( workerFile, { enableEvents: false, + errorHandler: (e: Error) => { + console.error('Cluster worker error:', e) + }, onlineHandler: () => { pool .execute({ port: 8080 }) @@ -30,8 +32,5 @@ const pool = new FixedClusterPool( console.error('Fastify failed to start in cluster worker:', error) }) }, - errorHandler: (e: Error) => { - console.error('Cluster worker error:', e) - }, } ) diff --git a/examples/typescript/http-server-pool/fastify-cluster/src/types.ts b/examples/typescript/http-server-pool/fastify-cluster/src/types.ts index 7d6c174e..efaa31f8 100644 --- a/examples/typescript/http-server-pool/fastify-cluster/src/types.ts +++ b/examples/typescript/http-server-pool/fastify-cluster/src/types.ts @@ -3,6 +3,6 @@ export interface WorkerData { } export interface WorkerResponse { - status: boolean port?: number + status: boolean } 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 7003e265..c723e207 100644 --- a/examples/typescript/http-server-pool/fastify-cluster/src/worker.ts +++ b/examples/typescript/http-server-pool/fastify-cluster/src/worker.ts @@ -6,9 +6,7 @@ import { ClusterWorker } from 'poolifier' import type { WorkerData, WorkerResponse } from './types.js' class FastifyWorker extends ClusterWorker { - private static fastify: FastifyInstance - - private static readonly factorial = (n: number | bigint): bigint => { + private static readonly factorial = (n: bigint | number): bigint => { if (n === 0 || n === 1) { return 1n } else { @@ -21,6 +19,8 @@ class FastifyWorker extends ClusterWorker { } } + private static fastify: FastifyInstance + private static readonly startFastify = async ( workerData?: WorkerData ): Promise => { @@ -44,8 +44,8 @@ class FastifyWorker extends ClusterWorker { await FastifyWorker.fastify.listen({ port }) return { - status: true, port: (FastifyWorker.fastify.server.address() as AddressInfo).port, + status: true, } } diff --git a/examples/typescript/http-server-pool/fastify-hybrid/@types/fastify/index.d.ts b/examples/typescript/http-server-pool/fastify-hybrid/@types/fastify/index.d.ts index 166bfd33..4f666be0 100644 --- a/examples/typescript/http-server-pool/fastify-hybrid/@types/fastify/index.d.ts +++ b/examples/typescript/http-server-pool/fastify-hybrid/@types/fastify/index.d.ts @@ -1,17 +1,16 @@ -import type { TransferListItem } from 'node:worker_threads' - import type * as fastify from 'fastify' +import type { TransferListItem } from 'node:worker_threads' import type { DynamicThreadPool } from 'poolifier' import type { ThreadWorkerData, ThreadWorkerResponse } from '../../src/types.ts' declare module 'fastify' { export interface FastifyInstance extends fastify.FastifyInstance { - pool: DynamicThreadPool execute: ( data?: ThreadWorkerData, name?: string, transferList?: readonly TransferListItem[] ) => Promise + pool: DynamicThreadPool } } diff --git a/examples/typescript/http-server-pool/fastify-hybrid/rollup.config.ts b/examples/typescript/http-server-pool/fastify-hybrid/rollup.config.ts index ae79b9bc..617736cb 100644 --- a/examples/typescript/http-server-pool/fastify-hybrid/rollup.config.ts +++ b/examples/typescript/http-server-pool/fastify-hybrid/rollup.config.ts @@ -3,31 +3,31 @@ import { defineConfig } from 'rollup' import del from 'rollup-plugin-delete' export default defineConfig({ + external: ['fastify', 'fastify-plugin', /^node:*/, 'poolifier'], input: [ './src/main.ts', './src/fastify-worker.ts', './src/request-handler-worker.ts', ], - strictDeprecations: true, output: [ { - format: 'cjs', + chunkFileNames: '[name]-[hash].cjs', dir: './dist', - sourcemap: true, entryFileNames: '[name].cjs', - chunkFileNames: '[name]-[hash].cjs', + format: 'cjs', + sourcemap: true, }, { - format: 'esm', dir: './dist', + format: 'esm', sourcemap: true, }, ], - external: ['fastify', 'fastify-plugin', /^node:*/, 'poolifier'], plugins: [ typescript(), del({ targets: ['./dist/*'], }), ], + strictDeprecations: true, }) diff --git a/examples/typescript/http-server-pool/fastify-hybrid/src/fastify-poolifier.ts b/examples/typescript/http-server-pool/fastify-hybrid/src/fastify-poolifier.ts index 753f6df1..ece9b8cf 100644 --- a/examples/typescript/http-server-pool/fastify-hybrid/src/fastify-poolifier.ts +++ b/examples/typescript/http-server-pool/fastify-hybrid/src/fastify-poolifier.ts @@ -1,6 +1,6 @@ +import type { FastifyPluginCallback } from 'fastify' import type { TransferListItem } from 'node:worker_threads' -import type { FastifyPluginCallback } from 'fastify' import fp from 'fastify-plugin' import { availableParallelism, DynamicThreadPool } from 'poolifier' @@ -17,12 +17,12 @@ const fastifyPoolifierPlugin: FastifyPluginCallback = ( ) => { options = { ...{ - minWorkers: 1, maxWorkers: availableParallelism(), + minWorkers: 1, }, ...options, } - const { workerFile, minWorkers, maxWorkers, ...poolOptions } = options + const { maxWorkers, minWorkers, workerFile, ...poolOptions } = options const pool = new DynamicThreadPool( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion minWorkers!, 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 4a697f3e..6b10dbd9 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 @@ -3,9 +3,10 @@ import type { AddressInfo } from 'node:net' import Fastify, { type FastifyInstance } from 'fastify' import { ClusterWorker } from 'poolifier' -import { fastifyPoolifier } from './fastify-poolifier.js' import type { ClusterWorkerData, ClusterWorkerResponse } from './types.js' +import { fastifyPoolifier } from './fastify-poolifier.js' + class FastifyWorker extends ClusterWorker< ClusterWorkerData, ClusterWorkerResponse @@ -44,8 +45,8 @@ class FastifyWorker extends ClusterWorker< await FastifyWorker.fastify.listen({ port }) return { - status: true, port: (FastifyWorker.fastify.server.address() as AddressInfo).port, + status: true, } } diff --git a/examples/typescript/http-server-pool/fastify-hybrid/src/main.ts b/examples/typescript/http-server-pool/fastify-hybrid/src/main.ts index 606e7766..60e8655e 100644 --- a/examples/typescript/http-server-pool/fastify-hybrid/src/main.ts +++ b/examples/typescript/http-server-pool/fastify-hybrid/src/main.ts @@ -1,6 +1,5 @@ import { dirname, extname, join } from 'node:path' import { fileURLToPath } from 'node:url' - import { availableParallelism, FixedClusterPool } from 'poolifier' import type { ClusterWorkerData, ClusterWorkerResponse } from './types.js' @@ -20,22 +19,25 @@ const pool = new FixedClusterPool( fastifyWorkerFile, { enableEvents: false, + errorHandler: (e: Error) => { + console.error('Cluster worker error:', e) + }, onlineHandler: () => { pool .execute({ - port: 8080, - workerFile: requestHandlerWorkerFile, + enableTasksQueue: true, + errorHandler: (e: Error) => { + console.error('Thread worker error', e) + }, maxWorkers: Math.round(availableParallelism() / 4) < 1 ? 1 : Math.round(availableParallelism() / 4), - enableTasksQueue: true, + port: 8080, tasksQueueOptions: { concurrency: 8, }, - errorHandler: (e: Error) => { - console.error('Thread worker error', e) - }, + workerFile: requestHandlerWorkerFile, }) .then(response => { if (response.status) { @@ -49,8 +51,5 @@ const pool = new FixedClusterPool( console.error('Fastify failed to start in cluster worker:', error) }) }, - errorHandler: (e: Error) => { - console.error('Cluster worker error:', e) - }, } ) 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 5297de12..aac0d8d0 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,7 +10,7 @@ class RequestHandlerWorker< Data extends ThreadWorkerData, Response extends ThreadWorkerResponse > extends ThreadWorker { - private static readonly factorial = (n: number | bigint): bigint => { + private static readonly factorial = (n: bigint | number): bigint => { if (n === 0 || n === 1) { return 1n } else { 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 179ac65e..b1a6971b 100644 --- a/examples/typescript/http-server-pool/fastify-hybrid/src/types.ts +++ b/examples/typescript/http-server-pool/fastify-hybrid/src/types.ts @@ -5,8 +5,8 @@ export interface ClusterWorkerData extends FastifyPoolifierOptions { } export interface ClusterWorkerResponse { - status: boolean port?: number + status: boolean } export interface DataPayload { @@ -22,7 +22,7 @@ export interface ThreadWorkerResponse { } export interface FastifyPoolifierOptions extends ThreadPoolOptions { - workerFile: string - minWorkers?: number maxWorkers?: number + minWorkers?: number + workerFile: string } diff --git a/examples/typescript/http-server-pool/fastify-worker_threads/@types/fastify/index.d.ts b/examples/typescript/http-server-pool/fastify-worker_threads/@types/fastify/index.d.ts index 899aa1a0..e78ab0d9 100644 --- a/examples/typescript/http-server-pool/fastify-worker_threads/@types/fastify/index.d.ts +++ b/examples/typescript/http-server-pool/fastify-worker_threads/@types/fastify/index.d.ts @@ -1,17 +1,16 @@ -import type { TransferListItem } from 'node:worker_threads' - import type * as fastify from 'fastify' +import type { TransferListItem } from 'node:worker_threads' import type { DynamicThreadPool } from 'poolifier' import type { WorkerData, WorkerResponse } from '../../src/types.ts' declare module 'fastify' { export interface FastifyInstance extends fastify.FastifyInstance { - pool: DynamicThreadPool execute: ( data?: WorkerData, name?: string, transferList?: readonly TransferListItem[] ) => Promise + pool: DynamicThreadPool } } diff --git a/examples/typescript/http-server-pool/fastify-worker_threads/src/fastify-poolifier.ts b/examples/typescript/http-server-pool/fastify-worker_threads/src/fastify-poolifier.ts index 451fcb80..a9156566 100644 --- a/examples/typescript/http-server-pool/fastify-worker_threads/src/fastify-poolifier.ts +++ b/examples/typescript/http-server-pool/fastify-worker_threads/src/fastify-poolifier.ts @@ -1,6 +1,6 @@ +import type { FastifyPluginCallback } from 'fastify' import type { TransferListItem } from 'node:worker_threads' -import type { FastifyPluginCallback } from 'fastify' import fp from 'fastify-plugin' import { availableParallelism, DynamicThreadPool } from 'poolifier' @@ -17,12 +17,12 @@ const fastifyPoolifierPlugin: FastifyPluginCallback = ( ) => { options = { ...{ - minWorkers: 1, maxWorkers: availableParallelism(), + minWorkers: 1, }, ...options, } - const { workerFile, minWorkers, maxWorkers, ...poolOptions } = options + const { maxWorkers, minWorkers, workerFile, ...poolOptions } = options const pool = new DynamicThreadPool( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion minWorkers!, diff --git a/examples/typescript/http-server-pool/fastify-worker_threads/src/main.ts b/examples/typescript/http-server-pool/fastify-worker_threads/src/main.ts index d30e913c..34dda267 100644 --- a/examples/typescript/http-server-pool/fastify-worker_threads/src/main.ts +++ b/examples/typescript/http-server-pool/fastify-worker_threads/src/main.ts @@ -1,9 +1,8 @@ +import Fastify from 'fastify' import { dirname, extname, join } from 'node:path' import { exit } from 'node:process' import { fileURLToPath } from 'node:url' -import Fastify from 'fastify' - import { fastifyPoolifier } from './fastify-poolifier.js' /** @@ -21,14 +20,14 @@ const workerFile = join( ) await fastify.register(fastifyPoolifier, { - workerFile, enableTasksQueue: true, - tasksQueueOptions: { - concurrency: 8, - }, errorHandler: (e: Error) => { fastify.log.error('Thread worker error:', e) }, + tasksQueueOptions: { + concurrency: 8, + }, + workerFile, }) fastify.all('/api/echo', async request => { 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 97c2c540..255a0fcb 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 @@ -13,7 +13,7 @@ export interface WorkerResponse { } export interface FastifyPoolifierOptions extends ThreadPoolOptions { - workerFile: string - minWorkers?: number 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 c5819f83..3e8699bc 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,7 +6,7 @@ class RequestHandlerWorker< Data extends WorkerData, Response extends WorkerResponse > extends ThreadWorker { - private static readonly factorial: (n: number | bigint) => bigint = n => { + private static readonly factorial: (n: bigint | number) => bigint = n => { if (n === 0 || n === 1) { return 1n } else { diff --git a/examples/typescript/pool.ts b/examples/typescript/pool.ts index 1be0691a..c339b705 100644 --- a/examples/typescript/pool.ts +++ b/examples/typescript/pool.ts @@ -1,6 +1,5 @@ import { dirname, extname, join } from 'node:path' import { fileURLToPath } from 'node:url' - import { availableParallelism, DynamicThreadPool, @@ -18,12 +17,12 @@ const fixedPool = new FixedThreadPool( availableParallelism(), workerFile, { - onlineHandler: () => { - console.info('Worker is online') - }, errorHandler: (e: Error) => { console.error(e) }, + onlineHandler: () => { + console.info('Worker is online') + }, } ) @@ -34,12 +33,12 @@ const dynamicPool = new DynamicThreadPool( availableParallelism(), workerFile, { - onlineHandler: () => { - console.info('Worker is online') - }, errorHandler: (e: Error) => { console.error(e) }, + onlineHandler: () => { + console.info('Worker is online') + }, } ) diff --git a/examples/typescript/smtp-client-pool/src/main.ts b/examples/typescript/smtp-client-pool/src/main.ts index 4d1f2ea8..f9f4d78e 100644 --- a/examples/typescript/smtp-client-pool/src/main.ts +++ b/examples/typescript/smtp-client-pool/src/main.ts @@ -8,21 +8,21 @@ const smtpClientPoolPromises = new Set>() for (const to of tos) { smtpClientPoolPromises.add( smtpClientPool.execute({ - smtpTransport: { - host: 'smtp.domain.tld', - port: 465, - secure: true, - auth: { - user: 'REPLACE-WITH-YOUR-ALIAS@DOMAIN.TLD', - pass: 'REPLACE-WITH-YOUR-GENERATED-PASSWORD', - }, - }, mail: { from: '"Foo" ', - to, + html: 'Hello world?', subject: 'Hello', text: 'Hello world?', - html: 'Hello world?', + to, + }, + smtpTransport: { + auth: { + pass: 'REPLACE-WITH-YOUR-GENERATED-PASSWORD', + user: 'REPLACE-WITH-YOUR-ALIAS@DOMAIN.TLD', + }, + host: 'smtp.domain.tld', + port: 465, + secure: true, }, }) ) diff --git a/examples/typescript/smtp-client-pool/src/pool.ts b/examples/typescript/smtp-client-pool/src/pool.ts index 74783703..2e9f788b 100644 --- a/examples/typescript/smtp-client-pool/src/pool.ts +++ b/examples/typescript/smtp-client-pool/src/pool.ts @@ -1,7 +1,7 @@ +import type SMTPTransport from 'nodemailer/lib/smtp-transport/index.js' + import { dirname, extname, join } from 'node:path' import { fileURLToPath } from 'node:url' - -import type SMTPTransport from 'nodemailer/lib/smtp-transport/index.js' import { availableParallelism, DynamicThreadPool } from 'poolifier' import type { WorkerData } from './types.js' @@ -16,10 +16,10 @@ export const smtpClientPool = new DynamicThreadPool< SMTPTransport.SentMessageInfo >(0, availableParallelism(), workerFile, { enableTasksQueue: true, - tasksQueueOptions: { - concurrency: 8, - }, errorHandler: (e: Error) => { console.error('Thread worker error:', e) }, + tasksQueueOptions: { + concurrency: 8, + }, }) diff --git a/examples/typescript/smtp-client-pool/src/types.ts b/examples/typescript/smtp-client-pool/src/types.ts index 7287ad54..41396c87 100644 --- a/examples/typescript/smtp-client-pool/src/types.ts +++ b/examples/typescript/smtp-client-pool/src/types.ts @@ -2,6 +2,6 @@ import type Mail from 'nodemailer/lib/mailer/index.js' import type SMTPTransport from 'nodemailer/lib/smtp-transport/index.js' export interface WorkerData { - smtpTransport: SMTPTransport.Options mail: Mail.Options + smtpTransport: SMTPTransport.Options } diff --git a/examples/typescript/smtp-client-pool/src/worker.ts b/examples/typescript/smtp-client-pool/src/worker.ts index e2667b32..dd8f908b 100644 --- a/examples/typescript/smtp-client-pool/src/worker.ts +++ b/examples/typescript/smtp-client-pool/src/worker.ts @@ -1,5 +1,6 @@ -import { createTransport } from 'nodemailer' import type SMTPTransport from 'nodemailer/lib/smtp-transport/index.js' + +import { createTransport } from 'nodemailer' import { ThreadWorker } from 'poolifier' import type { WorkerData } from './types.js' diff --git a/examples/typescript/websocket-server-pool/ws-cluster/requests.js b/examples/typescript/websocket-server-pool/ws-cluster/requests.js index 2c73376f..f9825e2b 100644 --- a/examples/typescript/websocket-server-pool/ws-cluster/requests.js +++ b/examples/typescript/websocket-server-pool/ws-cluster/requests.js @@ -7,11 +7,11 @@ ws.on('error', console.error) ws.on('open', () => { for (let i = 0; i < 60; i++) { ws.send( - JSON.stringify({ type: 'echo', data: { key1: 'value1', key2: 'value2' } }) + JSON.stringify({ data: { key1: 'value1', key2: 'value2' }, type: 'echo' }) ) } for (let i = 0; i < 60; i++) { - ws.send(JSON.stringify({ type: 'factorial', data: { number: 50000 } })) + ws.send(JSON.stringify({ data: { number: 50000 }, type: 'factorial' })) } }) diff --git a/examples/typescript/websocket-server-pool/ws-cluster/rollup.config.ts b/examples/typescript/websocket-server-pool/ws-cluster/rollup.config.ts index 4080e1ac..ee1b4c2c 100644 --- a/examples/typescript/websocket-server-pool/ws-cluster/rollup.config.ts +++ b/examples/typescript/websocket-server-pool/ws-cluster/rollup.config.ts @@ -3,27 +3,27 @@ import { defineConfig } from 'rollup' import del from 'rollup-plugin-delete' export default defineConfig({ + external: [/^node:*/, 'poolifier', 'ws'], input: ['./src/main.ts', './src/worker.ts'], - strictDeprecations: true, output: [ { - format: 'cjs', + chunkFileNames: '[name]-[hash].cjs', dir: './dist', - sourcemap: true, entryFileNames: '[name].cjs', - chunkFileNames: '[name]-[hash].cjs', + format: 'cjs', + sourcemap: true, }, { - format: 'esm', dir: './dist', + format: 'esm', sourcemap: true, }, ], - external: [/^node:*/, 'poolifier', 'ws'], plugins: [ typescript(), del({ targets: ['./dist/*'], }), ], + strictDeprecations: true, }) diff --git a/examples/typescript/websocket-server-pool/ws-cluster/src/main.ts b/examples/typescript/websocket-server-pool/ws-cluster/src/main.ts index 1950fec3..654be2ba 100644 --- a/examples/typescript/websocket-server-pool/ws-cluster/src/main.ts +++ b/examples/typescript/websocket-server-pool/ws-cluster/src/main.ts @@ -1,6 +1,5 @@ import { dirname, extname, join } from 'node:path' import { fileURLToPath } from 'node:url' - import { availableParallelism, FixedClusterPool } from 'poolifier' import type { WorkerData, WorkerResponse } from './types.js' @@ -15,6 +14,9 @@ const pool = new FixedClusterPool( workerFile, { enableEvents: false, + errorHandler: (e: Error) => { + console.error('Cluster worker error', e) + }, onlineHandler: () => { pool .execute({ port: 8080 }) @@ -33,8 +35,5 @@ const pool = new FixedClusterPool( ) }) }, - errorHandler: (e: Error) => { - console.error('Cluster worker error', e) - }, } ) 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 922914a1..851e47e6 100644 --- a/examples/typescript/websocket-server-pool/ws-cluster/src/types.ts +++ b/examples/typescript/websocket-server-pool/ws-cluster/src/types.ts @@ -4,8 +4,8 @@ export enum MessageType { } export interface MessagePayload { - type: MessageType data: T + type: MessageType } export interface DataPayload { @@ -17,6 +17,6 @@ export interface WorkerData { } export interface WorkerResponse { - status: boolean port?: number + status: boolean } 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 c1db83da..a5577d7a 100644 --- a/examples/typescript/websocket-server-pool/ws-cluster/src/worker.ts +++ b/examples/typescript/websocket-server-pool/ws-cluster/src/worker.ts @@ -10,9 +10,7 @@ import { } from './types.js' class WebSocketServerWorker extends ClusterWorker { - private static wss: WebSocketServer - - private static readonly factorial = (n: number | bigint): bigint => { + private static readonly factorial = (n: bigint | number): bigint => { if (n === 0 || n === 1) { return 1n } else { @@ -40,15 +38,15 @@ class WebSocketServerWorker extends ClusterWorker { WebSocketServerWorker.wss.on('connection', ws => { ws.on('error', console.error) ws.on('message', (message: RawData) => { - const { type, data } = JSON.parse( + const { data, type } = JSON.parse( message.toString() ) as MessagePayload switch (type) { case MessageType.echo: ws.send( JSON.stringify({ - type: MessageType.echo, data, + type: MessageType.echo, }) ) break @@ -56,11 +54,11 @@ class WebSocketServerWorker extends ClusterWorker { ws.send( JSON.stringify( { - type: MessageType.factorial, data: { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion number: WebSocketServerWorker.factorial(data.number!), }, + type: MessageType.factorial, }, (_, v: unknown) => (typeof v === 'bigint' ? v.toString() : v) ) @@ -70,11 +68,13 @@ class WebSocketServerWorker extends ClusterWorker { }) }) return { - status: true, port: WebSocketServerWorker.wss.options.port, + status: true, } } + private static wss: WebSocketServer + public constructor () { super(WebSocketServerWorker.startWebSocketServer, { killHandler: () => { diff --git a/examples/typescript/websocket-server-pool/ws-hybrid/requests.js b/examples/typescript/websocket-server-pool/ws-hybrid/requests.js index 2c73376f..f9825e2b 100644 --- a/examples/typescript/websocket-server-pool/ws-hybrid/requests.js +++ b/examples/typescript/websocket-server-pool/ws-hybrid/requests.js @@ -7,11 +7,11 @@ ws.on('error', console.error) ws.on('open', () => { for (let i = 0; i < 60; i++) { ws.send( - JSON.stringify({ type: 'echo', data: { key1: 'value1', key2: 'value2' } }) + JSON.stringify({ data: { key1: 'value1', key2: 'value2' }, type: 'echo' }) ) } for (let i = 0; i < 60; i++) { - ws.send(JSON.stringify({ type: 'factorial', data: { number: 50000 } })) + ws.send(JSON.stringify({ data: { number: 50000 }, type: 'factorial' })) } }) diff --git a/examples/typescript/websocket-server-pool/ws-hybrid/rollup.config.ts b/examples/typescript/websocket-server-pool/ws-hybrid/rollup.config.ts index fb878235..aa309de8 100644 --- a/examples/typescript/websocket-server-pool/ws-hybrid/rollup.config.ts +++ b/examples/typescript/websocket-server-pool/ws-hybrid/rollup.config.ts @@ -3,31 +3,31 @@ import { defineConfig } from 'rollup' import del from 'rollup-plugin-delete' export default defineConfig({ + external: [/^node:*/, 'poolifier', 'ws'], input: [ './src/main.ts', './src/websocket-server-worker.ts', './src/request-handler-worker.ts', ], - strictDeprecations: true, output: [ { - format: 'cjs', + chunkFileNames: '[name]-[hash].cjs', dir: './dist', - sourcemap: true, entryFileNames: '[name].cjs', - chunkFileNames: '[name]-[hash].cjs', + format: 'cjs', + sourcemap: true, }, { - format: 'esm', dir: './dist', + format: 'esm', sourcemap: true, }, ], - external: [/^node:*/, 'poolifier', 'ws'], plugins: [ typescript(), del({ targets: ['./dist/*'], }), ], + strictDeprecations: true, }) diff --git a/examples/typescript/websocket-server-pool/ws-hybrid/src/main.ts b/examples/typescript/websocket-server-pool/ws-hybrid/src/main.ts index 005680f5..13f78249 100644 --- a/examples/typescript/websocket-server-pool/ws-hybrid/src/main.ts +++ b/examples/typescript/websocket-server-pool/ws-hybrid/src/main.ts @@ -1,6 +1,5 @@ import { dirname, extname, join } from 'node:path' import { fileURLToPath } from 'node:url' - import { availableParallelism, FixedClusterPool } from 'poolifier' import type { ClusterWorkerData, ClusterWorkerResponse } from './types.js' @@ -20,22 +19,25 @@ const pool = new FixedClusterPool( webSocketServerWorkerFile, { enableEvents: false, + errorHandler: (e: Error) => { + console.error('Cluster worker error', e) + }, onlineHandler: () => { pool .execute({ - port: 8080, + enableTasksQueue: true, + errorHandler: (e: Error) => { + console.error('Thread worker error:', e) + }, maxWorkers: Math.round(availableParallelism() / 4) < 1 ? 1 : Math.round(availableParallelism() / 4), - workerFile: requestHandlerWorkerFile, - enableTasksQueue: true, + port: 8080, tasksQueueOptions: { concurrency: 8, }, - errorHandler: (e: Error) => { - console.error('Thread worker error:', e) - }, + workerFile: requestHandlerWorkerFile, }) .then(response => { if (response.status) { @@ -52,8 +54,5 @@ const pool = new FixedClusterPool( ) }) }, - errorHandler: (e: Error) => { - console.error('Cluster worker error', e) - }, } ) 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 444c5179..539a03ff 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,7 +10,7 @@ class RequestHandlerWorker< Data extends ThreadWorkerData, Response extends ThreadWorkerResponse > extends ThreadWorker { - private static readonly factorial = (n: number | bigint): bigint => { + private static readonly factorial = (n: bigint | number): bigint => { if (n === 0 || n === 1) { return 1n } else { 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 d52dbf23..80c01ece 100644 --- a/examples/typescript/websocket-server-pool/ws-hybrid/src/types.ts +++ b/examples/typescript/websocket-server-pool/ws-hybrid/src/types.ts @@ -6,8 +6,8 @@ export enum MessageType { } export interface MessagePayload { - type: MessageType data: T + type: MessageType } export interface DataPayload { @@ -15,15 +15,15 @@ export interface DataPayload { } export interface ClusterWorkerData extends ThreadPoolOptions { + maxWorkers?: number + minWorkers?: number port: number workerFile: string - minWorkers?: number - maxWorkers?: number } export interface ClusterWorkerResponse { - status: boolean port?: number + status: boolean } export interface ThreadWorkerData { 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 53d71df4..68cbdf2a 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 @@ -23,7 +23,6 @@ class WebSocketServerWorker extends ClusterWorker< ClusterWorkerData, ClusterWorkerResponse > { - private static wss: WebSocketServer private static requestHandlerPool: DynamicThreadPool< ThreadWorkerData, ThreadWorkerResponse @@ -32,7 +31,7 @@ class WebSocketServerWorker extends ClusterWorker< private static readonly startWebSocketServer = ( workerData?: ClusterWorkerData ): ClusterWorkerResponse => { - const { port, workerFile, minWorkers, maxWorkers, ...poolOptions } = + const { maxWorkers, minWorkers, port, workerFile, ...poolOptions } = // eslint-disable-next-line @typescript-eslint/no-non-null-assertion workerData! @@ -55,7 +54,7 @@ class WebSocketServerWorker extends ClusterWorker< WebSocketServerWorker.wss.on('connection', ws => { ws.on('error', console.error) ws.on('message', (message: RawData) => { - const { type, data } = JSON.parse( + const { data, type } = JSON.parse( message.toString() ) as MessagePayload switch (type) { @@ -65,8 +64,8 @@ class WebSocketServerWorker extends ClusterWorker< .then(response => { ws.send( JSON.stringify({ - type: MessageType.echo, data: response.data, + type: MessageType.echo, }) ) return undefined @@ -80,8 +79,8 @@ class WebSocketServerWorker extends ClusterWorker< ws.send( JSON.stringify( { - type: MessageType.factorial, data: response.data, + type: MessageType.factorial, }, (_, v: unknown) => typeof v === 'bigint' ? v.toString() : v @@ -95,11 +94,13 @@ class WebSocketServerWorker extends ClusterWorker< }) }) return { - status: true, port: WebSocketServerWorker.wss.options.port, + status: true, } } + private static wss: WebSocketServer + public constructor () { super(WebSocketServerWorker.startWebSocketServer, { killHandler: async () => { diff --git a/examples/typescript/websocket-server-pool/ws-worker_threads/requests.js b/examples/typescript/websocket-server-pool/ws-worker_threads/requests.js index 2c73376f..f9825e2b 100644 --- a/examples/typescript/websocket-server-pool/ws-worker_threads/requests.js +++ b/examples/typescript/websocket-server-pool/ws-worker_threads/requests.js @@ -7,11 +7,11 @@ ws.on('error', console.error) ws.on('open', () => { for (let i = 0; i < 60; i++) { ws.send( - JSON.stringify({ type: 'echo', data: { key1: 'value1', key2: 'value2' } }) + JSON.stringify({ data: { key1: 'value1', key2: 'value2' }, type: 'echo' }) ) } for (let i = 0; i < 60; i++) { - ws.send(JSON.stringify({ type: 'factorial', data: { number: 50000 } })) + ws.send(JSON.stringify({ data: { number: 50000 }, type: 'factorial' })) } }) diff --git a/examples/typescript/websocket-server-pool/ws-worker_threads/src/main.ts b/examples/typescript/websocket-server-pool/ws-worker_threads/src/main.ts index 0a308e98..38ea9d95 100644 --- a/examples/typescript/websocket-server-pool/ws-worker_threads/src/main.ts +++ b/examples/typescript/websocket-server-pool/ws-worker_threads/src/main.ts @@ -17,7 +17,7 @@ const emptyFunction = (): void => { wss.on('connection', ws => { ws.on('error', console.error) ws.on('message', (message: RawData) => { - const { type, data } = JSON.parse( + const { data, type } = JSON.parse( message.toString() ) as MessagePayload switch (type) { @@ -27,8 +27,8 @@ wss.on('connection', ws => { .then(response => { ws.send( JSON.stringify({ - type: MessageType.echo, data: response.data, + type: MessageType.echo, }) ) return undefined @@ -42,8 +42,8 @@ wss.on('connection', ws => { ws.send( JSON.stringify( { - type: MessageType.factorial, data: response.data, + type: MessageType.factorial, }, (_, v: unknown) => (typeof v === 'bigint' ? v.toString() : v) ) diff --git a/examples/typescript/websocket-server-pool/ws-worker_threads/src/pool.ts b/examples/typescript/websocket-server-pool/ws-worker_threads/src/pool.ts index 1cefa60c..e88a5181 100644 --- a/examples/typescript/websocket-server-pool/ws-worker_threads/src/pool.ts +++ b/examples/typescript/websocket-server-pool/ws-worker_threads/src/pool.ts @@ -1,6 +1,5 @@ import { dirname, extname, join } from 'node:path' import { fileURLToPath } from 'node:url' - import { availableParallelism, DynamicThreadPool } from 'poolifier' import type { DataPayload, WorkerData, WorkerResponse } from './types.js' @@ -15,10 +14,10 @@ export const requestHandlerPool = new DynamicThreadPool< WorkerResponse >(1, availableParallelism(), workerFile, { enableTasksQueue: true, - tasksQueueOptions: { - concurrency: 8, - }, errorHandler: (e: Error) => { console.error('Thread worker error:', e) }, + tasksQueueOptions: { + concurrency: 8, + }, }) 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 4f821e67..a930cdbb 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 @@ -4,8 +4,8 @@ export enum MessageType { } export interface MessagePayload { - type: MessageType data: T + type: MessageType } export interface DataPayload { 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 2c4de3e2..57506690 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,7 +6,7 @@ class RequestHandlerWorker< Data extends WorkerData, Response extends WorkerResponse > extends ThreadWorker { - private static readonly factorial = (n: number | bigint): bigint => { + private static readonly factorial = (n: bigint | number): bigint => { if (n === 0 || n === 1) { return 1n } else { diff --git a/examples/typescript/worker.ts b/examples/typescript/worker.ts index 7ab1fc56..989e2f4f 100644 --- a/examples/typescript/worker.ts +++ b/examples/typescript/worker.ts @@ -5,8 +5,8 @@ export interface MyData { } export interface MyResponse { - message: string data?: MyData + message: string } class MyThreadWorker extends ThreadWorker { @@ -19,7 +19,7 @@ class MyThreadWorker extends ThreadWorker { private async process (data?: MyData): Promise { return await new Promise(resolve => { setTimeout(() => { - resolve({ message: 'Hello from Worker :)', data }) + resolve({ data, message: 'Hello from Worker :)' }) }, 1000) }) } diff --git a/package.json b/package.json index a4d52aaf..59c798ab 100644 --- a/package.json +++ b/package.json @@ -107,13 +107,13 @@ "@eslint/js": "^9.9.0", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^11.1.6", - "@types/node": "^22.4.1", + "@types/node": "^22.5.0", "c8": "^10.1.2", "cross-env": "^7.0.3", "eslint": "^9.9.0", "eslint-define-config": "^2.1.0", "eslint-plugin-jsdoc": "^50.2.2", - "eslint-plugin-simple-import-sort": "^12.1.1", + "eslint-plugin-perfectionist": "^3.2.0", "expect": "^29.7.0", "globals": "^15.9.0", "husky": "^9.1.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 154ea75e..e8b476d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,7 +16,7 @@ importers: version: 1.8.3 '@commitlint/cli': specifier: ^19.4.0 - version: 19.4.0(@types/node@22.4.1)(typescript@5.5.4) + version: 19.4.0(@types/node@22.5.0)(typescript@5.5.4) '@commitlint/config-conventional': specifier: ^19.2.2 version: 19.2.2 @@ -33,8 +33,8 @@ importers: specifier: ^11.1.6 version: 11.1.6(rollup@4.21.0)(tslib@2.6.3)(typescript@5.5.4) '@types/node': - specifier: ^22.4.1 - version: 22.4.1 + specifier: ^22.5.0 + version: 22.5.0 c8: specifier: ^10.1.2 version: 10.1.2 @@ -50,9 +50,9 @@ importers: eslint-plugin-jsdoc: specifier: ^50.2.2 version: 50.2.2(eslint@9.9.0(jiti@1.21.6)) - eslint-plugin-simple-import-sort: - specifier: ^12.1.1 - version: 12.1.1(eslint@9.9.0(jiti@1.21.6)) + eslint-plugin-perfectionist: + specifier: ^3.2.0 + version: 3.2.0(eslint@9.9.0(jiti@1.21.6))(typescript@5.5.4) expect: specifier: ^29.7.0 version: 29.7.0 @@ -276,8 +276,8 @@ packages: '@cspell/dict-companies@3.1.4': resolution: {integrity: sha512-y9e0amzEK36EiiKx3VAA+SHQJPpf2Qv5cCt5eTUSggpTkiFkCh6gRKQ97rVlrKh5GJrqinDwYIJtTsxuh2vy2Q==} - '@cspell/dict-cpp@5.1.12': - resolution: {integrity: sha512-6lXLOFIa+k/qBcu0bjaE/Kc6v3sh9VhsDOXD1Dalm3zgd0QIMjp5XBmkpSdCAK3pWCPV0Se7ysVLDfCea1BuXg==} + '@cspell/dict-cpp@5.1.14': + resolution: {integrity: sha512-DxmlkwDhfPvA2fcYHg46Ly84E3BWwKt2mxUZ41pdgqmzPXdsvoAXbTAgXsRHuHfoHzD6hB1xai9z2JYHeiMqKQ==} '@cspell/dict-cryptocurrencies@5.0.0': resolution: {integrity: sha512-Z4ARIw5+bvmShL+4ZrhDzGhnc9znaAGHOEMaB/GURdS/jdoreEDY34wdN0NtdLHDO5KO7GduZnZyqGdRoiSmYA==} @@ -300,8 +300,8 @@ packages: '@cspell/dict-docker@1.1.7': resolution: {integrity: sha512-XlXHAr822euV36GGsl2J1CkBIVg3fZ6879ZOg5dxTIssuhUOCiV2BuzKZmt6aIFmcdPmR14+9i9Xq+3zuxeX0A==} - '@cspell/dict-dotnet@5.0.2': - resolution: {integrity: sha512-UD/pO2A2zia/YZJ8Kck/F6YyDSpCMq0YvItpd4YbtDVzPREfTZ48FjZsbYi4Jhzwfvc6o8R56JusAE58P+4sNQ==} + '@cspell/dict-dotnet@5.0.3': + resolution: {integrity: sha512-q8+b8YWYv+9Q+AbU3mH/RHE9aovhCuGtMuNSsx+YnTofEhVQkJR3vdrYjhOBg3epIiZVUS83VP0vxPLPa+UTug==} '@cspell/dict-elixir@4.0.3': resolution: {integrity: sha512-g+uKLWvOp9IEZvrIvBPTr/oaO6619uH/wyqypqvwpmnmpjcfi8+/hqZH8YNKt15oviK8k4CkINIqNhyndG9d9Q==} @@ -333,8 +333,8 @@ packages: '@cspell/dict-git@3.0.0': resolution: {integrity: sha512-simGS/lIiXbEaqJu9E2VPoYW1OTC2xrwPPXNXFMa2uo/50av56qOuaxDrZ5eH1LidFXwoc8HROCHYeKoNrDLSw==} - '@cspell/dict-golang@6.0.9': - resolution: {integrity: sha512-etDt2WQauyEQDA+qPS5QtkYTb2I9l5IfQftAllVoB1aOrT6bxxpHvMEpJ0Hsn/vezxrCqa/BmtUbRxllIxIuSg==} + '@cspell/dict-golang@6.0.11': + resolution: {integrity: sha512-BMFIDGh1HaFUe1cYBT1dotqyIQG2j3VkNntGQTBa/7i0aBnC5PBJDiAXnUeBHi0AVrz0hyAc7xtcK5KyKCEzwg==} '@cspell/dict-google@1.0.1': resolution: {integrity: sha512-dQr4M3n95uOhtloNSgB9tYYGXGGEGEykkFyRtfcp5pFuEecYUa0BSgtlGKx9RXVtJtKgR+yFT/a5uQSlt8WjqQ==} @@ -384,8 +384,8 @@ packages: '@cspell/dict-powershell@5.0.5': resolution: {integrity: sha512-3JVyvMoDJesAATYGOxcUWPbQPUvpZmkinV3m8HL1w1RrjeMVXXuK7U1jhopSneBtLhkU+9HKFwgh9l9xL9mY2Q==} - '@cspell/dict-public-licenses@2.0.7': - resolution: {integrity: sha512-KlBXuGcN3LE7tQi/GEqKiDewWGGuopiAD0zRK1QilOx5Co8XAvs044gk4MNIQftc8r0nHeUI+irJKLGcR36DIQ==} + '@cspell/dict-public-licenses@2.0.8': + resolution: {integrity: sha512-Sup+tFS7cDV0fgpoKtUqEZ6+fA/H+XUgBiqQ/Fbs6vUE3WCjJHOIVsP+udHuyMH7iBfJ4UFYOYeORcY4EaKdMg==} '@cspell/dict-python@4.2.4': resolution: {integrity: sha512-sCtLBqMreb+8zRW2bXvFsfSnRUVU6IFm4mT6Dc4xbz0YajprbaPPh/kOUTw5IJRP8Uh+FFb7Xp2iH03CNWRq/A==} @@ -402,8 +402,8 @@ packages: '@cspell/dict-scala@5.0.3': resolution: {integrity: sha512-4yGb4AInT99rqprxVNT9TYb1YSpq58Owzq7zi3ZS5T0u899Y4VsxsBiOgHnQ/4W+ygi+sp+oqef8w8nABR2lkg==} - '@cspell/dict-software-terms@4.0.6': - resolution: {integrity: sha512-UDhUzNSf7GN529a0Ip9hlSoGbpscz0YlUYBEJmZBXi8otpkrbCJqs50T74Ppd+SWqNil04De8urv4af2c6SY5Q==} + '@cspell/dict-software-terms@4.0.9': + resolution: {integrity: sha512-zh68RM83efPenrH0n/QLU5OwIg5fgJDxJiIo4ThQUgvaxihwd4R/iFVCDNJTdtjbn5eHqkjVXj8f5EKc3Y0OLA==} '@cspell/dict-sql@2.1.5': resolution: {integrity: sha512-FmxanytHXss7GAWAXmgaxl3icTCW7YxlimyOSPNfm+njqeUDjw3kEv4mFNDDObBJv8Ec5AWCbUDkWIpkE3IpKg==} @@ -738,8 +738,8 @@ packages: '@types/minimatch@5.1.2': resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} - '@types/node@22.4.1': - resolution: {integrity: sha512-1tbpb9325+gPnKK0dMm+/LMriX0vKxf6RnB0SZUqfyVkQ4fMgUSySqhxE/y8Jvs4NyF1yHzTfG9KlnkIODxPKg==} + '@types/node@22.5.0': + resolution: {integrity: sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==} '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -1301,6 +1301,25 @@ packages: peerDependencies: eslint: '>=8.23.0' + eslint-plugin-perfectionist@3.2.0: + resolution: {integrity: sha512-cX1aztMbSfRWPKJH8CD+gadrbkS+RNH1OGWuNGws8J6rHzYYhawxWTU/yzMYjq2IRJCpBCfhgfa7BHRXQYxLHA==} + 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.0 + 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==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1313,11 +1332,6 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - eslint-plugin-simple-import-sort@12.1.1: - resolution: {integrity: sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==} - peerDependencies: - eslint: '>=5.0.0' - eslint-scope@8.0.2: resolution: {integrity: sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1997,6 +2011,10 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} + minimatch@10.0.1: + resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} + engines: {node: 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -2035,6 +2053,9 @@ 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==} @@ -2401,8 +2422,8 @@ packages: spdx-expression-parse@4.0.0: resolution: {integrity: sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==} - spdx-license-ids@3.0.18: - resolution: {integrity: sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==} + spdx-license-ids@3.0.20: + resolution: {integrity: sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==} split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} @@ -2753,11 +2774,11 @@ snapshots: '@biomejs/cli-win32-x64@1.8.3': optional: true - '@commitlint/cli@19.4.0(@types/node@22.4.1)(typescript@5.5.4)': + '@commitlint/cli@19.4.0(@types/node@22.5.0)(typescript@5.5.4)': dependencies: '@commitlint/format': 19.3.0 '@commitlint/lint': 19.2.2 - '@commitlint/load': 19.4.0(@types/node@22.4.1)(typescript@5.5.4) + '@commitlint/load': 19.4.0(@types/node@22.5.0)(typescript@5.5.4) '@commitlint/read': 19.4.0 '@commitlint/types': 19.0.3 execa: 8.0.1 @@ -2804,7 +2825,7 @@ snapshots: '@commitlint/rules': 19.0.3 '@commitlint/types': 19.0.3 - '@commitlint/load@19.4.0(@types/node@22.4.1)(typescript@5.5.4)': + '@commitlint/load@19.4.0(@types/node@22.5.0)(typescript@5.5.4)': dependencies: '@commitlint/config-validator': 19.0.3 '@commitlint/execute-rule': 19.0.0 @@ -2812,7 +2833,7 @@ snapshots: '@commitlint/types': 19.0.3 chalk: 5.3.0 cosmiconfig: 9.0.0(typescript@5.5.4) - cosmiconfig-typescript-loader: 5.0.0(@types/node@22.4.1)(cosmiconfig@9.0.0(typescript@5.5.4))(typescript@5.5.4) + cosmiconfig-typescript-loader: 5.0.0(@types/node@22.5.0)(cosmiconfig@9.0.0(typescript@5.5.4))(typescript@5.5.4) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 @@ -2870,14 +2891,14 @@ snapshots: '@cspell/dict-aws': 4.0.3 '@cspell/dict-bash': 4.1.3 '@cspell/dict-companies': 3.1.4 - '@cspell/dict-cpp': 5.1.12 + '@cspell/dict-cpp': 5.1.14 '@cspell/dict-cryptocurrencies': 5.0.0 '@cspell/dict-csharp': 4.0.2 '@cspell/dict-css': 4.0.13 '@cspell/dict-dart': 2.0.3 '@cspell/dict-django': 4.1.0 '@cspell/dict-docker': 1.1.7 - '@cspell/dict-dotnet': 5.0.2 + '@cspell/dict-dotnet': 5.0.3 '@cspell/dict-elixir': 4.0.3 '@cspell/dict-en-common-misspellings': 2.0.4 '@cspell/dict-en-gb': 1.1.33 @@ -2888,7 +2909,7 @@ snapshots: '@cspell/dict-fullstack': 3.2.0 '@cspell/dict-gaming-terms': 1.0.5 '@cspell/dict-git': 3.0.0 - '@cspell/dict-golang': 6.0.9 + '@cspell/dict-golang': 6.0.11 '@cspell/dict-google': 1.0.1 '@cspell/dict-haskell': 4.0.1 '@cspell/dict-html': 4.0.5 @@ -2905,13 +2926,13 @@ snapshots: '@cspell/dict-npm': 5.0.18 '@cspell/dict-php': 4.0.8 '@cspell/dict-powershell': 5.0.5 - '@cspell/dict-public-licenses': 2.0.7 + '@cspell/dict-public-licenses': 2.0.8 '@cspell/dict-python': 4.2.4 '@cspell/dict-r': 2.0.1 '@cspell/dict-ruby': 5.0.2 '@cspell/dict-rust': 4.0.5 '@cspell/dict-scala': 5.0.3 - '@cspell/dict-software-terms': 4.0.6 + '@cspell/dict-software-terms': 4.0.9 '@cspell/dict-sql': 2.1.5 '@cspell/dict-svelte': 1.0.2 '@cspell/dict-swift': 2.0.1 @@ -2937,7 +2958,7 @@ snapshots: '@cspell/dict-companies@3.1.4': {} - '@cspell/dict-cpp@5.1.12': {} + '@cspell/dict-cpp@5.1.14': {} '@cspell/dict-cryptocurrencies@5.0.0': {} @@ -2953,7 +2974,7 @@ snapshots: '@cspell/dict-docker@1.1.7': {} - '@cspell/dict-dotnet@5.0.2': {} + '@cspell/dict-dotnet@5.0.3': {} '@cspell/dict-elixir@4.0.3': {} @@ -2975,7 +2996,7 @@ snapshots: '@cspell/dict-git@3.0.0': {} - '@cspell/dict-golang@6.0.9': {} + '@cspell/dict-golang@6.0.11': {} '@cspell/dict-google@1.0.1': {} @@ -3009,7 +3030,7 @@ snapshots: '@cspell/dict-powershell@5.0.5': {} - '@cspell/dict-public-licenses@2.0.7': {} + '@cspell/dict-public-licenses@2.0.8': {} '@cspell/dict-python@4.2.4': dependencies: @@ -3023,7 +3044,7 @@ snapshots: '@cspell/dict-scala@5.0.3': {} - '@cspell/dict-software-terms@4.0.6': {} + '@cspell/dict-software-terms@4.0.9': {} '@cspell/dict-sql@2.1.5': {} @@ -3124,7 +3145,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.4.1 + '@types/node': 22.5.0 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -3313,7 +3334,7 @@ snapshots: '@types/conventional-commits-parser@5.0.0': dependencies: - '@types/node': 22.4.1 + '@types/node': 22.5.0 '@types/eslint@9.6.0': dependencies: @@ -3325,7 +3346,7 @@ snapshots: '@types/glob@7.2.0': dependencies: '@types/minimatch': 5.1.2 - '@types/node': 22.4.1 + '@types/node': 22.5.0 '@types/hast@3.0.4': dependencies: @@ -3345,7 +3366,7 @@ snapshots: '@types/minimatch@5.1.2': {} - '@types/node@22.4.1': + '@types/node@22.5.0': dependencies: undici-types: 6.19.8 @@ -3723,9 +3744,9 @@ snapshots: core-util-is@1.0.3: {} - cosmiconfig-typescript-loader@5.0.0(@types/node@22.4.1)(cosmiconfig@9.0.0(typescript@5.5.4))(typescript@5.5.4): + cosmiconfig-typescript-loader@5.0.0(@types/node@22.5.0)(cosmiconfig@9.0.0(typescript@5.5.4))(typescript@5.5.4): dependencies: - '@types/node': 22.4.1 + '@types/node': 22.5.0 cosmiconfig: 9.0.0(typescript@5.5.4) jiti: 1.21.6 typescript: 5.5.4 @@ -4053,6 +4074,17 @@ snapshots: minimatch: 9.0.5 semver: 7.6.3 + eslint-plugin-perfectionist@3.2.0(eslint@9.9.0(jiti@1.21.6))(typescript@5.5.4): + dependencies: + '@typescript-eslint/types': 8.2.0 + '@typescript-eslint/utils': 8.2.0(eslint@9.9.0(jiti@1.21.6))(typescript@5.5.4) + eslint: 9.9.0(jiti@1.21.6) + minimatch: 10.0.1 + natural-compare-lite: 1.4.0 + transitivePeerDependencies: + - supports-color + - typescript + eslint-plugin-promise@7.1.0(eslint@9.9.0(jiti@1.21.6)): dependencies: eslint: 9.9.0(jiti@1.21.6) @@ -4079,10 +4111,6 @@ snapshots: string.prototype.matchall: 4.0.11 string.prototype.repeat: 1.0.0 - eslint-plugin-simple-import-sort@12.1.1(eslint@9.9.0(jiti@1.21.6)): - dependencies: - eslint: 9.9.0(jiti@1.21.6) - eslint-scope@8.0.2: dependencies: esrecurse: 4.3.0 @@ -4609,7 +4637,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.4.1 + '@types/node': 22.5.0 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -4786,6 +4814,10 @@ snapshots: mimic-function@5.0.1: {} + minimatch@10.0.1: + dependencies: + brace-expansion: 2.0.1 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -4858,6 +4890,8 @@ snapshots: ms@2.1.3: {} + natural-compare-lite@1.4.0: {} + natural-compare@1.4.0: {} neostandard@0.11.3(eslint@9.9.0(jiti@1.21.6))(typescript@5.5.4): @@ -5245,9 +5279,9 @@ snapshots: spdx-expression-parse@4.0.0: dependencies: spdx-exceptions: 2.5.0 - spdx-license-ids: 3.0.18 + spdx-license-ids: 3.0.20 - spdx-license-ids@3.0.18: {} + spdx-license-ids@3.0.20: {} split2@4.2.0: {} diff --git a/rollup.config.mjs b/rollup.config.mjs index a6d7eae8..826fba1e 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -1,8 +1,7 @@ -import * as os from 'node:os' -import { env } from 'node:process' - import terser from '@rollup/plugin-terser' import typescript from '@rollup/plugin-typescript' +import * as os from 'node:os' +import { env } from 'node:process' import { defineConfig } from 'rollup' import analyze from 'rollup-plugin-analyzer' import command from 'rollup-plugin-command' @@ -32,16 +31,16 @@ const maxWorkers = Math.floor(availableParallelism() / 2) export default defineConfig([ { + external: [/^node:*/], input: './src/index.ts', - strictDeprecations: true, output: [ { format: 'cjs', ...(isDevelopmentBuild ? { + chunkFileNames: '[name]-[hash].cjs', dir: './lib', entryFileNames: '[name].cjs', - chunkFileNames: '[name]-[hash].cjs', preserveModules: true, preserveModulesRoot: './src', } @@ -57,9 +56,9 @@ export default defineConfig([ format: 'esm', ...(isDevelopmentBuild ? { + chunkFileNames: '[name]-[hash].mjs', dir: './lib', entryFileNames: '[name].mjs', - chunkFileNames: '[name]-[hash].mjs', preserveModules: true, preserveModulesRoot: './src', } @@ -72,13 +71,12 @@ export default defineConfig([ }), }, ], - external: [/^node:*/], plugins: [ typescript({ - tsconfig: './tsconfig.build.json', compilerOptions: { sourceMap: sourcemap, }, + tsconfig: './tsconfig.build.json', }), del({ targets: ['./lib/*'], @@ -86,19 +84,20 @@ export default defineConfig([ isAnalyzeBuild && analyze(), isDocumentationBuild && command('pnpm typedoc'), ], + strictDeprecations: true, }, { - input: './lib/dts/index.d.ts', - strictDeprecations: true, - output: [{ format: 'esm', file: './lib/index.d.ts' }], external: [/^node:*/], + input: './lib/dts/index.d.ts', + output: [{ file: './lib/index.d.ts', format: 'esm' }], plugins: [ dts(), del({ - targets: ['./lib/dts'], hook: 'buildEnd', + targets: ['./lib/dts'], }), isAnalyzeBuild && analyze(), ], + strictDeprecations: true, }, ]) diff --git a/src/circular-buffer.ts b/src/circular-buffer.ts index 5015483b..803af129 100644 --- a/src/circular-buffer.ts +++ b/src/circular-buffer.ts @@ -8,10 +8,10 @@ export const defaultBufferSize = 2048 * @internal */ export class CircularBuffer { - private readIdx: number - private writeIdx: number private readonly items: Float32Array private readonly maxArrayIdx: number + private readIdx: number + private writeIdx: number public size: number /** @@ -27,6 +27,23 @@ 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. @@ -43,18 +60,6 @@ export class CircularBuffer { return this.size === this.items.length } - /** - * Puts number into buffer. - * @param number - Number to put into buffer. - */ - public put (number: number): void { - this.items[this.writeIdx] = number - this.writeIdx = this.writeIdx === this.maxArrayIdx ? 0 : this.writeIdx + 1 - if (this.size < this.items.length) { - ++this.size - } - } - /** * Gets number from buffer. * @returns Number from buffer. @@ -71,27 +76,22 @@ export class CircularBuffer { } /** - * Returns buffer as numbers' array. - * @returns Numbers' array. + * Puts number into buffer. + * @param number - Number to put into buffer. */ - public toArray (): number[] { - return Array.from(this.items.filter(item => item !== -1)) + public put (number: number): void { + this.items[this.writeIdx] = number + this.writeIdx = this.writeIdx === this.maxArrayIdx ? 0 : this.writeIdx + 1 + if (this.size < this.items.length) { + ++this.size + } } /** - * Checks the buffer size. - * @param size - Buffer size. + * Returns buffer as numbers' array. + * @returns Numbers' array. */ - 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` - ) - } + public toArray (): number[] { + return Array.from(this.items.filter(item => item !== -1)) } } diff --git a/src/pools/abstract-pool.ts b/src/pools/abstract-pool.ts index cd55b7ff..f04f03f8 100644 --- a/src/pools/abstract-pool.ts +++ b/src/pools/abstract-pool.ts @@ -1,16 +1,29 @@ +import type { TransferListItem } from 'node:worker_threads' + import { AsyncResource } from 'node:async_hooks' import { randomUUID } from 'node:crypto' import { EventEmitterAsyncResource } from 'node:events' import { performance } from 'node:perf_hooks' -import type { TransferListItem } from 'node:worker_threads' -import { defaultBucketSize } from '../queues/queue-types.js' import type { MessageValue, PromiseResponseWrapper, Task, TaskFunctionProperties, } from '../utility-types.js' +import type { + TaskFunction, + TaskFunctionObject, +} from '../worker/task-functions.js' +import type { + IWorker, + IWorkerNode, + WorkerInfo, + WorkerNodeEventDetail, + WorkerType, +} from './worker.js' + +import { defaultBucketSize } from '../queues/queue-types.js' import { average, buildTaskFunctionProperties, @@ -25,10 +38,6 @@ import { round, sleep, } from '../utils.js' -import type { - TaskFunction, - TaskFunctionObject, -} from '../worker/task-functions.js' import { KillBehaviors } from '../worker/worker-options.js' import { type IPool, @@ -59,13 +68,6 @@ import { waitWorkerNodeEvents, } from './utils.js' import { version } from './version.js' -import type { - IWorker, - IWorkerNode, - WorkerInfo, - WorkerNodeEventDetail, - WorkerType, -} from './worker.js' import { WorkerNode } from './worker-node.js' /** @@ -79,12 +81,6 @@ export abstract class AbstractPool< Data = unknown, Response = unknown > implements IPool { - /** @inheritDoc */ - public readonly workerNodes: IWorkerNode[] = [] - - /** @inheritDoc */ - public emitter?: EventEmitterAsyncResource - /** * The task execution response promise map: * - `key`: The message id of each submitted task. @@ -110,1272 +106,649 @@ export abstract class AbstractPool< > /** - * The task functions added at runtime map: - * - `key`: The task function name. - * - `value`: The task function object. + * This method is the message listener registered on each worker. + * @param message - The message received from the worker. */ - private readonly taskFunctions: Map< - string, - TaskFunctionObject - > + 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 is started or not. - */ - private started: boolean - /** - * Whether the pool is starting or not. - */ - private starting: boolean /** * Whether the pool is destroying or not. */ private destroying: boolean + /** - * Whether the minimum number of workers is starting or not. - */ - private startingMinimumNumberOfWorkers: boolean - /** - * Whether the pool ready event has been emitted or not. + * 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 readyEventEmitted: boolean + 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 + } + /** - * The start timestamp of the pool. + * Gets the worker choice strategies registered in this pool. + * @returns The worker choice strategies. */ - private startTimestamp?: number + 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 + ), + ]) + } /** - * 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. + * 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. */ - 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' - ) + private readonly getWorkerNodeTaskFunctionPriority = ( + workerNodeKey: number, + name?: string + ): number | undefined => { + const workerInfo = this.getWorkerInfo(workerNodeKey) + if (workerInfo == null) { + return } - this.checkPoolType() - checkFilePath(this.filePath) - this.checkMinimumNumberOfWorkers(this.minimumNumberOfWorkers) - this.checkPoolOptions(this.opts) - - 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() + name = name ?? DEFAULT_TASK_NAME + if (name === DEFAULT_TASK_NAME) { + name = workerInfo.taskFunctionsProperties?.[1]?.name } - this.workerChoiceStrategiesContext = new WorkerChoiceStrategiesContext< - Worker, - Data, - Response - >( - this, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - [this.opts.workerChoiceStrategy!], - this.opts.workerChoiceStrategyOptions - ) - - this.setupHook() - - this.taskFunctions = new Map>() + return workerInfo.taskFunctionsProperties?.find( + (taskFunctionProperties: TaskFunctionProperties) => + taskFunctionProperties.name === name + )?.priority + } - this.started = false - this.starting = false - this.destroying = false - this.readyEventEmitted = false - this.startingMinimumNumberOfWorkers = false - if (this.opts.startWorkers === true) { - this.start() + /** + * 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 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 readonly handleWorkerNodeBackPressureEvent = ( + eventDetail: WorkerNodeEventDetail + ): void => { + if ( + this.cannotStealTask() || + this.hasBackPressure() || + (this.info.stealingWorkerNodes ?? 0) > + Math.round( + this.workerNodes.length * + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.opts.tasksQueueOptions!.tasksStealingRatio! + ) + ) { + 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 && + workerNode.info.id !== workerId && + workerNode.usage.tasks.queued < + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.opts.tasksQueueOptions!.size! - sizeOffset + ) { + if (workerNode.info.backPressureStealing) { + continue + } + workerNode.info.backPressureStealing = true + this.stealTask(sourceWorkerNode, workerNodeKey) + workerNode.info.backPressureStealing = false + } } } - private checkMinimumNumberOfWorkers ( - minimumNumberOfWorkers: number | undefined - ): void { - if (minimumNumberOfWorkers == null) { + private readonly handleWorkerNodeIdleEvent = ( + eventDetail: WorkerNodeEventDetail, + previousStolenTask?: Task + ): void => { + const { workerNodeKey } = eventDetail + if (workerNodeKey == null) { throw new Error( - 'Cannot instantiate a pool without specifying the number of workers' - ) - } 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' + "WorkerNode event detail 'workerNodeKey' property must be defined" ) - } else if (this.type === PoolTypes.fixed && minimumNumberOfWorkers === 0) { - throw new RangeError('Cannot instantiate a fixed pool with zero worker') } - } - - 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 + const workerNodeInfo = this.getWorkerInfo(workerNodeKey) + if (workerNodeInfo == null) { + throw new Error( + `Worker node with key '${workerNodeKey.toString()}' not found in pool` ) - if (opts.workerChoiceStrategyOptions != null) { - this.opts.workerChoiceStrategyOptions = opts.workerChoiceStrategyOptions - } - 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 - ) - } - } else { - throw new TypeError('Invalid pool options: must be a plain object') } - } - - private checkValidWorkerChoiceStrategyOptions ( - workerChoiceStrategyOptions: WorkerChoiceStrategyOptions | undefined - ): void { if ( - workerChoiceStrategyOptions != null && - !isPlainObject(workerChoiceStrategyOptions) + !workerNodeInfo.continuousStealing && + (this.cannotStealTask() || + (this.info.stealingWorkerNodes ?? 0) > + Math.round( + this.workerNodes.length * + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.opts.tasksQueueOptions!.tasksStealingRatio! + )) ) { - throw new TypeError( - 'Invalid worker choice strategy options: must be a plain object' - ) + return } + const workerNodeTasksUsage = this.workerNodes[workerNodeKey].usage.tasks if ( - workerChoiceStrategyOptions?.weights != null && - Object.keys(workerChoiceStrategyOptions.weights).length !== - (this.maximumNumberOfWorkers ?? this.minimumNumberOfWorkers) + workerNodeInfo.continuousStealing && + (workerNodeTasksUsage.executing > 0 || + this.tasksQueueSize(workerNodeKey) > 0) ) { + workerNodeInfo.continuousStealing = false + if (workerNodeTasksUsage.sequentiallyStolen > 0) { + this.resetTaskSequentiallyStolenStatisticsWorkerUsage( + workerNodeKey, + previousStolenTask?.name + ) + } + return + } + workerNodeInfo.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) + }) + } + + /** + * 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. + */ + private startingMinimumNumberOfWorkers: boolean + + /** + * The start timestamp of the pool. + */ + private startTimestamp?: number + + private readonly stealTask = ( + sourceWorkerNode: IWorkerNode, + destinationWorkerNodeKey: number + ): Task | undefined => { + const destinationWorkerInfo = this.getWorkerInfo(destinationWorkerNodeKey) + if (destinationWorkerInfo == null) { throw new Error( - 'Invalid worker choice strategy options: must have a weight for each worker node' + `Worker node with key '${destinationWorkerNodeKey.toString()}' not found in pool` ) } + // Avoid cross and cascading task stealing. Could be smarter by checking stealing/stolen worker ids pair. if ( - workerChoiceStrategyOptions?.measurement != null && - !Object.values(Measurements).includes( - workerChoiceStrategyOptions.measurement - ) + !sourceWorkerNode.info.ready || + sourceWorkerNode.info.stolen || + sourceWorkerNode.info.stealing || + !destinationWorkerInfo.ready || + destinationWorkerInfo.stolen || + destinationWorkerInfo.stealing ) { - throw new Error( - `Invalid worker choice strategy options: invalid measurement '${workerChoiceStrategyOptions.measurement}'` - ) + return } + destinationWorkerInfo.stealing = true + sourceWorkerNode.info.stolen = true + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const stolenTask = sourceWorkerNode.dequeueLastPrioritizedTask()! + sourceWorkerNode.info.stolen = false + destinationWorkerInfo.stealing = false + this.handleTask(destinationWorkerNodeKey, stolenTask) + this.updateTaskStolenStatisticsWorkerUsage( + destinationWorkerNodeKey, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + stolenTask.name! + ) + return stolenTask } - private initEventEmitter (): void { - this.emitter = new EventEmitterAsyncResource({ - name: `poolifier:${this.type}-${this.worker}-pool`, - }) + /** + * The task functions added at runtime map: + * - `key`: The task function name. + * - `value`: The task function object. + */ + 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 get info (): PoolInfo { - return { - version, - type: this.type, - worker: this.worker, - started: this.started, - ready: this.ready, + public emitter?: EventEmitterAsyncResource + + /** @inheritDoc */ + public readonly workerNodes: IWorkerNode[] = [] + + /** + * 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) + + 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 - defaultStrategy: this.opts.workerChoiceStrategy!, - strategyRetries: this.workerChoiceStrategiesContext?.retriesCount ?? 0, - minSize: this.minimumNumberOfWorkers, - maxSize: this.maximumNumberOfWorkers ?? this.minimumNumberOfWorkers, - ...(this.workerChoiceStrategiesContext?.getTaskStatisticsRequirements() - .runTime.aggregate === true && - this.workerChoiceStrategiesContext.getTaskStatisticsRequirements() - .waitTime.aggregate && { - utilization: round(this.utilization), - }), - workerNodes: this.workerNodes.length, - idleWorkerNodes: this.workerNodes.reduce( - (accumulator, _, workerNodeKey) => - this.isWorkerNodeIdle(workerNodeKey) ? accumulator + 1 : accumulator, - 0 - ), - busyWorkerNodes: this.workerNodes.reduce( - (accumulator, _, workerNodeKey) => - this.isWorkerNodeBusy(workerNodeKey) ? accumulator + 1 : accumulator, - 0 - ), - ...(this.opts.enableTasksQueue === true && { - stealingWorkerNodes: this.workerNodes.reduce( - (accumulator, workerNode) => - workerNode.info.continuousStealing || - workerNode.info.backPressureStealing - ? accumulator + 1 - : accumulator, - 0 - ), - }), - ...(this.opts.enableTasksQueue === true && { - backPressureWorkerNodes: this.workerNodes.reduce( - (accumulator, workerNode) => - workerNode.info.backPressure ? 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 - ), - ...(this.opts.enableTasksQueue === true && { - queuedTasks: this.workerNodes.reduce( - (accumulator, workerNode) => - accumulator + workerNode.usage.tasks.queued, - 0 - ), - }), - ...(this.opts.enableTasksQueue === true && { - maxQueuedTasks: this.workerNodes.reduce( - (accumulator, workerNode) => - accumulator + (workerNode.usage.tasks.maxQueued ?? 0), - 0 - ), - }), - ...(this.opts.enableTasksQueue === true && { - backPressure: this.hasBackPressure(), - }), - ...(this.opts.enableTasksQueue === true && { - stolenTasks: this.workerNodes.reduce( - (accumulator, workerNode) => - accumulator + workerNode.usage.tasks.stolen, - 0 - ), - }), - failedTasks: this.workerNodes.reduce( - (accumulator, workerNode) => - accumulator + workerNode.usage.tasks.failed, - 0 - ), - ...(this.workerChoiceStrategiesContext?.getTaskStatisticsRequirements() - .runTime.aggregate === true && { - runTime: { - minimum: round( - min( - ...this.workerNodes.map( - workerNode => - workerNode.usage.runTime.minimum ?? Number.POSITIVE_INFINITY - ) - ) - ), - maximum: round( - max( - ...this.workerNodes.map( - workerNode => - workerNode.usage.runTime.maximum ?? Number.NEGATIVE_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: { - minimum: round( - min( - ...this.workerNodes.map( - workerNode => - workerNode.usage.waitTime.minimum ?? Number.POSITIVE_INFINITY - ) - ) - ), - maximum: round( - max( - ...this.workerNodes.map( - workerNode => - workerNode.usage.waitTime.maximum ?? Number.NEGATIVE_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: { - idle: { - minimum: round( - min( - ...this.workerNodes.map( - workerNode => - workerNode.usage.elu.idle.minimum ?? - Number.POSITIVE_INFINITY - ) - ) - ), - maximum: round( - max( - ...this.workerNodes.map( - workerNode => - workerNode.usage.elu.idle.maximum ?? - Number.NEGATIVE_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() - ), - [] - ) - ) - ), - }), - }, - active: { - minimum: round( - min( - ...this.workerNodes.map( - workerNode => - workerNode.usage.elu.active.minimum ?? - Number.POSITIVE_INFINITY - ) - ) - ), - maximum: round( - max( - ...this.workerNodes.map( - workerNode => - workerNode.usage.elu.active.maximum ?? - Number.NEGATIVE_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() - ), - [] - ) - ) - ), - }), - }, - 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 - ) - ) - ), - }, - }, - }), - } - } - - /** - * Whether the pool is ready or not. - * @returns The pool readiness boolean status. - */ - private get ready (): boolean { - if (this.empty) { - return false - } - return ( - this.workerNodes.reduce( - (accumulator, workerNode) => - !workerNode.info.dynamic && workerNode.info.ready - ? accumulator + 1 - : accumulator, - 0 - ) >= this.minimumNumberOfWorkers - ) - } - - /** - * Whether the pool is empty or not. - * @returns The pool emptiness boolean status. - */ - protected get empty (): boolean { - return this.minimumNumberOfWorkers === 0 && this.workerNodes.length === 0 - } - - /** - * The approximate pool utilization. - * @returns The pool utilization. - */ - 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 - ) - const totalTasksWaitTime = this.workerNodes.reduce( - (accumulator, workerNode) => - accumulator + (workerNode.usage.waitTime.aggregate ?? 0), - 0 - ) - return (totalTasksRunTime + totalTasksWaitTime) / poolTimeCapacity - } - - /** - * The pool type. - * - * If it is `'dynamic'`, it provides the `max` property. - */ - protected abstract get type (): PoolType - - /** - * The worker type. - */ - protected abstract get worker (): WorkerType - - /** - * 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 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()}'` - ) - } - } - - /** - * 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 - ) - } - - /** @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) - } - } - } - - /** @inheritDoc */ - public setWorkerChoiceStrategyOptions ( - workerChoiceStrategyOptions: WorkerChoiceStrategyOptions | undefined - ): 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 - } - - /** @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 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 buildTasksQueueOptions ( - tasksQueueOptions: TasksQueueOptions | undefined - ): TasksQueueOptions { - return { - ...getDefaultTasksQueueOptions( - this.maximumNumberOfWorkers ?? this.minimumNumberOfWorkers - ), - ...this.opts.tasksQueueOptions, - ...tasksQueueOptions, - } - } - - private setTasksQueueSize (size: number): void { - for (const workerNode of this.workerNodes) { - workerNode.tasksQueueBackPressureSize = size - } - } - - private setTaskStealing (): void { - for (const workerNodeKey of this.workerNodes.keys()) { - this.workerNodes[workerNodeKey].on('idle', this.handleWorkerNodeIdleEvent) - } - } - - private unsetTaskStealing (): void { - for (const workerNodeKey of this.workerNodes.keys()) { - this.workerNodes[workerNodeKey].off( - 'idle', - this.handleWorkerNodeIdleEvent - ) - } - } - - private setTasksStealingOnBackPressure (): void { - for (const workerNodeKey of this.workerNodes.keys()) { - this.workerNodes[workerNodeKey].on( - 'backPressure', - this.handleWorkerNodeBackPressureEvent - ) - } - } - - private unsetTasksStealingOnBackPressure (): void { - for (const workerNodeKey of this.workerNodes.keys()) { - this.workerNodes[workerNodeKey].off( - 'backPressure', - this.handleWorkerNodeBackPressureEvent - ) - } - } - - /** - * Whether the pool is full or not. - * @returns The pool fullness boolean status. - */ - protected get full (): boolean { - return ( - this.workerNodes.length >= - (this.maximumNumberOfWorkers ?? this.minimumNumberOfWorkers) - ) - } - - /** - * Whether the pool is busy or not. - * @returns The pool busyness boolean status. - */ - protected abstract get busy (): boolean - - /** - * Whether worker nodes are executing concurrently their tasks quota or not. - * @returns Worker nodes busyness boolean status. - */ - protected internalBusy (): boolean { - if (this.opts.enableTasksQueue === true) { - return ( - this.workerNodes.findIndex( - workerNode => - workerNode.info.ready && - workerNode.usage.tasks.executing < - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.opts.tasksQueueOptions!.concurrency! - ) === -1 - ) - } - return ( - this.workerNodes.findIndex( - workerNode => - workerNode.info.ready && workerNode.usage.tasks.executing === 0 - ) === -1 - ) - } - - private isWorkerNodeIdle (workerNodeKey: number): boolean { - if (this.opts.enableTasksQueue === true) { - return ( - this.workerNodes[workerNodeKey].usage.tasks.executing === 0 && - this.tasksQueueSize(workerNodeKey) === 0 - ) - } - return this.workerNodes[workerNodeKey].usage.tasks.executing === 0 - } - - private isWorkerNodeBusy (workerNodeKey: number): boolean { - if (this.opts.enableTasksQueue === true) { - return ( - this.workerNodes[workerNodeKey].usage.tasks.executing >= - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.opts.tasksQueueOptions!.concurrency! - ) - } - return this.workerNodes[workerNodeKey].usage.tasks.executing > 0 - } - - 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 - ) - this.sendToWorker(workerNodeKey, message) - }) - } - - private async sendTaskFunctionOperationToWorkers ( - message: MessageValue - ): Promise { - return await new Promise((resolve, reject) => { - const responsesReceived = new Array>() - const taskFunctionOperationsListener = ( - message: MessageValue - ): void => { - this.checkMessageWorkerId(message) - if (message.taskFunctionOperationStatus != null) { - responsesReceived.push(message) - if (responsesReceived.length === this.workerNodes.length) { - if ( - responsesReceived.every( - message => message.taskFunctionOperationStatus === true - ) - ) { - resolve(true) - } else if ( - responsesReceived.some( - message => message.taskFunctionOperationStatus === false - ) - ) { - const errorResponse = responsesReceived.find( - response => response.taskFunctionOperationStatus === false - ) - reject( - new Error( - `Task function operation '${ - message.taskFunctionOperation as string - // 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) - } - }) - } - - /** @inheritDoc */ - public hasTaskFunction (name: string): boolean { - return this.listTaskFunctionsProperties().some( - taskFunctionProperties => taskFunctionProperties.name === name - ) - } - - /** @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({ - taskFunctionOperation: 'add', - taskFunctionProperties: buildTaskFunctionProperties(name, fn), - taskFunction: fn.taskFunction.toString(), - }) - 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 removeTaskFunction (name: string): Promise { - if (!this.taskFunctions.has(name)) { - throw new Error( - 'Cannot remove a task function not handled on the pool side' - ) - } - const opResult = await this.sendTaskFunctionOperationToWorkers({ - taskFunctionOperation: 'remove', - 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() + [this.opts.workerChoiceStrategy!], + this.opts.workerChoiceStrategyOptions ) - for (const workerNodeKey of this.workerNodes.keys()) { - this.sendStatisticsMessageToWorker(workerNodeKey) - } - return opResult - } - /** @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 [] - } + this.setupHook() - /** - * 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 - ): WorkerChoiceStrategy | undefined => { - 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 - } + this.taskFunctions = new Map>() - /** - * 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 - ): WorkerChoiceStrategy | 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 + this.started = false + this.starting = false + this.destroying = false + this.readyEventEmitted = false + this.startingMinimumNumberOfWorkers = false + if (this.opts.startWorkers === true) { + this.start() } - return workerInfo.taskFunctionsProperties?.find( - (taskFunctionProperties: TaskFunctionProperties) => - taskFunctionProperties.name === name - )?.strategy } /** - * Gets worker node task function priority, if any. + * Hook executed after the worker task execution. + * Can be overridden. * @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. + * @param message - The received message. */ - private readonly getWorkerNodeTaskFunctionPriority = ( + protected afterTaskExecutionHook ( 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 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: WorkerChoiceStrategy | undefined) => strategy != null - ), - ]) - } - - /** @inheritDoc */ - public async setDefaultTaskFunction (name: string): Promise { - return await this.sendTaskFunctionOperationToWorkers({ - taskFunctionOperation: 'default', - taskFunctionProperties: buildTaskFunctionProperties( - name, - this.taskFunctions.get(name) - ), - }) - } - - 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! - ) - } - - 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 = { - name: name ?? DEFAULT_TASK_NAME, - data: data ?? ({} as Data), - priority: this.getWorkerNodeTaskFunctionPriority(workerNodeKey, name), - strategy: this.getWorkerNodeTaskFunctionWorkerChoiceStrategy( - workerNodeKey, - name - ), - transferList, - timestamp, - taskId: randomUUID(), - } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.promiseResponseMap.set(task.taskId!, { - resolve, - reject, - workerNodeKey, - ...(this.emitter != null && { - asyncResource: new AsyncResource('poolifier:task', { - triggerAsyncId: this.emitter.asyncId, - requireManualDestroy: true, - }), - }), - }) - if ( - this.opts.enableTasksQueue === false || - (this.opts.enableTasksQueue === true && - this.shallExecuteTask(workerNodeKey)) - ) { - this.executeTask(workerNodeKey, task) - } else { - this.enqueueTask(workerNodeKey, task) - } - }) - } - - /** @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') - } - if (transferList != null && !Array.isArray(transferList)) { - throw new TypeError('transferList argument must be an array') - } - return await this.internalExecute(data, name, transferList) - } - - /** @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') - } + message: MessageValue + ): void { + let needWorkerChoiceStrategiesUpdate = false // 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 (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 (transferList != null && !Array.isArray(transferList)) { - throw new TypeError('transferList argument must be an array') + 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 (!Array.isArray(data)) { - data = [...data] + if (needWorkerChoiceStrategiesUpdate) { + this.workerChoiceStrategiesContext?.update(workerNodeKey) } - return await Promise.all( - (data as Data[]).map(data => - this.internalExecute(data, name, transferList) - ) - ) } /** - * Starts the minimum number of workers. - * @param initWorkerNodeUsage - Whether to initialize the worker node usage or not. @defaultValue false + * Method hooked up after a worker node has been newly created. + * Can be overridden. + * @param workerNodeKey - The newly created worker node key. */ - 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]) + 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 + ) + } } - this.startingMinimumNumberOfWorkers = false } - /** @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') + /** + * 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 + ) } - if (this.destroying) { - throw new Error('Cannot start a destroying pool') + 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 + ) } - this.starting = true - this.startMinimumNumberOfWorkers() - this.startTimestamp = performance.now() - this.starting = false - this.started = true } - /** @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') + /** + * Emits dynamic worker creation events. + */ + protected abstract checkAndEmitDynamicWorkerCreationEvents (): 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 + ) + const workerInfo = this.getWorkerInfo(localWorkerNodeKey) + // Kill message received from worker + if ( + isKillBehavior(KillBehaviors.HARD, message.kill) || + (isKillBehavior(KillBehaviors.SOFT, message.kill) && + this.isWorkerNodeIdle(localWorkerNodeKey) && + workerInfo != null && + !workerInfo.continuousStealing && + !workerInfo.backPressureStealing) + ) { + // 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) + }) + } } - if (this.destroying) { - throw new Error('Cannot destroy an already destroying pool') + const workerNode = this.workerNodes[workerNodeKey] + workerNode.info.dynamic = true + if ( + this.workerChoiceStrategiesContext?.getPolicy().dynamicWorkerReady === + true || + this.workerChoiceStrategiesContext?.getPolicy().dynamicWorkerUsage === + true + ) { + workerNode.info.ready = true } - this.destroying = true - await Promise.all( - this.workerNodes.map(async (_, workerNodeKey) => { - await this.destroyWorkerNode(workerNodeKey) - }) - ) - this.emitter?.emit(PoolEvents.destroy, this.info) - this.emitter?.emitDestroy() - this.readyEventEmitted = false - delete this.startTimestamp - this.destroying = false - this.started = false + this.initWorkerNodeUsage(workerNode) + this.checkAndEmitDynamicWorkerCreationEvents() + return workerNodeKey } - 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()}` - ) - ) + /** + * Creates a new, completely set up worker node. + * @returns New, completely set up worker node key. + */ + 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.started && + !this.destroying && + this.opts.restartWorkerOnError === true + ) { + if (workerNode.info.dynamic) { + this.createAndSetupDynamicWorkerNode() + } else if (!this.startingMinimumNumberOfWorkers) { + this.startMinimumNumberOfWorkers(true) } } - // FIXME: should be registered only once - this.registerWorkerMessageListener(workerNodeKey, killMessageListener) - this.sendToWorker(workerNodeKey, { kill: 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 + ) + 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 } + /** + * Deregisters a listener callback on the worker given its worker node key. + * @param workerNodeKey - The worker node key. + * @param listener - The message listener callback. + */ + protected abstract deregisterWorkerMessageListener< + Message extends Data | Response + >( + workerNodeKey: number, + listener: (message: MessageValue) => void + ): void + /** * Terminates the worker node given its worker node key. * @param workerNodeKey - The worker node key. @@ -1397,12 +770,55 @@ export abstract class AbstractPool< await workerNode.terminate() } + protected flagWorkerNodeAsNotReady (workerNodeKey: number): void { + const workerInfo = this.getWorkerInfo(workerNodeKey) + if (workerInfo != null) { + workerInfo.ready = 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 + } + this.workerNodes[workerNodeKey].clearTasksQueue() + return flushedTasks + } + /** - * Setup hook to execute code before worker nodes are created in the abstract constructor. - * Can be overridden. + * Gets the worker information given its worker node key. + * @param workerNodeKey - The worker node key. + * @returns The worker information. */ - protected setupHook (): void { - /* Intentionally empty */ + protected getWorkerInfo (workerNodeKey: number): undefined | WorkerInfo { + return this.workerNodes[workerNodeKey]?.info + } + + /** + * Whether worker nodes are executing concurrently their tasks quota or not. + * @returns Worker nodes busyness boolean status. + */ + protected internalBusy (): boolean { + if (this.opts.enableTasksQueue === true) { + return ( + this.workerNodes.findIndex( + workerNode => + workerNode.info.ready && + workerNode.usage.tasks.executing < + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.opts.tasksQueueOptions!.concurrency! + ) === -1 + ) + } + return ( + this.workerNodes.findIndex( + workerNode => + workerNode.info.ready && workerNode.usage.tasks.executing === 0 + ) === -1 + ) } /** @@ -1412,115 +828,219 @@ export abstract class AbstractPool< protected abstract isMain (): boolean /** - * Hook executed before the worker task execution. - * Can be overridden. + * Registers once 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. */ - protected beforeTaskExecutionHook ( + protected abstract registerOnceWorkerMessageListener< + Message extends Data | Response + >( workerNodeKey: number, - task: Task + 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[] + ): 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 + + /** + * 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 buildTasksQueueOptions ( + tasksQueueOptions: TasksQueueOptions | undefined + ): TasksQueueOptions { + return { + ...getDefaultTasksQueueOptions( + this.maximumNumberOfWorkers ?? this.minimumNumberOfWorkers + ), + ...this.opts.tasksQueueOptions, + ...tasksQueueOptions, + } + } + + private cannotStealTask (): boolean { + return this.workerNodes.length <= 1 || this.info.queuedTasks === 0 + } + + private checkAndEmitEmptyEvent (): void { + if (this.empty) { + this.emitter?.emit(PoolEvents.empty, this.info) + this.readyEventEmitted = false + } + } + + private checkAndEmitReadyEvent (): void { + if (!this.readyEventEmitted && this.ready) { + this.emitter?.emit(PoolEvents.ready, this.info) + this.readyEventEmitted = true + } + } + + private checkAndEmitTaskExecutionEvents (): void { + if (this.busy) { + this.emitter?.emit(PoolEvents.busy, this.info) + } + } + + private checkAndEmitTaskQueuingEvents (): void { + if (this.hasBackPressure()) { + this.emitter?.emit(PoolEvents.backPressure, this.info) + } + } + + /** + * 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 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 ): 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 (minimumNumberOfWorkers == null) { + throw new Error( + 'Cannot instantiate a pool without specifying the number of workers' + ) + } 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 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 + } + 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 + ) + } + } else { + throw new TypeError('Invalid pool options: must be a plain object') } - 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 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 after the worker task execution. - * Can be overridden. - * @param workerNodeKey - The worker node key. - * @param message - The received message. - */ - protected afterTaskExecutionHook ( - workerNodeKey: number, - message: MessageValue + private checkValidWorkerChoiceStrategyOptions ( + workerChoiceStrategyOptions: undefined | WorkerChoiceStrategyOptions ): 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 + if ( + workerChoiceStrategyOptions != null && + !isPlainObject(workerChoiceStrategyOptions) + ) { + throw new TypeError( + 'Invalid worker choice strategy options: must be a plain object' ) - needWorkerChoiceStrategiesUpdate = true } if ( - this.shallUpdateTaskFunctionWorkerUsage(workerNodeKey) && - message.taskPerformance?.name != null && - this.workerNodes[workerNodeKey].getTaskFunctionWorkerUsage( - message.taskPerformance.name - ) != null + workerChoiceStrategyOptions?.weights != null && + Object.keys(workerChoiceStrategyOptions.weights).length !== + (this.maximumNumberOfWorkers ?? this.minimumNumberOfWorkers) ) { - // 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 + throw new Error( + 'Invalid worker choice strategy options: must have a weight for each worker node' ) - needWorkerChoiceStrategiesUpdate = true } - if (needWorkerChoiceStrategiesUpdate) { - this.workerChoiceStrategiesContext?.update(workerNodeKey) + if ( + workerChoiceStrategyOptions?.measurement != null && + !Object.values(Measurements).includes( + workerChoiceStrategyOptions.measurement + ) + ) { + throw new Error( + `Invalid worker choice strategy options: invalid measurement '${workerChoiceStrategyOptions.measurement}'` + ) } } - /** - * 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. - */ - private shallUpdateTaskFunctionWorkerUsage (workerNodeKey: number): boolean { - const workerInfo = this.getWorkerInfo(workerNodeKey) - return ( - workerInfo != null && - Array.isArray(workerInfo.taskFunctionsProperties) && - workerInfo.taskFunctionsProperties.length > 2 - ) - } - /** * Chooses a worker node for the next task. * @param name - The task function name. @@ -1542,23 +1062,162 @@ export abstract class AbstractPool< ) } - /** - * Conditions for dynamic worker creation. - * @returns Whether to create a dynamic worker or not. - */ - protected abstract shallCreateDynamicWorker (): boolean - - /** - * 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[] - ): void + /** + * 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, + } + ) + // Flag the worker node as ready at pool startup. + if (this.starting) { + workerNode.info.ready = true + } + return workerNode + } + + private dequeueTask (workerNodeKey: number): Task | undefined { + return this.workerNodes[workerNodeKey].dequeueTask() + } + + private enqueueTask (workerNodeKey: number, task: Task): number { + const tasksQueueSize = this.workerNodes[workerNodeKey].enqueueTask(task) + this.checkAndEmitTaskQueuingEvents() + return tasksQueueSize + } + + /** + * 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.flushTasksQueue(workerNodeKey) + } + } + + private getTasksQueuePriority (): boolean { + return this.listTaskFunctionsProperties().some( + taskFunctionProperties => taskFunctionProperties.priority != 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 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) + asyncResource != null + ? asyncResource.runInAsyncScope( + reject, + this.emitter, + workerError.message + ) + : reject(workerError.message) + } else { + asyncResource != null + ? asyncResource.runInAsyncScope(resolve, this.emitter, data) + : resolve(data as Response) + } + asyncResource?.emitDestroy() + this.afterTaskExecutionHook(workerNodeKey, message) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.promiseResponseMap.delete(taskId!) + if (this.opts.enableTasksQueue === true && !this.destroying) { + const workerNodeTasksUsage = workerNode.usage.tasks + if ( + this.tasksQueueSize(workerNodeKey) > 0 && + workerNodeTasksUsage.executing < + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.opts.tasksQueueOptions!.concurrency! + ) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.executeTask(workerNodeKey, this.dequeueTask(workerNodeKey)!) + } + if (this.isWorkerNodeIdle(workerNodeKey)) { + workerNode.emit('idle', { + workerNodeKey, + }) + } + } + // FIXME: cannot be theoretically undefined. Schedule in the next tick to avoid race conditions? + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + workerNode?.emit('taskFinished', taskId) + } + } + + 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() + } + + private hasBackPressure (): boolean { + return ( + this.opts.enableTasksQueue === true && + this.workerNodes.findIndex( + workerNode => !workerNode.hasBackPressure() + ) === -1 + ) + } + + private initEventEmitter (): void { + this.emitter = new EventEmitterAsyncResource({ + name: `poolifier:${this.type}-${this.worker}-pool`, + }) + } /** * Initializes the worker node usage with sensible default values gathered during runtime. @@ -1600,201 +1259,154 @@ export abstract class AbstractPool< } } - /** - * Creates a new, completely set up worker node. - * @returns New, completely set up worker node key. - */ - 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.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)) + 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, } - // 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) + // 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, + }), + }), }) - }) - workerNode.registerWorkerEventHandler( - 'exit', - this.opts.exitHandler ?? EMPTY_FUNCTION - ) - workerNode.registerOnceWorkerEventHandler('exit', () => { - this.removeWorkerNode(workerNode) if ( - this.started && - !this.startingMinimumNumberOfWorkers && - !this.destroying + this.opts.enableTasksQueue === false || + (this.opts.enableTasksQueue === true && + this.shallExecuteTask(workerNodeKey)) ) { - this.startMinimumNumberOfWorkers(true) + this.executeTask(workerNodeKey, task) + } else { + this.enqueueTask(workerNodeKey, task) } }) - const workerNodeKey = this.addWorkerNode(workerNode) - this.afterWorkerNodeSetup(workerNodeKey) - return workerNodeKey } - /** - * 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 + private isWorkerNodeBusy (workerNodeKey: number): boolean { + if (this.opts.enableTasksQueue === true) { + return ( + this.workerNodes[workerNodeKey].usage.tasks.executing >= + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.opts.tasksQueueOptions!.concurrency! ) - const workerInfo = this.getWorkerInfo(localWorkerNodeKey) - // Kill message received from worker - if ( - isKillBehavior(KillBehaviors.HARD, message.kill) || - (isKillBehavior(KillBehaviors.SOFT, message.kill) && - this.isWorkerNodeIdle(localWorkerNodeKey) && - workerInfo != null && - !workerInfo.continuousStealing && - !workerInfo.backPressureStealing) - ) { - // 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, { - taskFunctionOperation: 'add', - taskFunctionProperties: buildTaskFunctionProperties( - taskFunctionName, - taskFunctionObject - ), - taskFunction: taskFunctionObject.taskFunction.toString(), - }).catch((error: unknown) => { - this.emitter?.emit(PoolEvents.error, error) - }) - } - } - const workerNode = this.workerNodes[workerNodeKey] - workerNode.info.dynamic = true - if ( - this.workerChoiceStrategiesContext?.getPolicy().dynamicWorkerReady === - true || - this.workerChoiceStrategiesContext?.getPolicy().dynamicWorkerUsage === - true - ) { - workerNode.info.ready = true } - this.initWorkerNodeUsage(workerNode) - this.checkAndEmitDynamicWorkerCreationEvents() - return workerNodeKey + return this.workerNodes[workerNodeKey].usage.tasks.executing > 0 } - /** - * 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 - - /** - * Registers once a listener callback on the worker given its worker node key. - * @param workerNodeKey - The worker node key. - * @param listener - The message listener callback. - */ - protected abstract registerOnceWorkerMessageListener< - Message extends Data | Response - >( - workerNodeKey: number, - listener: (message: MessageValue) => void - ): void - - /** - * Deregisters a listener callback on the worker given its worker node key. - * @param workerNodeKey - The worker node key. - * @param listener - The message listener callback. - */ - protected abstract deregisterWorkerMessageListener< - Message extends Data | Response - >( - workerNodeKey: number, - listener: (message: MessageValue) => void - ): void - - /** - * Method hooked up after a worker node has been newly created. - * Can be overridden. - * @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) + private isWorkerNodeIdle (workerNodeKey: number): boolean { 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 - ) - } + return ( + this.workerNodes[workerNodeKey].usage.tasks.executing === 0 && + this.tasksQueueSize(workerNodeKey) === 0 + ) + } + return this.workerNodes[workerNodeKey].usage.tasks.executing === 0 + } + + 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)! + ) } } /** - * Sends the startup message to worker given its worker node key. - * @param workerNodeKey - The worker node key. + * Removes the worker node from the pool worker nodes. + * @param workerNode - The worker node. */ - protected abstract sendStartupMessageToWorker (workerNodeKey: number): void + private removeWorkerNode (workerNode: IWorkerNode): void { + const workerNodeKey = this.workerNodes.indexOf(workerNode) + if (workerNodeKey !== -1) { + this.workerNodes.splice(workerNodeKey, 1) + this.workerChoiceStrategiesContext?.remove(workerNodeKey) + } + this.checkAndEmitEmptyEvent() + } + + 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()}` + ) + ) + } + } + // 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. @@ -1803,67 +1415,204 @@ export abstract class AbstractPool< private sendStatisticsMessageToWorker (workerNodeKey: number): void { this.sendToWorker(workerNodeKey, { statistics: { - runTime: - this.workerChoiceStrategiesContext?.getTaskStatisticsRequirements() - .runTime.aggregate ?? false, elu: this.workerChoiceStrategiesContext?.getTaskStatisticsRequirements() .elu.aggregate ?? false, + runTime: + this.workerChoiceStrategiesContext?.getTaskStatisticsRequirements() + .runTime.aggregate ?? false, }, }) } - private cannotStealTask (): boolean { - return this.workerNodes.length <= 1 || this.info.queuedTasks === 0 + 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 + ) + this.sendToWorker(workerNodeKey, message) + }) } - private handleTask (workerNodeKey: number, task: Task): void { - if (this.shallExecuteTask(workerNodeKey)) { - this.executeTask(workerNodeKey, task) - } else { - this.enqueueTask(workerNodeKey, task) + 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 + ) + } + } + } + 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 } } - private redistributeQueuedTasks (sourceWorkerNodeKey: number): void { - if (sourceWorkerNodeKey === -1 || this.cannotStealTask()) { + 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.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. + */ + private shallUpdateTaskFunctionWorkerUsage (workerNodeKey: number): boolean { + const workerInfo = this.getWorkerInfo(workerNodeKey) + return ( + workerInfo != null && + Array.isArray(workerInfo.taskFunctionsProperties) && + workerInfo.taskFunctionsProperties.length > 2 + ) + } + + /** + * Starts the minimum number of workers. + * @param initWorkerNodeUsage - Whether to initialize the worker node usage or not. @defaultValue false + */ + private startMinimumNumberOfWorkers (initWorkerNodeUsage = false): void { + if (this.minimumNumberOfWorkers === 0) { 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 - }, + this.startingMinimumNumberOfWorkers = true + while ( + this.workerNodes.reduce( + (accumulator, workerNode) => + !workerNode.info.dynamic ? accumulator + 1 : accumulator, 0 - ) - this.handleTask( - destinationWorkerNodeKey, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.dequeueTask(sourceWorkerNodeKey)! - ) + ) < this.minimumNumberOfWorkers + ) { + const workerNodeKey = this.createAndSetupWorkerNode() + initWorkerNodeUsage && + this.initWorkerNodeUsage(this.workerNodes[workerNodeKey]) } + this.startingMinimumNumberOfWorkers = false } - 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 + 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 + ) } - if ( - this.shallUpdateTaskFunctionWorkerUsage(workerNodeKey) && - workerNode.getTaskFunctionWorkerUsage(taskName) != null - ) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - ++workerNode.getTaskFunctionWorkerUsage(taskName)!.tasks.stolen + } + + private unsetTaskStealing (): void { + for (const workerNodeKey of this.workerNodes.keys()) { + this.workerNodes[workerNodeKey].off( + 'idle', + this.handleWorkerNodeIdleEvent + ) } } @@ -1898,441 +1647,699 @@ export abstract class AbstractPool< } } - private resetTaskSequentiallyStolenStatisticsWorkerUsage ( + private updateTaskStolenStatisticsWorkerUsage ( workerNodeKey: number, - taskName?: string + 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 + ++workerNode.usage.tasks.stolen } 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 + ++workerNode.getTaskFunctionWorkerUsage(taskName)!.tasks.stolen } } - private readonly stealTask = ( - sourceWorkerNode: IWorkerNode, - destinationWorkerNodeKey: number - ): Task | undefined => { - const destinationWorkerInfo = this.getWorkerInfo(destinationWorkerNodeKey) - if (destinationWorkerInfo == null) { - throw new Error( - `Worker node with key '${destinationWorkerNodeKey.toString()}' not found in pool` - ) + /** @inheritDoc */ + public async addTaskFunction ( + name: string, + fn: TaskFunction | TaskFunctionObject + ): Promise { + if (typeof name !== 'string') { + throw new TypeError('name argument must be a string') } - // Avoid cross and cascading task stealing. Could be smarter by checking stealing/stolen worker ids pair. - if ( - !sourceWorkerNode.info.ready || - sourceWorkerNode.info.stolen || - sourceWorkerNode.info.stealing || - !destinationWorkerInfo.ready || - destinationWorkerInfo.stolen || - destinationWorkerInfo.stealing - ) { - return + if (typeof name === 'string' && name.trim().length === 0) { + throw new TypeError('name argument must not be an empty string') } - destinationWorkerInfo.stealing = true - sourceWorkerNode.info.stolen = true - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const stolenTask = sourceWorkerNode.dequeueLastPrioritizedTask()! - sourceWorkerNode.info.stolen = false - destinationWorkerInfo.stealing = false - this.handleTask(destinationWorkerNodeKey, stolenTask) - this.updateTaskStolenStatisticsWorkerUsage( - destinationWorkerNodeKey, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - stolenTask.name! + 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() ) - return stolenTask + for (const workerNodeKey of this.workerNodes.keys()) { + this.sendStatisticsMessageToWorker(workerNodeKey) + } + return opResult } - 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" - ) + /** @inheritDoc */ + public async destroy (): Promise { + if (!this.started) { + throw new Error('Cannot destroy an already destroyed pool') } - const workerNodeInfo = this.getWorkerInfo(workerNodeKey) - if (workerNodeInfo == null) { - throw new Error( - `Worker node with key '${workerNodeKey.toString()}' not found in pool` - ) + if (this.starting) { + throw new Error('Cannot destroy an starting pool') } - if ( - !workerNodeInfo.continuousStealing && - (this.cannotStealTask() || - (this.info.stealingWorkerNodes ?? 0) > - Math.round( - this.workerNodes.length * - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.opts.tasksQueueOptions!.tasksStealingRatio! - )) - ) { - return + if (this.destroying) { + throw new Error('Cannot destroy an already destroying pool') } - const workerNodeTasksUsage = this.workerNodes[workerNodeKey].usage.tasks - if ( - workerNodeInfo.continuousStealing && - (workerNodeTasksUsage.executing > 0 || - this.tasksQueueSize(workerNodeKey) > 0) - ) { - workerNodeInfo.continuousStealing = false - if (workerNodeTasksUsage.sequentiallyStolen > 0) { - this.resetTaskSequentiallyStolenStatisticsWorkerUsage( - workerNodeKey, - previousStolenTask?.name - ) + this.destroying = true + await Promise.all( + this.workerNodes.map(async (_, workerNodeKey) => { + await this.destroyWorkerNode(workerNodeKey) + }) + ) + 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() + } + 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') + } + if (transferList != null && !Array.isArray(transferList)) { + throw new TypeError('transferList argument must be an array') + } + return await this.internalExecute(data, name, transferList) + } + + /** @inheritDoc */ + public hasTaskFunction (name: string): boolean { + return this.listTaskFunctionsProperties().some( + taskFunctionProperties => taskFunctionProperties.name === name + ) + } + + /** @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 } - workerNodeInfo.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 workerNodeStealTask = ( - workerNodeKey: number - ): Task | undefined => { - const workerNodes = this.workerNodes - .slice() - .sort( - (workerNodeA, workerNodeB) => - workerNodeB.usage.tasks.queued - workerNodeA.usage.tasks.queued + return [] + } + + /** @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) ) - const sourceWorkerNode = workerNodes.find( - (sourceWorkerNode, sourceWorkerNodeKey) => - sourceWorkerNodeKey !== workerNodeKey && - sourceWorkerNode.usage.tasks.queued > 0 ) - if (sourceWorkerNode != null) { - return this.stealTask(sourceWorkerNode, workerNodeKey) - } } - private readonly handleWorkerNodeBackPressureEvent = ( - eventDetail: WorkerNodeEventDetail - ): void => { - if ( - this.cannotStealTask() || - this.hasBackPressure() || - (this.info.stealingWorkerNodes ?? 0) > - Math.round( - this.workerNodes.length * - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.opts.tasksQueueOptions!.tasksStealingRatio! - ) - ) { - return + /** @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' + ) } - const sizeOffset = 1 - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - if (this.opts.tasksQueueOptions!.size! <= sizeOffset) { - return + const opResult = await this.sendTaskFunctionOperationToWorkers({ + taskFunctionOperation: 'remove', + taskFunctionProperties: buildTaskFunctionProperties( + name, + this.taskFunctions.get(name) + ), + }) + for (const workerNode of this.workerNodes) { + workerNode.deleteTaskFunctionWorkerUsage(name) } - 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 && - workerNode.info.id !== workerId && - workerNode.usage.tasks.queued < - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.opts.tasksQueueOptions!.size! - sizeOffset - ) { - if (workerNode.info.backPressureStealing) { - continue - } - workerNode.info.backPressureStealing = true - this.stealTask(sourceWorkerNode, workerNodeKey) - workerNode.info.backPressureStealing = false - } + this.taskFunctions.delete(name) + this.workerChoiceStrategiesContext?.syncWorkerChoiceStrategies( + this.getWorkerChoiceStrategies() + ) + for (const workerNodeKey of this.workerNodes.keys()) { + this.sendStatisticsMessageToWorker(workerNodeKey) } + return opResult } - private setTasksQueuePriority (workerNodeKey: number): void { - this.workerNodes[workerNodeKey].setTasksQueuePriority( - this.getTasksQueuePriority() - ) + /** @inheritDoc */ + public async setDefaultTaskFunction (name: string): Promise { + return await this.sendTaskFunctionOperationToWorkers({ + taskFunctionOperation: 'default', + taskFunctionProperties: buildTaskFunctionProperties( + name, + this.taskFunctions.get(name) + ), + }) } - /** - * 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 { workerId, ready, taskId, taskFunctionsProperties } = 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) + /** @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() } - } else if (taskId != null) { - // Task execution response received from worker - this.handleTaskExecutionResponse(message) + 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.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 + ) } - } - - private handleWorkerReadyResponse (message: MessageValue): void { - const { workerId, ready, taskFunctionsProperties } = message - if (ready == null || !ready) { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - throw new Error(`Worker ${workerId?.toString()} failed to initialize`) + 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) + } } - 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() } - private handleTaskExecutionResponse (message: MessageValue): void { - const { taskId, workerError, data } = message - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const promiseResponse = this.promiseResponseMap.get(taskId!) - if (promiseResponse != null) { - const { resolve, reject, workerNodeKey, asyncResource } = promiseResponse - const workerNode = this.workerNodes[workerNodeKey] - if (workerError != null) { - this.emitter?.emit(PoolEvents.taskError, workerError) - asyncResource != null - ? asyncResource.runInAsyncScope( - reject, - this.emitter, - workerError.message - ) - : reject(workerError.message) - } else { - asyncResource != null - ? asyncResource.runInAsyncScope(resolve, this.emitter, data) - : resolve(data as Response) + /** @inheritDoc */ + public setWorkerChoiceStrategyOptions ( + workerChoiceStrategyOptions: undefined | WorkerChoiceStrategyOptions + ): boolean { + this.checkValidWorkerChoiceStrategyOptions(workerChoiceStrategyOptions) + if (workerChoiceStrategyOptions != null) { + this.opts.workerChoiceStrategyOptions = { + ...this.opts.workerChoiceStrategyOptions, + ...workerChoiceStrategyOptions, } - asyncResource?.emitDestroy() - this.afterTaskExecutionHook(workerNodeKey, message) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.promiseResponseMap.delete(taskId!) - if (this.opts.enableTasksQueue === true && !this.destroying) { - const workerNodeTasksUsage = workerNode.usage.tasks - if ( - this.tasksQueueSize(workerNodeKey) > 0 && - workerNodeTasksUsage.executing < - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.opts.tasksQueueOptions!.concurrency! - ) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.executeTask(workerNodeKey, this.dequeueTask(workerNodeKey)!) - } - if (this.isWorkerNodeIdle(workerNodeKey)) { - workerNode.emit('idle', { - workerNodeKey, - }) - } + this.workerChoiceStrategiesContext?.setOptions( + this.opts.workerChoiceStrategyOptions + ) + this.workerChoiceStrategiesContext?.syncWorkerChoiceStrategies( + this.getWorkerChoiceStrategies(), + this.opts.workerChoiceStrategyOptions + ) + for (const workerNodeKey of this.workerNodes.keys()) { + this.sendStatisticsMessageToWorker(workerNodeKey) } - // FIXME: cannot be theoretically undefined. Schedule in the next tick to avoid race conditions? - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - workerNode?.emit('taskFinished', taskId) + return true } + return false } - private checkAndEmitTaskExecutionEvents (): void { - if (this.busy) { - this.emitter?.emit(PoolEvents.busy, this.info) + /** @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') + } + 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 } - private checkAndEmitTaskQueuingEvents (): void { - if (this.hasBackPressure()) { - this.emitter?.emit(PoolEvents.backPressure, this.info) + /** + * Whether the pool is busy or not. + * @returns The pool busyness boolean status. + */ + protected abstract get busy (): boolean + + /** + * Whether the pool is empty or not. + * @returns The pool emptiness boolean status. + */ + protected get empty (): boolean { + return this.minimumNumberOfWorkers === 0 && this.workerNodes.length === 0 + } + + /** + * Whether the pool is full or not. + * @returns The pool fullness boolean status. + */ + protected get full (): boolean { + return ( + this.workerNodes.length >= + (this.maximumNumberOfWorkers ?? this.minimumNumberOfWorkers) + ) + } + + /** @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 + ), + idleWorkerNodes: this.workerNodes.reduce( + (accumulator, _, workerNodeKey) => + this.isWorkerNodeIdle(workerNodeKey) ? accumulator + 1 : accumulator, + 0 + ), + workerNodes: this.workerNodes.length, + ...(this.opts.enableTasksQueue === true && { + stealingWorkerNodes: this.workerNodes.reduce( + (accumulator, workerNode) => + workerNode.info.continuousStealing || + workerNode.info.backPressureStealing + ? accumulator + 1 + : accumulator, + 0 + ), + }), + ...(this.opts.enableTasksQueue === true && { + backPressureWorkerNodes: this.workerNodes.reduce( + (accumulator, workerNode) => + workerNode.info.backPressure ? 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 + ), + ...(this.opts.enableTasksQueue === true && { + queuedTasks: this.workerNodes.reduce( + (accumulator, workerNode) => + accumulator + workerNode.usage.tasks.queued, + 0 + ), + }), + ...(this.opts.enableTasksQueue === true && { + maxQueuedTasks: this.workerNodes.reduce( + (accumulator, workerNode) => + accumulator + (workerNode.usage.tasks.maxQueued ?? 0), + 0 + ), + }), + ...(this.opts.enableTasksQueue === true && { + backPressure: this.hasBackPressure(), + }), + ...(this.opts.enableTasksQueue === true && { + stolenTasks: this.workerNodes.reduce( + (accumulator, workerNode) => + accumulator + workerNode.usage.tasks.stolen, + 0 + ), + }), + failedTasks: this.workerNodes.reduce( + (accumulator, workerNode) => + accumulator + workerNode.usage.tasks.failed, + 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 + ) + ) + ), + 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 + ) + ) + ), + }, + }, + }), } } /** - * Emits dynamic worker creation events. - */ - protected abstract checkAndEmitDynamicWorkerCreationEvents (): void - - /** - * Gets the worker information given its worker node key. - * @param workerNodeKey - The worker node key. - * @returns The worker information. - */ - protected getWorkerInfo (workerNodeKey: number): WorkerInfo | undefined { - return this.workerNodes[workerNodeKey]?.info - } - - private getTasksQueuePriority (): boolean { - return this.listTaskFunctionsProperties().some( - taskFunctionProperties => taskFunctionProperties.priority != null - ) - } - - /** - * Creates a worker node. - * @returns The created worker node. + * Whether the pool is ready or not. + * @returns The pool readiness boolean status. */ - private createWorkerNode (): IWorkerNode { - const workerNode = new WorkerNode( - this.worker, - this.filePath, - { - env: this.opts.env, - workerOptions: this.opts.workerOptions, - tasksQueueBackPressureSize: - this.opts.tasksQueueOptions?.size ?? - getDefaultTasksQueueOptions( - this.maximumNumberOfWorkers ?? this.minimumNumberOfWorkers - ).size, - tasksQueueBucketSize: defaultBucketSize, - tasksQueuePriority: this.getTasksQueuePriority(), - } - ) - // Flag the worker node as ready at pool startup. - if (this.starting) { - workerNode.info.ready = true + private get ready (): boolean { + if (this.empty) { + return false } - return workerNode + return ( + this.workerNodes.reduce( + (accumulator, workerNode) => + !workerNode.info.dynamic && workerNode.info.ready + ? accumulator + 1 + : accumulator, + 0 + ) >= this.minimumNumberOfWorkers + ) } /** - * 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. + * The pool type. + * + * If it is `'dynamic'`, it provides the `max` property. */ - 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 checkAndEmitEmptyEvent (): void { - if (this.empty) { - this.emitter?.emit(PoolEvents.empty, this.info) - this.readyEventEmitted = false - } - } + protected abstract get type (): PoolType /** - * Removes the worker node from the pool worker nodes. - * @param workerNode - The worker node. + * The approximate pool utilization. + * @returns The pool utilization. */ - private removeWorkerNode (workerNode: IWorkerNode): void { - const workerNodeKey = this.workerNodes.indexOf(workerNode) - if (workerNodeKey !== -1) { - this.workerNodes.splice(workerNodeKey, 1) - this.workerChoiceStrategiesContext?.remove(workerNodeKey) - } - this.checkAndEmitEmptyEvent() - } - - protected flagWorkerNodeAsNotReady (workerNodeKey: number): void { - const workerInfo = this.getWorkerInfo(workerNodeKey) - if (workerInfo != null) { - workerInfo.ready = false + private get utilization (): number { + if (this.startTimestamp == null) { + return 0 } - } - - private hasBackPressure (): boolean { - return ( - this.opts.enableTasksQueue === true && - this.workerNodes.findIndex( - workerNode => !workerNode.hasBackPressure() - ) === -1 + 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 ) + return (totalTasksRunTime + totalTasksWaitTime) / poolTimeCapacity } /** - * Executes the given task on the worker given its worker node key. - * @param workerNodeKey - The worker node key. - * @param task - The task to execute. + * The worker type. */ - private executeTask (workerNodeKey: number, task: Task): void { - this.beforeTaskExecutionHook(workerNodeKey, task) - this.sendToWorker(workerNodeKey, task, task.transferList) - this.checkAndEmitTaskExecutionEvents() - } - - private enqueueTask (workerNodeKey: number, task: Task): number { - const tasksQueueSize = this.workerNodes[workerNodeKey].enqueueTask(task) - this.checkAndEmitTaskQueuingEvents() - return tasksQueueSize - } - - private dequeueTask (workerNodeKey: number): Task | undefined { - return this.workerNodes[workerNodeKey].dequeueTask() - } - - private tasksQueueSize (workerNodeKey: number): number { - return this.workerNodes[workerNodeKey].tasksQueueSize() - } - - 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 - } - this.workerNodes[workerNodeKey].clearTasksQueue() - return flushedTasks - } - - private flushTasksQueues (): void { - for (const workerNodeKey of this.workerNodes.keys()) { - this.flushTasksQueue(workerNodeKey) - } - } + protected abstract get worker (): WorkerType } diff --git a/src/pools/cluster/dynamic.ts b/src/pools/cluster/dynamic.ts index 47799f5e..f13ebf64 100644 --- a/src/pools/cluster/dynamic.ts +++ b/src/pools/cluster/dynamic.ts @@ -36,11 +36,6 @@ export class DynamicClusterPool< ) } - /** @inheritDoc */ - protected shallCreateDynamicWorker (): boolean { - return (!this.full && this.internalBusy()) || this.empty - } - /** @inheritDoc */ protected checkAndEmitDynamicWorkerCreationEvents (): void { if (this.full) { @@ -49,12 +44,17 @@ export class DynamicClusterPool< } /** @inheritDoc */ - protected get type (): PoolType { - return PoolTypes.dynamic + protected shallCreateDynamicWorker (): boolean { + return (!this.full && this.internalBusy()) || this.empty } /** @inheritDoc */ protected get busy (): boolean { return this.full && this.internalBusy() } + + /** @inheritDoc */ + protected get type (): PoolType { + return PoolTypes.dynamic + } } diff --git a/src/pools/cluster/fixed.ts b/src/pools/cluster/fixed.ts index bf61cd47..2a12e080 100644 --- a/src/pools/cluster/fixed.ts +++ b/src/pools/cluster/fixed.ts @@ -1,6 +1,7 @@ import cluster, { type Worker } from 'node:cluster' import type { MessageValue } from '../../utility-types.js' + import { AbstractPool } from '../abstract-pool.js' import { type PoolOptions, type PoolType, PoolTypes } from '../pool.js' import { type WorkerType, WorkerTypes } from '../worker.js' @@ -38,8 +39,16 @@ export class FixedClusterPool< } /** @inheritDoc */ - protected setupHook (): void { - cluster.setupPrimary({ ...this.opts.settings, exec: this.filePath }) + protected checkAndEmitDynamicWorkerCreationEvents (): void { + /* noop */ + } + + /** @inheritDoc */ + protected deregisterWorkerMessageListener( + workerNodeKey: number, + listener: (message: MessageValue) => void + ): void { + this.workerNodes[workerNodeKey].worker.off('message', listener) } /** @inheritDoc */ @@ -48,21 +57,11 @@ export class FixedClusterPool< } /** @inheritDoc */ - protected sendToWorker ( + protected registerOnceWorkerMessageListener( workerNodeKey: number, - message: MessageValue + listener: (message: MessageValue) => void ): void { - this.workerNodes[workerNodeKey]?.worker.send({ - ...message, - workerId: this.getWorkerInfo(workerNodeKey)?.id, - } satisfies MessageValue) - } - - /** @inheritDoc */ - protected sendStartupMessageToWorker (workerNodeKey: number): void { - this.sendToWorker(workerNodeKey, { - ready: false, - }) + this.workerNodes[workerNodeKey].worker.once('message', listener) } /** @inheritDoc */ @@ -74,19 +73,26 @@ export class FixedClusterPool< } /** @inheritDoc */ - protected registerOnceWorkerMessageListener( - workerNodeKey: number, - listener: (message: MessageValue) => void - ): void { - this.workerNodes[workerNodeKey].worker.once('message', listener) + protected sendStartupMessageToWorker (workerNodeKey: number): void { + this.sendToWorker(workerNodeKey, { + ready: false, + }) } /** @inheritDoc */ - protected deregisterWorkerMessageListener( + protected sendToWorker ( workerNodeKey: number, - listener: (message: MessageValue) => void + message: MessageValue ): void { - this.workerNodes[workerNodeKey].worker.off('message', listener) + this.workerNodes[workerNodeKey]?.worker.send({ + ...message, + workerId: this.getWorkerInfo(workerNodeKey)?.id, + } satisfies MessageValue) + } + + /** @inheritDoc */ + protected setupHook (): void { + cluster.setupPrimary({ ...this.opts.settings, exec: this.filePath }) } /** @inheritDoc */ @@ -95,8 +101,8 @@ export class FixedClusterPool< } /** @inheritDoc */ - protected checkAndEmitDynamicWorkerCreationEvents (): void { - /* noop */ + protected get busy (): boolean { + return this.internalBusy() } /** @inheritDoc */ @@ -108,9 +114,4 @@ export class FixedClusterPool< protected get worker (): WorkerType { return WorkerTypes.cluster } - - /** @inheritDoc */ - protected get busy (): boolean { - return this.internalBusy() - } } diff --git a/src/pools/pool.ts b/src/pools/pool.ts index b6c343bb..a9493f9a 100644 --- a/src/pools/pool.ts +++ b/src/pools/pool.ts @@ -25,17 +25,17 @@ import type { * Enumeration of pool types. */ export const PoolTypes: Readonly<{ - fixed: 'fixed' dynamic: 'dynamic' + fixed: 'fixed' }> = Object.freeze({ - /** - * Fixed pool type. - */ - fixed: 'fixed', /** * Dynamic pool type. */ dynamic: 'dynamic', + /** + * Fixed pool type. + */ + fixed: 'fixed', } as const) /** @@ -47,23 +47,23 @@ export type PoolType = keyof typeof PoolTypes * Enumeration of pool events. */ export const PoolEvents: Readonly<{ - ready: 'ready' + backPressure: 'backPressure' busy: 'busy' - full: 'full' - empty: 'empty' destroy: 'destroy' + empty: 'empty' error: 'error' + full: 'full' + ready: 'ready' taskError: 'taskError' - backPressure: 'backPressure' }> = Object.freeze({ - ready: 'ready', + backPressure: 'backPressure', busy: 'busy', - full: 'full', - empty: 'empty', destroy: 'destroy', + empty: 'empty', error: 'error', + full: 'full', + ready: 'ready', taskError: 'taskError', - backPressure: 'backPressure', } as const) /** @@ -75,85 +75,85 @@ export type PoolEvent = keyof typeof PoolEvents * Pool information. */ export interface PoolInfo { - readonly version: string - readonly type: PoolType - readonly worker: WorkerType - readonly started: boolean - readonly ready: boolean - readonly defaultStrategy: WorkerChoiceStrategy - readonly strategyRetries: number - readonly minSize: number - readonly maxSize: number - /** Pool utilization. */ - readonly utilization?: number - /** Pool total worker nodes. */ - readonly workerNodes: number - /** Pool idle worker nodes. */ - readonly idleWorkerNodes: number - /** Pool busy worker nodes. */ - readonly busyWorkerNodes: number - /** Pool tasks stealing worker nodes. */ - readonly stealingWorkerNodes?: number + readonly backPressure?: boolean /** Pool tasks back pressure worker nodes. */ readonly backPressureWorkerNodes?: number - readonly executedTasks: number - readonly executingTasks: number - readonly queuedTasks?: number - readonly maxQueuedTasks?: number - readonly backPressure?: boolean - readonly stolenTasks?: number - readonly failedTasks: number - readonly runTime?: { - readonly minimum: number - readonly maximum: number - readonly average?: number - readonly median?: number - } - readonly waitTime?: { - readonly minimum: number - readonly maximum: number - readonly average?: number - readonly median?: number - } + /** Pool busy worker nodes. */ + readonly busyWorkerNodes: number + readonly defaultStrategy: WorkerChoiceStrategy readonly elu?: { - idle: { - readonly minimum: number - readonly maximum: number + active: { readonly average?: number + readonly maximum: number readonly median?: number - } - active: { readonly minimum: number - readonly maximum: number + } + idle: { readonly average?: number + readonly maximum: number readonly median?: number + readonly minimum: number } utilization: { readonly average?: number readonly median?: number } } + readonly executedTasks: number + readonly executingTasks: number + readonly failedTasks: number + /** Pool idle worker nodes. */ + readonly idleWorkerNodes: number + readonly maxQueuedTasks?: number + readonly maxSize: number + readonly minSize: number + readonly queuedTasks?: number + readonly ready: boolean + readonly runTime?: { + readonly average?: number + readonly maximum: number + readonly median?: number + readonly minimum: number + } + readonly started: boolean + /** Pool tasks stealing worker nodes. */ + readonly stealingWorkerNodes?: number + readonly stolenTasks?: number + readonly strategyRetries: number + readonly type: PoolType + /** Pool utilization. */ + readonly utilization?: number + readonly version: string + readonly waitTime?: { + readonly average?: number + readonly maximum: number + readonly median?: number + readonly minimum: number + } + readonly worker: WorkerType + /** Pool total worker nodes. */ + readonly workerNodes: number } /** * Worker node tasks queue options. */ export interface TasksQueueOptions { - /** - * Maximum tasks queue size per worker node flagging it as back pressured. - * @defaultValue (pool maximum size)^2 - */ - readonly size?: number /** * Maximum number of tasks that can be executed concurrently on a worker node. * @defaultValue 1 */ readonly concurrency?: number /** - * Whether to enable task stealing on idle. - * @defaultValue true + * Maximum tasks queue size per worker node flagging it as back pressured. + * @defaultValue (pool maximum size)^2 */ - readonly taskStealing?: boolean + 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 @@ -165,10 +165,10 @@ export interface TasksQueueOptions { */ readonly tasksStealingRatio?: number /** - * Queued tasks finished timeout in milliseconds at worker node termination. - * @defaultValue 2000 + * Whether to enable task stealing on idle. + * @defaultValue true */ - readonly tasksFinishedTimeout?: number + readonly taskStealing?: boolean } /** @@ -177,15 +177,20 @@ export interface TasksQueueOptions { */ export interface PoolOptions { /** - * A function that will listen for online event on each worker. - * @defaultValue `() => {}` + * Pool events integrated with async resource emission. + * @defaultValue true */ - onlineHandler?: OnlineHandler + enableEvents?: boolean /** - * A function that will listen for message event on each worker. - * @defaultValue `() => {}` + * Pool worker node tasks queue. + * @defaultValue false */ - messageHandler?: MessageHandler + enableTasksQueue?: boolean + /** + * Key/value pairs to add to worker process environment. + * @see https://nodejs.org/api/cluster.html#cluster_cluster_fork_env + */ + env?: Record /** * A function that will listen for error event on each worker. * @defaultValue `() => {}` @@ -197,52 +202,47 @@ export interface PoolOptions { */ exitHandler?: ExitHandler /** - * Whether to start the minimum number of workers at pool initialization. - * @defaultValue true - */ - startWorkers?: boolean - /** - * The default worker choice strategy to use in this pool. - * @defaultValue WorkerChoiceStrategies.ROUND_ROBIN + * A function that will listen for message event on each worker. + * @defaultValue `() => {}` */ - workerChoiceStrategy?: WorkerChoiceStrategy + messageHandler?: MessageHandler /** - * The worker choice strategy options. + * A function that will listen for online event on each worker. + * @defaultValue `() => {}` */ - workerChoiceStrategyOptions?: WorkerChoiceStrategyOptions + onlineHandler?: OnlineHandler /** * Restart worker on error. */ restartWorkerOnError?: boolean /** - * Pool events integrated with async resource emission. - * @defaultValue true + * Cluster settings. + * @see https://nodejs.org/api/cluster.html#cluster_cluster_settings */ - enableEvents?: boolean + settings?: ClusterSettings /** - * Pool worker node tasks queue. - * @defaultValue false + * Whether to start the minimum number of workers at pool initialization. + * @defaultValue true */ - enableTasksQueue?: boolean + startWorkers?: boolean /** * Pool worker node tasks queue options. */ tasksQueueOptions?: TasksQueueOptions /** - * Worker options. - * @see https://nodejs.org/api/worker_threads.html#new-workerfilename-options + * The default worker choice strategy to use in this pool. + * @defaultValue WorkerChoiceStrategies.ROUND_ROBIN */ - workerOptions?: WorkerOptions + workerChoiceStrategy?: WorkerChoiceStrategy /** - * Key/value pairs to add to worker process environment. - * @see https://nodejs.org/api/cluster.html#cluster_cluster_fork_env + * The worker choice strategy options. */ - env?: Record + workerChoiceStrategyOptions?: WorkerChoiceStrategyOptions /** - * Cluster settings. - * @see https://nodejs.org/api/cluster.html#cluster_cluster_settings + * Worker options. + * @see https://nodejs.org/api/worker_threads.html#new-workerfilename-options */ - settings?: ClusterSettings + workerOptions?: WorkerOptions } /** @@ -257,14 +257,22 @@ export interface IPool< Response = unknown > { /** - * Pool information. + * 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 info: PoolInfo + readonly addTaskFunction: ( + name: string, + fn: TaskFunction | TaskFunctionObject + ) => Promise /** - * Pool worker nodes. - * @internal + * Terminates all workers in this pool. */ - readonly workerNodes: IWorkerNode[] + readonly destroy: () => Promise /** * Pool event emitter integrated with async resource. * The async tracking tooling identifier is `poolifier:--pool`. @@ -281,6 +289,15 @@ export interface IPool< * - `'backPressure'`: Emitted when all worker nodes have back pressure (i.e. their tasks queue is 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. @@ -293,6 +310,21 @@ export interface IPool< 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. @@ -305,50 +337,23 @@ export interface IPool< name?: string, transferList?: readonly TransferListItem[] ) => Promise - /** - * Starts the minimum number of workers in this pool. - */ - readonly start: () => void - /** - * Terminates all workers in this pool. - */ - readonly destroy: () => Promise - /** - * Whether the specified task function exists in this pool. - * @param name - The name of the task function. - * @returns `true` if the task function exists, `false` otherwise. - */ - readonly hasTaskFunction: (name: string) => boolean - /** - * Adds a task function to this pool. - * If a task function with the same name already exists, it will be overwritten. - * @param name - The name of the task function. - * @param 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 /** * 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 - /** - * Lists the properties of task functions available in this pool. - * @returns The properties of task functions available in this pool. - */ - readonly listTaskFunctionsProperties: () => TaskFunctionProperties[] /** * 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. @@ -367,17 +372,12 @@ export interface IPool< workerChoiceStrategyOptions: WorkerChoiceStrategyOptions ) => boolean /** - * 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. + * Starts the minimum number of workers in this pool. */ - readonly enableTasksQueue: ( - enable: boolean, - tasksQueueOptions?: TasksQueueOptions - ) => void + readonly start: () => void /** - * Sets the worker node tasks queue options in this pool. - * @param tasksQueueOptions - The worker node tasks queue options. + * Pool worker nodes. + * @internal */ - readonly setTasksQueueOptions: (tasksQueueOptions: TasksQueueOptions) => void + readonly workerNodes: IWorkerNode[] } diff --git a/src/pools/selection-strategies/abstract-worker-choice-strategy.ts b/src/pools/selection-strategies/abstract-worker-choice-strategy.ts index f4273dcd..ef14015d 100644 --- a/src/pools/selection-strategies/abstract-worker-choice-strategy.ts +++ b/src/pools/selection-strategies/abstract-worker-choice-strategy.ts @@ -1,5 +1,4 @@ import type { IPool } from '../pool.js' -import { DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS } from '../utils.js' import type { IWorker } from '../worker.js' import type { IWorkerChoiceStrategy, @@ -7,6 +6,8 @@ import type { TaskStatisticsRequirements, WorkerChoiceStrategyOptions, } from './selection-strategies-types.js' + +import { DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS } from '../utils.js' import { buildWorkerChoiceStrategyOptions, toggleMedianMeasurementStatisticsRequirements, @@ -35,15 +36,15 @@ export abstract class AbstractWorkerChoiceStrategy< /** @inheritDoc */ public readonly strategyPolicy: StrategyPolicy = { - dynamicWorkerUsage: false, dynamicWorkerReady: true, + dynamicWorkerUsage: false, } /** @inheritDoc */ public readonly taskStatisticsRequirements: TaskStatisticsRequirements = { + elu: DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS, runTime: DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS, waitTime: DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS, - elu: DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS, } /** @@ -59,61 +60,6 @@ export abstract class AbstractWorkerChoiceStrategy< this.setOptions(this.opts) } - protected setTaskStatisticsRequirements ( - opts: WorkerChoiceStrategyOptions | undefined - ): void { - toggleMedianMeasurementStatisticsRequirements( - this.taskStatisticsRequirements.runTime, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - opts!.runTime!.median - ) - toggleMedianMeasurementStatisticsRequirements( - this.taskStatisticsRequirements.waitTime, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - opts!.waitTime!.median - ) - toggleMedianMeasurementStatisticsRequirements( - this.taskStatisticsRequirements.elu, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - opts!.elu!.median - ) - } - - protected resetWorkerNodeKeyProperties (): void { - this.nextWorkerNodeKey = 0 - this.previousWorkerNodeKey = 0 - } - - /** @inheritDoc */ - public abstract reset (): boolean - - /** @inheritDoc */ - public abstract update (workerNodeKey: number): boolean - - /** @inheritDoc */ - public abstract choose (): number | undefined - - /** @inheritDoc */ - public abstract remove (workerNodeKey: number): boolean - - /** @inheritDoc */ - public setOptions (opts: WorkerChoiceStrategyOptions | undefined): void { - this.opts = buildWorkerChoiceStrategyOptions( - this.pool, - opts - ) - this.setTaskStatisticsRequirements(this.opts) - } - - /** - * Whether the worker node is ready or not. - * @param workerNodeKey - The worker node key. - * @returns Whether the worker node is ready or not. - */ - protected isWorkerNodeReady (workerNodeKey: number): boolean { - return this.pool.workerNodes[workerNodeKey]?.info?.ready ?? false - } - /** * Check the next worker node key. */ @@ -127,6 +73,19 @@ export abstract class AbstractWorkerChoiceStrategy< } } + /** + * Gets the worker node task ELU. + * If the task statistics require the average ELU, the average ELU is returned. + * If the task statistics require the median ELU, the median ELU is returned. + * @param workerNodeKey - The worker node key. + * @returns The worker node task ELU. + */ + protected getWorkerNodeTaskElu (workerNodeKey: number): number { + return this.taskStatisticsRequirements.elu.median + ? this.pool.workerNodes[workerNodeKey].usage.elu.active.median ?? 0 + : this.pool.workerNodes[workerNodeKey].usage.elu.active.average ?? 0 + } + /** * Gets the worker node task runtime. * If the task statistics require the average runtime, the average runtime is returned. @@ -154,16 +113,17 @@ export abstract class AbstractWorkerChoiceStrategy< } /** - * Gets the worker node task ELU. - * If the task statistics require the average ELU, the average ELU is returned. - * If the task statistics require the median ELU, the median ELU is returned. + * Whether the worker node is ready or not. * @param workerNodeKey - The worker node key. - * @returns The worker node task ELU. + * @returns Whether the worker node is ready or not. */ - protected getWorkerNodeTaskElu (workerNodeKey: number): number { - return this.taskStatisticsRequirements.elu.median - ? this.pool.workerNodes[workerNodeKey].usage.elu.active.median ?? 0 - : this.pool.workerNodes[workerNodeKey].usage.elu.active.average ?? 0 + protected isWorkerNodeReady (workerNodeKey: number): boolean { + return this.pool.workerNodes[workerNodeKey]?.info?.ready ?? false + } + + protected resetWorkerNodeKeyProperties (): void { + this.nextWorkerNodeKey = 0 + this.previousWorkerNodeKey = 0 } /** @@ -176,4 +136,45 @@ export abstract class AbstractWorkerChoiceStrategy< ? workerNodeKey : this.previousWorkerNodeKey } + + protected setTaskStatisticsRequirements ( + opts: undefined | WorkerChoiceStrategyOptions + ): void { + toggleMedianMeasurementStatisticsRequirements( + this.taskStatisticsRequirements.runTime, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + opts!.runTime!.median + ) + toggleMedianMeasurementStatisticsRequirements( + this.taskStatisticsRequirements.waitTime, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + opts!.waitTime!.median + ) + toggleMedianMeasurementStatisticsRequirements( + this.taskStatisticsRequirements.elu, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + 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 88e12b94..92c8546c 100644 --- a/src/pools/selection-strategies/fair-share-worker-choice-strategy.ts +++ b/src/pools/selection-strategies/fair-share-worker-choice-strategy.ts @@ -1,5 +1,6 @@ import type { IPool } from '../pool.js' import type { IWorker } from '../worker.js' + import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy.js' import { type IWorkerChoiceStrategy, @@ -24,17 +25,17 @@ export class FairShareWorkerChoiceStrategy< implements IWorkerChoiceStrategy { /** @inheritDoc */ public readonly taskStatisticsRequirements: TaskStatisticsRequirements = { - runTime: { + elu: { aggregate: true, average: true, median: false, }, - waitTime: { + runTime: { aggregate: true, average: true, median: false, }, - elu: { + waitTime: { aggregate: true, average: true, median: false, @@ -50,33 +51,18 @@ export class FairShareWorkerChoiceStrategy< this.setTaskStatisticsRequirements(this.opts) } - /** @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 - } - - /** @inheritDoc */ - public choose (): number | undefined { - this.setPreviousWorkerNodeKey(this.nextWorkerNodeKey) - this.nextWorkerNodeKey = this.fairShareNextWorkerNodeKey() - return this.nextWorkerNodeKey - } - - /** @inheritDoc */ - public remove (): boolean { - return true + /** + * Computes the worker node key virtual task end timestamp. + * @param workerNodeKey - The worker node key. + * @returns The worker node key virtual task end timestamp. + */ + private computeWorkerNodeVirtualTaskEndTimestamp ( + workerNodeKey: number + ): number { + return this.getWorkerNodeVirtualTaskEndTimestamp( + workerNodeKey, + this.getWorkerNodeVirtualTaskStartTimestamp(workerNodeKey) + ) } private fairShareNextWorkerNodeKey (): number | undefined { @@ -100,20 +86,6 @@ export class FairShareWorkerChoiceStrategy< ) } - /** - * Computes the worker node key virtual task end timestamp. - * @param workerNodeKey - The worker node key. - * @returns The worker node key virtual task end timestamp. - */ - private computeWorkerNodeVirtualTaskEndTimestamp ( - workerNodeKey: number - ): number { - return this.getWorkerNodeVirtualTaskEndTimestamp( - workerNodeKey, - this.getWorkerNodeVirtualTaskStartTimestamp(workerNodeKey) - ) - } - private getWorkerNodeVirtualTaskEndTimestamp ( workerNodeKey: number, workerNodeVirtualTaskStartTimestamp: number @@ -139,4 +111,33 @@ 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 61dd2cff..36871f3c 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 @@ -1,13 +1,14 @@ import type { IPool } from '../pool.js' -import { DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS } from '../utils.js' import type { IWorker } from '../worker.js' -import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy.js' import type { IWorkerChoiceStrategy, TaskStatisticsRequirements, WorkerChoiceStrategyOptions, } from './selection-strategies-types.js' +import { DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS } from '../utils.js' +import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy.js' + /** * Selects the next worker with an interleaved weighted round robin scheduling algorithm. * @typeParam Worker - Type of worker which manages the strategy. @@ -21,25 +22,11 @@ export class InterleavedWeightedRoundRobinWorkerChoiceStrategy< > extends AbstractWorkerChoiceStrategy implements IWorkerChoiceStrategy { - /** @inheritDoc */ - public readonly taskStatisticsRequirements: TaskStatisticsRequirements = { - runTime: { - aggregate: true, - average: true, - median: false, - }, - waitTime: { - aggregate: true, - average: true, - median: false, - }, - elu: DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS, - } - /** * Round id. */ private roundId = 0 + /** * Round weights. */ @@ -52,6 +39,20 @@ export class InterleavedWeightedRoundRobinWorkerChoiceStrategy< * Worker node virtual execution time. */ private workerNodeVirtualTaskExecutionTime = 0 + /** @inheritDoc */ + public readonly taskStatisticsRequirements: TaskStatisticsRequirements = { + elu: DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS, + runTime: { + aggregate: true, + average: true, + median: false, + }, + waitTime: { + aggregate: true, + average: true, + median: false, + }, + } /** @inheritDoc */ public constructor ( @@ -63,18 +64,32 @@ export class InterleavedWeightedRoundRobinWorkerChoiceStrategy< this.roundWeights = this.getRoundWeights() } - /** @inheritDoc */ - public reset (): boolean { - this.resetWorkerNodeKeyProperties() - this.roundId = 0 - this.workerNodeId = 0 - this.workerNodeVirtualTaskExecutionTime = 0 - 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) + ), + ] } - /** @inheritDoc */ - public update (): boolean { - return true + 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 */ @@ -116,23 +131,6 @@ export class InterleavedWeightedRoundRobinWorkerChoiceStrategy< this.interleavedWeightedRoundRobinNextWorkerNodeId() } - 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 remove (workerNodeKey: number): boolean { if (this.pool.workerNodes.length === 0) { @@ -157,19 +155,22 @@ export class InterleavedWeightedRoundRobinWorkerChoiceStrategy< } /** @inheritDoc */ - public setOptions (opts: WorkerChoiceStrategyOptions | undefined): void { + public reset (): boolean { + this.resetWorkerNodeKeyProperties() + this.roundId = 0 + this.workerNodeId = 0 + this.workerNodeVirtualTaskExecutionTime = 0 + return true + } + + /** @inheritDoc */ + public setOptions (opts: undefined | WorkerChoiceStrategyOptions): void { super.setOptions(opts) 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) - ), - ] + /** @inheritDoc */ + public update (): boolean { + return true } } 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 33199368..fba23504 100644 --- a/src/pools/selection-strategies/least-busy-worker-choice-strategy.ts +++ b/src/pools/selection-strategies/least-busy-worker-choice-strategy.ts @@ -1,13 +1,14 @@ import type { IPool } from '../pool.js' -import { DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS } from '../utils.js' import type { IWorker } from '../worker.js' -import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy.js' import type { IWorkerChoiceStrategy, TaskStatisticsRequirements, WorkerChoiceStrategyOptions, } from './selection-strategies-types.js' +import { DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS } from '../utils.js' +import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy.js' + /** * Selects the least busy worker. * @typeParam Worker - Type of worker which manages the strategy. @@ -23,6 +24,7 @@ export class LeastBusyWorkerChoiceStrategy< implements IWorkerChoiceStrategy { /** @inheritDoc */ public readonly taskStatisticsRequirements: TaskStatisticsRequirements = { + elu: DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS, runTime: { aggregate: true, average: false, @@ -33,7 +35,6 @@ export class LeastBusyWorkerChoiceStrategy< average: false, median: false, }, - elu: DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS, } /** @inheritDoc */ @@ -45,14 +46,19 @@ export class LeastBusyWorkerChoiceStrategy< this.setTaskStatisticsRequirements(this.opts) } - /** @inheritDoc */ - public reset (): boolean { - return true - } - - /** @inheritDoc */ - 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 + ) } /** @inheritDoc */ @@ -67,18 +73,13 @@ export class LeastBusyWorkerChoiceStrategy< 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 - ) + /** @inheritDoc */ + public reset (): boolean { + return true + } + + /** @inheritDoc */ + public update (): boolean { + return true } } 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 ead02e85..c7b86b8f 100644 --- a/src/pools/selection-strategies/least-elu-worker-choice-strategy.ts +++ b/src/pools/selection-strategies/least-elu-worker-choice-strategy.ts @@ -1,13 +1,14 @@ import type { IPool } from '../pool.js' -import { DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS } from '../utils.js' import type { IWorker } from '../worker.js' -import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy.js' import type { IWorkerChoiceStrategy, TaskStatisticsRequirements, WorkerChoiceStrategyOptions, } from './selection-strategies-types.js' +import { DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS } from '../utils.js' +import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy.js' + /** * Selects the worker with the least ELU. * @typeParam Worker - Type of worker which manages the strategy. @@ -23,13 +24,13 @@ export class LeastEluWorkerChoiceStrategy< implements IWorkerChoiceStrategy { /** @inheritDoc */ public readonly taskStatisticsRequirements: TaskStatisticsRequirements = { - runTime: DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS, - waitTime: DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS, elu: { aggregate: true, average: false, median: false, }, + runTime: DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS, + waitTime: DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS, } /** @inheritDoc */ @@ -41,14 +42,17 @@ export class LeastEluWorkerChoiceStrategy< this.setTaskStatisticsRequirements(this.opts) } - /** @inheritDoc */ - public reset (): boolean { - return true - } - - /** @inheritDoc */ - 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 + ) } /** @inheritDoc */ @@ -63,16 +67,13 @@ export class LeastEluWorkerChoiceStrategy< 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 - ) + /** @inheritDoc */ + public reset (): boolean { + return true + } + + /** @inheritDoc */ + public update (): boolean { + return true } } 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 1ee6b7b8..067e1238 100644 --- a/src/pools/selection-strategies/least-used-worker-choice-strategy.ts +++ b/src/pools/selection-strategies/least-used-worker-choice-strategy.ts @@ -1,11 +1,12 @@ import type { IPool } from '../pool.js' import type { IWorker } from '../worker.js' -import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy.js' import type { IWorkerChoiceStrategy, WorkerChoiceStrategyOptions, } from './selection-strategies-types.js' +import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy.js' + /** * Selects the least used worker. * @typeParam Worker - Type of worker which manages the strategy. @@ -27,14 +28,18 @@ export class LeastUsedWorkerChoiceStrategy< super(pool, opts) } - /** @inheritDoc */ - public reset (): boolean { - return true - } - - /** @inheritDoc */ - 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 + ) } /** @inheritDoc */ @@ -49,17 +54,13 @@ export class LeastUsedWorkerChoiceStrategy< 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 - ) + /** @inheritDoc */ + public reset (): boolean { + return true + } + + /** @inheritDoc */ + public update (): boolean { + return true } } 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 1f5c499e..39cb3a08 100644 --- a/src/pools/selection-strategies/round-robin-worker-choice-strategy.ts +++ b/src/pools/selection-strategies/round-robin-worker-choice-strategy.ts @@ -1,11 +1,12 @@ import type { IPool } from '../pool.js' import type { IWorker } from '../worker.js' -import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy.js' import type { IWorkerChoiceStrategy, WorkerChoiceStrategyOptions, } from './selection-strategies-types.js' +import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy.js' + /** * Selects the next worker in a round robin fashion. * @typeParam Worker - Type of worker which manages the strategy. @@ -27,15 +28,12 @@ export class RoundRobinWorkerChoiceStrategy< super(pool, opts) } - /** @inheritDoc */ - public reset (): boolean { - this.resetWorkerNodeKeyProperties() - return true - } - - /** @inheritDoc */ - 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 } /** @inheritDoc */ @@ -68,11 +66,14 @@ export class RoundRobinWorkerChoiceStrategy< return true } - private roundRobinNextWorkerNodeKey (): number | undefined { - this.nextWorkerNodeKey = - this.nextWorkerNodeKey === this.pool.workerNodes.length - 1 - ? 0 - : (this.nextWorkerNodeKey ?? this.previousWorkerNodeKey) + 1 - return this.nextWorkerNodeKey + /** @inheritDoc */ + public reset (): boolean { + this.resetWorkerNodeKeyProperties() + return true + } + + /** @inheritDoc */ + public update (): boolean { + return true } } diff --git a/src/pools/selection-strategies/selection-strategies-types.ts b/src/pools/selection-strategies/selection-strategies-types.ts index 745b140a..236a3adf 100644 --- a/src/pools/selection-strategies/selection-strategies-types.ts +++ b/src/pools/selection-strategies/selection-strategies-types.ts @@ -2,22 +2,23 @@ * Enumeration of worker choice strategies. */ export const WorkerChoiceStrategies: Readonly<{ - ROUND_ROBIN: 'ROUND_ROBIN' - LEAST_USED: 'LEAST_USED' + FAIR_SHARE: 'FAIR_SHARE' + INTERLEAVED_WEIGHTED_ROUND_ROBIN: 'INTERLEAVED_WEIGHTED_ROUND_ROBIN' LEAST_BUSY: 'LEAST_BUSY' LEAST_ELU: 'LEAST_ELU' - FAIR_SHARE: 'FAIR_SHARE' + LEAST_USED: 'LEAST_USED' + ROUND_ROBIN: 'ROUND_ROBIN' WEIGHTED_ROUND_ROBIN: 'WEIGHTED_ROUND_ROBIN' - INTERLEAVED_WEIGHTED_ROUND_ROBIN: 'INTERLEAVED_WEIGHTED_ROUND_ROBIN' }> = Object.freeze({ /** - * Round robin worker selection strategy. + * Fair share worker selection strategy. */ - ROUND_ROBIN: 'ROUND_ROBIN', + FAIR_SHARE: 'FAIR_SHARE', /** - * Least used worker selection strategy. + * Interleaved weighted round robin worker selection strategy. + * @experimental */ - LEAST_USED: 'LEAST_USED', + INTERLEAVED_WEIGHTED_ROUND_ROBIN: 'INTERLEAVED_WEIGHTED_ROUND_ROBIN', /** * Least busy worker selection strategy. */ @@ -27,18 +28,17 @@ export const WorkerChoiceStrategies: Readonly<{ */ LEAST_ELU: 'LEAST_ELU', /** - * Fair share worker selection strategy. + * Least used worker selection strategy. */ - FAIR_SHARE: 'FAIR_SHARE', + LEAST_USED: 'LEAST_USED', /** - * Weighted round robin worker selection strategy. + * Round robin worker selection strategy. */ - WEIGHTED_ROUND_ROBIN: 'WEIGHTED_ROUND_ROBIN', + ROUND_ROBIN: 'ROUND_ROBIN', /** - * Interleaved weighted round robin worker selection strategy. - * @experimental + * Weighted round robin worker selection strategy. */ - INTERLEAVED_WEIGHTED_ROUND_ROBIN: 'INTERLEAVED_WEIGHTED_ROUND_ROBIN', + WEIGHTED_ROUND_ROBIN: 'WEIGHTED_ROUND_ROBIN', } as const) /** @@ -50,13 +50,13 @@ export type WorkerChoiceStrategy = keyof typeof WorkerChoiceStrategies * Enumeration of measurements. */ export const Measurements: Readonly<{ + elu: 'elu' runTime: 'runTime' waitTime: 'waitTime' - elu: 'elu' }> = Object.freeze({ + elu: 'elu', runTime: 'runTime', waitTime: 'waitTime', - elu: 'elu', } as const) /** @@ -78,6 +78,11 @@ export interface MeasurementOptions { * Worker choice strategy options. */ export interface WorkerChoiceStrategyOptions { + /** + * Event loop utilization options. + * @defaultValue \{ median: false \} + */ + readonly elu?: MeasurementOptions /** * Measurement to use in worker choice strategy supporting it. */ @@ -92,11 +97,6 @@ export interface WorkerChoiceStrategyOptions { * @defaultValue \{ median: false \} */ readonly waitTime?: MeasurementOptions - /** - * Event loop utilization options. - * @defaultValue \{ median: false \} - */ - readonly elu?: 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. @@ -129,6 +129,10 @@ export interface MeasurementStatisticsRequirements { * @internal */ export interface TaskStatisticsRequirements { + /** + * Tasks event loop utilization requirements. + */ + readonly elu: MeasurementStatisticsRequirements /** * Tasks runtime requirements. */ @@ -137,10 +141,6 @@ export interface TaskStatisticsRequirements { * Tasks wait time requirements. */ readonly waitTime: MeasurementStatisticsRequirements - /** - * Tasks event loop utilization requirements. - */ - readonly elu: MeasurementStatisticsRequirements } /** @@ -148,14 +148,14 @@ export interface TaskStatisticsRequirements { * @internal */ export interface StrategyPolicy { - /** - * Expects tasks execution on the newly created dynamic worker. - */ - readonly dynamicWorkerUsage: boolean /** * Expects the newly created dynamic worker to be flagged as ready. */ readonly dynamicWorkerReady: boolean + /** + * Expects tasks execution on the newly created dynamic worker. + */ + readonly dynamicWorkerUsage: boolean } /** @@ -163,25 +163,6 @@ export interface StrategyPolicy { * @internal */ export interface IWorkerChoiceStrategy { - /** - * Strategy policy. - */ - readonly strategyPolicy: StrategyPolicy - /** - * Tasks statistics requirements. - */ - readonly taskStatisticsRequirements: TaskStatisticsRequirements - /** - * Resets strategy internals. - * @returns `true` if the reset is successful, `false` otherwise. - */ - readonly reset: () => boolean - /** - * 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 /** * Chooses a worker node in the pool and returns its key. * If no worker nodes are not eligible, `undefined` is returned. @@ -195,9 +176,28 @@ export interface IWorkerChoiceStrategy { * @returns `true` if the worker node key is removed, `false` otherwise. */ readonly remove: (workerNodeKey: number) => boolean + /** + * Resets strategy internals. + * @returns `true` if the reset is successful, `false` otherwise. + */ + readonly reset: () => boolean /** * Sets the worker choice strategy options. * @param opts - The worker choice strategy options. */ - readonly setOptions: (opts: WorkerChoiceStrategyOptions | undefined) => void + readonly setOptions: (opts: undefined | WorkerChoiceStrategyOptions) => void + /** + * Strategy policy. + */ + readonly strategyPolicy: StrategyPolicy + /** + * Tasks statistics requirements. + */ + 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 } diff --git a/src/pools/selection-strategies/selection-strategies-utils.ts b/src/pools/selection-strategies/selection-strategies-utils.ts index 1c0b91b4..f4268a0b 100644 --- a/src/pools/selection-strategies/selection-strategies-utils.ts +++ b/src/pools/selection-strategies/selection-strategies-utils.ts @@ -2,6 +2,8 @@ import { cpus } from 'node:os' import type { IPool } from '../pool.js' import type { IWorker } from '../worker.js' +import type { WorkerChoiceStrategiesContext } from './worker-choice-strategies-context.js' + import { FairShareWorkerChoiceStrategy } from './fair-share-worker-choice-strategy.js' import { InterleavedWeightedRoundRobinWorkerChoiceStrategy } from './interleaved-weighted-round-robin-worker-choice-strategy.js' import { LeastBusyWorkerChoiceStrategy } from './least-busy-worker-choice-strategy.js' @@ -18,7 +20,6 @@ import { type WorkerChoiceStrategyOptions, } from './selection-strategies-types.js' import { WeightedRoundRobinWorkerChoiceStrategy } from './weighted-round-robin-worker-choice-strategy.js' -import type { WorkerChoiceStrategiesContext } from './worker-choice-strategies-context.js' const estimatedCpuSpeed = (): number => { const runs = 150000000 @@ -93,9 +94,9 @@ export const buildWorkerChoiceStrategyOptions = < opts.weights = opts.weights ?? getDefaultWeights(pool.info.maxSize) return { ...{ + elu: { median: false }, runTime: { median: false }, waitTime: { median: false }, - elu: { median: false }, }, ...opts, } @@ -123,8 +124,8 @@ export const buildWorkerChoiceStrategiesPolicy = ( ([_, workerChoiceStrategy]) => workerChoiceStrategy.strategyPolicy ) return { - dynamicWorkerUsage: policies.some(p => p.dynamicWorkerUsage), dynamicWorkerReady: policies.some(p => p.dynamicWorkerReady), + dynamicWorkerUsage: policies.some(p => p.dynamicWorkerUsage), } } @@ -137,6 +138,11 @@ export const buildWorkerChoiceStrategiesTaskStatisticsRequirements = ( workerChoiceStrategy.taskStatisticsRequirements ) return { + elu: { + aggregate: taskStatisticsRequirements.some(r => r.elu.aggregate), + average: taskStatisticsRequirements.some(r => r.elu.average), + median: taskStatisticsRequirements.some(r => r.elu.median), + }, runTime: { aggregate: taskStatisticsRequirements.some(r => r.runTime.aggregate), average: taskStatisticsRequirements.some(r => r.runTime.average), @@ -147,11 +153,6 @@ export const buildWorkerChoiceStrategiesTaskStatisticsRequirements = ( average: taskStatisticsRequirements.some(r => r.waitTime.average), median: taskStatisticsRequirements.some(r => r.waitTime.median), }, - elu: { - aggregate: taskStatisticsRequirements.some(r => r.elu.aggregate), - average: taskStatisticsRequirements.some(r => r.elu.average), - median: taskStatisticsRequirements.some(r => r.elu.median), - }, } } @@ -162,25 +163,25 @@ export const getWorkerChoiceStrategy = ( opts?: WorkerChoiceStrategyOptions ): IWorkerChoiceStrategy => { switch (workerChoiceStrategy) { - case WorkerChoiceStrategies.ROUND_ROBIN: - return new (RoundRobinWorkerChoiceStrategy.bind(context))(pool, opts) - case WorkerChoiceStrategies.LEAST_USED: - return new (LeastUsedWorkerChoiceStrategy.bind(context))(pool, opts) + case WorkerChoiceStrategies.FAIR_SHARE: + return new (FairShareWorkerChoiceStrategy.bind(context))(pool, opts) + case WorkerChoiceStrategies.INTERLEAVED_WEIGHTED_ROUND_ROBIN: + return new (InterleavedWeightedRoundRobinWorkerChoiceStrategy.bind( + context + ))(pool, opts) case WorkerChoiceStrategies.LEAST_BUSY: return new (LeastBusyWorkerChoiceStrategy.bind(context))(pool, opts) case WorkerChoiceStrategies.LEAST_ELU: return new (LeastEluWorkerChoiceStrategy.bind(context))(pool, opts) - case WorkerChoiceStrategies.FAIR_SHARE: - return new (FairShareWorkerChoiceStrategy.bind(context))(pool, opts) + case WorkerChoiceStrategies.LEAST_USED: + return new (LeastUsedWorkerChoiceStrategy.bind(context))(pool, opts) + case WorkerChoiceStrategies.ROUND_ROBIN: + return new (RoundRobinWorkerChoiceStrategy.bind(context))(pool, opts) case WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN: return new (WeightedRoundRobinWorkerChoiceStrategy.bind(context))( pool, opts ) - case WorkerChoiceStrategies.INTERLEAVED_WEIGHTED_ROUND_ROBIN: - return new (InterleavedWeightedRoundRobinWorkerChoiceStrategy.bind( - context - ))(pool, opts) default: throw new Error( // eslint-disable-next-line @typescript-eslint/restrict-template-expressions 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 ffd4fc39..3cb06272 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 @@ -1,13 +1,14 @@ import type { IPool } from '../pool.js' -import { DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS } from '../utils.js' import type { IWorker } from '../worker.js' -import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy.js' import type { IWorkerChoiceStrategy, TaskStatisticsRequirements, WorkerChoiceStrategyOptions, } from './selection-strategies-types.js' +import { DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS } from '../utils.js' +import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy.js' + /** * Selects the next worker with a weighted round robin scheduling algorithm. * Loosely modeled after the weighted round robin queueing algorithm: https://en.wikipedia.org/wiki/Weighted_round_robin. @@ -22,8 +23,14 @@ export class WeightedRoundRobinWorkerChoiceStrategy< > extends AbstractWorkerChoiceStrategy implements IWorkerChoiceStrategy { + /** + * Worker node virtual execution time. + */ + private workerNodeVirtualTaskExecutionTime = 0 + /** @inheritDoc */ public readonly taskStatisticsRequirements: TaskStatisticsRequirements = { + elu: DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS, runTime: { aggregate: true, average: true, @@ -34,14 +41,8 @@ export class WeightedRoundRobinWorkerChoiceStrategy< average: true, median: false, }, - elu: DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS, } - /** - * Worker node virtual execution time. - */ - private workerNodeVirtualTaskExecutionTime = 0 - /** @inheritDoc */ public constructor ( pool: IPool, @@ -51,16 +52,26 @@ export class WeightedRoundRobinWorkerChoiceStrategy< this.setTaskStatisticsRequirements(this.opts) } - /** @inheritDoc */ - public reset (): boolean { - this.resetWorkerNodeKeyProperties() - this.workerNodeVirtualTaskExecutionTime = 0 - return true - } - - /** @inheritDoc */ - 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 } /** @inheritDoc */ @@ -92,25 +103,15 @@ export class WeightedRoundRobinWorkerChoiceStrategy< 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 + /** @inheritDoc */ + public reset (): boolean { + this.resetWorkerNodeKeyProperties() + this.workerNodeVirtualTaskExecutionTime = 0 + return true + } + + /** @inheritDoc */ + public update (): boolean { + return true } } diff --git a/src/pools/selection-strategies/worker-choice-strategies-context.ts b/src/pools/selection-strategies/worker-choice-strategies-context.ts index 65024212..a1ccb4e9 100644 --- a/src/pools/selection-strategies/worker-choice-strategies-context.ts +++ b/src/pools/selection-strategies/worker-choice-strategies-context.ts @@ -7,6 +7,7 @@ import type { WorkerChoiceStrategy, WorkerChoiceStrategyOptions, } from './selection-strategies-types.js' + import { WorkerChoiceStrategies } from './selection-strategies-types.js' import { buildWorkerChoiceStrategiesPolicy, @@ -28,14 +29,14 @@ export class WorkerChoiceStrategiesContext< Response = unknown > { /** - * The number of worker choice strategies execution retries. + * The default worker choice strategy in the context. */ - public retriesCount: number + private defaultWorkerChoiceStrategy: WorkerChoiceStrategy /** - * The default worker choice strategy in the context. + * The maximum number of worker choice strategies execution retries. */ - private defaultWorkerChoiceStrategy: WorkerChoiceStrategy + private readonly retries: number /** * The worker choice strategies registered in the context. @@ -56,9 +57,9 @@ export class WorkerChoiceStrategiesContext< private workerChoiceStrategiesTaskStatisticsRequirements: TaskStatisticsRequirements /** - * The maximum number of worker choice strategies execution retries. + * The number of worker choice strategies execution retries. */ - private readonly retries: number + public retriesCount: number /** * Worker choice strategies context constructor. @@ -97,62 +98,29 @@ export class WorkerChoiceStrategiesContext< } /** - * Gets the active worker choice strategies in the context policy. - * @returns The strategies policy. - */ - public getPolicy (): StrategyPolicy { - return this.workerChoiceStrategiesPolicy - } - - /** - * Gets the active worker choice strategies in the context task statistics requirements. - * @returns The strategies task statistics requirements. - */ - public getTaskStatisticsRequirements (): TaskStatisticsRequirements { - return this.workerChoiceStrategiesTaskStatisticsRequirements - } - - /** - * Sets the default worker choice strategy to use in the context. - * @param workerChoiceStrategy - The default worker choice strategy to set. + * 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. */ - public setDefaultWorkerChoiceStrategy ( + private addWorkerChoiceStrategy ( workerChoiceStrategy: WorkerChoiceStrategy, + pool: IPool, opts?: WorkerChoiceStrategyOptions - ): void { - if (workerChoiceStrategy !== this.defaultWorkerChoiceStrategy) { - this.defaultWorkerChoiceStrategy = workerChoiceStrategy - this.addWorkerChoiceStrategy(workerChoiceStrategy, this.pool, opts) + ): Map { + if (!this.workerChoiceStrategies.has(workerChoiceStrategy)) { + return this.workerChoiceStrategies.set( + workerChoiceStrategy, + getWorkerChoiceStrategy( + workerChoiceStrategy, + pool, + this, + opts + ) + ) } - } - - /** - * Updates the worker node key in the active worker choice strategies in the context internals. - * @param workerNodeKey - The worker node key. - * @returns `true` if the update is successful, `false` otherwise. - */ - public update (workerNodeKey: number): boolean { - return Array.from( - this.workerChoiceStrategies, - ([_, workerChoiceStrategy]) => workerChoiceStrategy.update(workerNodeKey) - ).every(r => r) - } - - /** - * Executes the given worker choice strategy in the context algorithm. - * @param workerChoiceStrategy - The worker choice strategy algorithm to execute. @defaultValue this.defaultWorkerChoiceStrategy - * @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. - */ - public execute ( - workerChoiceStrategy: WorkerChoiceStrategy = this - .defaultWorkerChoiceStrategy - ): number { - return this.executeStrategy( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.workerChoiceStrategies.get(workerChoiceStrategy)! - ) + return this.workerChoiceStrategies } /** @@ -181,6 +149,49 @@ export class WorkerChoiceStrategiesContext< 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 + * @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. + */ + public execute ( + workerChoiceStrategy: WorkerChoiceStrategy = this + .defaultWorkerChoiceStrategy + ): number { + return this.executeStrategy( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.workerChoiceStrategies.get(workerChoiceStrategy)! + ) + } + + /** + * Gets the active worker choice strategies in the context policy. + * @returns The strategies policy. + */ + public getPolicy (): StrategyPolicy { + return this.workerChoiceStrategiesPolicy + } + + /** + * Gets the active worker choice strategies in the context task statistics requirements. + * @returns The strategies task statistics requirements. + */ + public getTaskStatisticsRequirements (): TaskStatisticsRequirements { + return this.workerChoiceStrategiesTaskStatisticsRequirements + } + /** * Removes the worker node key from the active worker choice strategies in the context. * @param workerNodeKey - The worker node key. @@ -193,11 +204,26 @@ export class WorkerChoiceStrategiesContext< ).every(r => r) } + /** + * Sets the default worker choice strategy to use in the context. + * @param workerChoiceStrategy - The default worker choice strategy to set. + * @param opts - The worker choice strategy options. + */ + public setDefaultWorkerChoiceStrategy ( + workerChoiceStrategy: WorkerChoiceStrategy, + opts?: WorkerChoiceStrategyOptions + ): void { + if (workerChoiceStrategy !== this.defaultWorkerChoiceStrategy) { + this.defaultWorkerChoiceStrategy = workerChoiceStrategy + this.addWorkerChoiceStrategy(workerChoiceStrategy, this.pool, opts) + } + } + /** * Sets the active worker choice strategies in the context options. * @param opts - The worker choice strategy options. */ - public setOptions (opts: WorkerChoiceStrategyOptions | undefined): void { + public setOptions (opts: undefined | WorkerChoiceStrategyOptions): void { for (const workerChoiceStrategy of this.workerChoiceStrategies.values()) { workerChoiceStrategy.setOptions(opts) } @@ -232,39 +258,14 @@ 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 - } - - /** - * 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. + * Updates the worker node key in the active worker choice strategies in the context internals. + * @param workerNodeKey - The worker node key. + * @returns `true` if the update is successful, `false` otherwise. */ - private removeWorkerChoiceStrategy ( - workerChoiceStrategy: WorkerChoiceStrategy - ): boolean { - return this.workerChoiceStrategies.delete(workerChoiceStrategy) + public update (workerNodeKey: number): boolean { + return Array.from( + this.workerChoiceStrategies, + ([_, workerChoiceStrategy]) => workerChoiceStrategy.update(workerNodeKey) + ).every(r => r) } } diff --git a/src/pools/thread/dynamic.ts b/src/pools/thread/dynamic.ts index 0d088da1..fc4115be 100644 --- a/src/pools/thread/dynamic.ts +++ b/src/pools/thread/dynamic.ts @@ -36,11 +36,6 @@ export class DynamicThreadPool< ) } - /** @inheritDoc */ - protected shallCreateDynamicWorker (): boolean { - return (!this.full && this.internalBusy()) || this.empty - } - /** @inheritDoc */ protected checkAndEmitDynamicWorkerCreationEvents (): void { if (this.full) { @@ -49,12 +44,17 @@ export class DynamicThreadPool< } /** @inheritDoc */ - protected get type (): PoolType { - return PoolTypes.dynamic + protected shallCreateDynamicWorker (): boolean { + return (!this.full && this.internalBusy()) || this.empty } /** @inheritDoc */ protected get busy (): boolean { return this.full && this.internalBusy() } + + /** @inheritDoc */ + protected get type (): PoolType { + return PoolTypes.dynamic + } } diff --git a/src/pools/thread/fixed.ts b/src/pools/thread/fixed.ts index 69b3c307..f65a0cd5 100644 --- a/src/pools/thread/fixed.ts +++ b/src/pools/thread/fixed.ts @@ -5,6 +5,7 @@ import { } from 'node:worker_threads' import type { MessageValue } from '../../utility-types.js' + import { AbstractPool } from '../abstract-pool.js' import { type PoolOptions, type PoolType, PoolTypes } from '../pool.js' import { type WorkerType, WorkerTypes } from '../worker.js' @@ -42,70 +43,75 @@ export class FixedThreadPool< } /** @inheritDoc */ - protected isMain (): boolean { - return isMainThread + protected checkAndEmitDynamicWorkerCreationEvents (): void { + /* noop */ } /** @inheritDoc */ - protected sendToWorker ( + protected deregisterWorkerMessageListener( workerNodeKey: number, - message: MessageValue, - transferList?: readonly TransferListItem[] + listener: (message: MessageValue) => void ): void { - this.workerNodes[workerNodeKey]?.messageChannel?.port1.postMessage( - { - ...message, - workerId: this.getWorkerInfo(workerNodeKey)?.id, - } satisfies MessageValue, - transferList + this.workerNodes[workerNodeKey].messageChannel?.port1.off( + 'message', + listener ) } /** @inheritDoc */ - protected sendStartupMessageToWorker (workerNodeKey: number): void { - const workerNode = this.workerNodes[workerNodeKey] - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const port2 = workerNode.messageChannel!.port2 - workerNode.worker.postMessage( - { - ready: false, - workerId: this.getWorkerInfo(workerNodeKey)?.id, - port: port2, - } satisfies MessageValue, - [port2] - ) + protected isMain (): boolean { + return isMainThread } /** @inheritDoc */ - protected registerWorkerMessageListener( + protected registerOnceWorkerMessageListener( workerNodeKey: number, listener: (message: MessageValue) => void ): void { - this.workerNodes[workerNodeKey].messageChannel?.port1.on( + this.workerNodes[workerNodeKey].messageChannel?.port1.once( 'message', listener ) } /** @inheritDoc */ - protected registerOnceWorkerMessageListener( + protected registerWorkerMessageListener( workerNodeKey: number, listener: (message: MessageValue) => void ): void { - this.workerNodes[workerNodeKey].messageChannel?.port1.once( + this.workerNodes[workerNodeKey].messageChannel?.port1.on( 'message', listener ) } /** @inheritDoc */ - protected deregisterWorkerMessageListener( + protected sendStartupMessageToWorker (workerNodeKey: number): void { + const workerNode = this.workerNodes[workerNodeKey] + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const port2 = workerNode.messageChannel!.port2 + workerNode.worker.postMessage( + { + port: port2, + ready: false, + workerId: this.getWorkerInfo(workerNodeKey)?.id, + } satisfies MessageValue, + [port2] + ) + } + + /** @inheritDoc */ + protected sendToWorker ( workerNodeKey: number, - listener: (message: MessageValue) => void + message: MessageValue, + transferList?: readonly TransferListItem[] ): void { - this.workerNodes[workerNodeKey].messageChannel?.port1.off( - 'message', - listener + this.workerNodes[workerNodeKey]?.messageChannel?.port1.postMessage( + { + ...message, + workerId: this.getWorkerInfo(workerNodeKey)?.id, + } satisfies MessageValue, + transferList ) } @@ -115,8 +121,8 @@ export class FixedThreadPool< } /** @inheritDoc */ - protected checkAndEmitDynamicWorkerCreationEvents (): void { - /* noop */ + protected get busy (): boolean { + return this.internalBusy() } /** @inheritDoc */ @@ -128,9 +134,4 @@ export class FixedThreadPool< protected get worker (): WorkerType { return WorkerTypes.thread } - - /** @inheritDoc */ - protected get busy (): boolean { - return this.internalBusy() - } } diff --git a/src/pools/utils.ts b/src/pools/utils.ts index 7b3c21fa..3c88dd45 100644 --- a/src/pools/utils.ts +++ b/src/pools/utils.ts @@ -8,14 +8,15 @@ import { } from 'node:worker_threads' import type { MessageValue, Task } from '../utility-types.js' -import { average, isPlainObject, max, median, min } from '../utils.js' import type { TasksQueueOptions } from './pool.js' +import type { WorkerChoiceStrategiesContext } from './selection-strategies/worker-choice-strategies-context.js' + +import { average, isPlainObject, max, median, min } from '../utils.js' import { type MeasurementStatisticsRequirements, WorkerChoiceStrategies, type WorkerChoiceStrategy, } from './selection-strategies/selection-strategies-types.js' -import type { WorkerChoiceStrategiesContext } from './selection-strategies/worker-choice-strategies-context.js' import { type IWorker, type IWorkerNode, @@ -40,12 +41,12 @@ export const getDefaultTasksQueueOptions = ( poolMaxSize: number ): Required => { return { - size: Math.pow(poolMaxSize, 2), concurrency: 1, - taskStealing: true, + size: Math.pow(poolMaxSize, 2), + tasksFinishedTimeout: 2000, tasksStealingOnBackPressure: true, tasksStealingRatio: 0.6, - tasksFinishedTimeout: 2000, + taskStealing: true, } } @@ -102,7 +103,7 @@ export const checkValidPriority = (priority: number | undefined): void => { } export const checkValidWorkerChoiceStrategy = ( - workerChoiceStrategy: WorkerChoiceStrategy | undefined + workerChoiceStrategy: undefined | WorkerChoiceStrategy ): void => { if ( workerChoiceStrategy != null && @@ -167,9 +168,9 @@ export const checkValidTasksQueueOptions = ( } export const checkWorkerNodeArguments = ( - type: WorkerType | undefined, + type: undefined | WorkerType, filePath: string | undefined, - opts: WorkerNodeOptions | undefined + opts: undefined | WorkerNodeOptions ): void => { if (type == null) { throw new TypeError('Cannot construct a worker node without a worker type') @@ -289,8 +290,8 @@ export const updateWaitTimeWorkerUsage = < Response = unknown >( workerChoiceStrategiesContext: - | WorkerChoiceStrategiesContext - | undefined, + | undefined + | WorkerChoiceStrategiesContext, workerUsage: WorkerUsage, task: Task ): void => { @@ -328,8 +329,8 @@ export const updateRunTimeWorkerUsage = < Response = unknown >( workerChoiceStrategiesContext: - | WorkerChoiceStrategiesContext - | undefined, + | undefined + | WorkerChoiceStrategiesContext, workerUsage: WorkerUsage, message: MessageValue ): void => { @@ -349,8 +350,8 @@ export const updateEluWorkerUsage = < Response = unknown >( workerChoiceStrategiesContext: - | WorkerChoiceStrategiesContext - | undefined, + | undefined + | WorkerChoiceStrategiesContext, workerUsage: WorkerUsage, message: MessageValue ): void => { @@ -390,13 +391,13 @@ export const createWorker = ( opts: { env?: Record; workerOptions?: WorkerOptions } ): Worker => { switch (type) { + case WorkerTypes.cluster: + return cluster.fork(opts.env) as unknown as Worker case WorkerTypes.thread: return new ThreadWorker(filePath, { env: SHARE_ENV, ...opts.workerOptions, }) as unknown as Worker - case WorkerTypes.cluster: - return cluster.fork(opts.env) as unknown as Worker default: // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error(`Unknown worker type '${type}'`) @@ -409,7 +410,7 @@ export const createWorker = ( * @returns The worker type of the given worker. * @internal */ -export const getWorkerType = (worker: IWorker): WorkerType | undefined => { +export const getWorkerType = (worker: IWorker): undefined | WorkerType => { if (worker instanceof ThreadWorker) { return WorkerTypes.thread } else if (worker instanceof ClusterWorker) { @@ -447,8 +448,8 @@ export const waitWorkerNodeEvents = async < return } switch (workerNodeEvent) { - case 'idle': case 'backPressure': + case 'idle': case 'taskFinished': workerNode.on(workerNodeEvent, () => { ++events diff --git a/src/pools/worker-node.ts b/src/pools/worker-node.ts index 8748b414..b29601b3 100644 --- a/src/pools/worker-node.ts +++ b/src/pools/worker-node.ts @@ -1,9 +1,10 @@ import { EventEmitter } from 'node:events' import { MessageChannel } from 'node:worker_threads' +import type { Task } from '../utility-types.js' + import { CircularBuffer } from '../circular-buffer.js' import { PriorityQueue } from '../queues/priority-queue.js' -import type { Task } from '../utility-types.js' import { DEFAULT_TASK_NAME } from '../utils.js' import { checkWorkerNodeArguments, @@ -32,21 +33,21 @@ import { export class WorkerNode extends EventEmitter implements IWorkerNode { - /** @inheritdoc */ - public readonly worker: Worker + private setBackPressureFlag: boolean + private readonly taskFunctionsUsage: Map + private readonly tasksQueue: PriorityQueue> /** @inheritdoc */ public readonly info: WorkerInfo /** @inheritdoc */ - public usage: WorkerUsage + public messageChannel?: MessageChannel /** @inheritdoc */ public strategyData?: StrategyData /** @inheritdoc */ - public messageChannel?: MessageChannel - /** @inheritdoc */ public tasksQueueBackPressureSize: number - private readonly tasksQueue: PriorityQueue> - private setBackPressureFlag: boolean - private readonly taskFunctionsUsage: Map + /** @inheritdoc */ + public usage: WorkerUsage + /** @inheritdoc */ + public readonly worker: Worker /** * Constructs a new worker node. @@ -76,30 +77,126 @@ 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 + } + } + + 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 setTasksQueuePriority (enablePriority: boolean): void { - this.tasksQueue.enablePriority = enablePriority + public clearTasksQueue (): void { + this.tasksQueue.clear() } /** @inheritdoc */ - public tasksQueueSize (): number { - return this.tasksQueue.size + public deleteTaskFunctionWorkerUsage (name: string): boolean { + return this.taskFunctionsUsage.delete(name) } /** @inheritdoc */ - public enqueueTask (task: Task): number { - const tasksQueueSize = this.tasksQueue.enqueue(task, task.priority) - if ( - !this.setBackPressureFlag && - this.hasBackPressure() && - !this.info.backPressure - ) { - this.setBackPressureFlag = true - this.info.backPressure = true - this.emit('backPressure', { workerId: this.info.id }) - this.setBackPressureFlag = false - } - return tasksQueueSize + public dequeueLastPrioritizedTask (): Task | undefined { + // Start from the last empty or partially filled bucket + return this.dequeueTask(this.tasksQueue.buckets + 1) } /** @inheritdoc */ @@ -118,63 +215,23 @@ export class WorkerNode } /** @inheritdoc */ - public dequeueLastPrioritizedTask (): Task | undefined { - // Start from the last empty or partially filled bucket - return this.dequeueTask(this.tasksQueue.buckets + 1) - } - - /** @inheritdoc */ - public clearTasksQueue (): void { - this.tasksQueue.clear() - } - - /** @inheritdoc */ - public hasBackPressure (): boolean { - return this.tasksQueue.size >= this.tasksQueueBackPressureSize - } - - /** @inheritdoc */ - public async terminate (): Promise { - const waitWorkerExit = new Promise(resolve => { - this.registerOnceWorkerEventHandler('exit', () => { - resolve() - }) - }) - this.closeMessageChannel() - this.removeAllListeners() - switch (this.info.type) { - case WorkerTypes.thread: - this.worker.unref?.() - await this.worker.terminate?.() - break - case WorkerTypes.cluster: - this.registerOnceWorkerEventHandler('disconnect', () => { - this.worker.kill?.() - }) - this.worker.disconnect?.() - break + public enqueueTask (task: Task): number { + const tasksQueueSize = this.tasksQueue.enqueue(task, task.priority) + if ( + !this.setBackPressureFlag && + this.hasBackPressure() && + !this.info.backPressure + ) { + this.setBackPressureFlag = true + this.info.backPressure = true + this.emit('backPressure', { workerId: this.info.id }) + this.setBackPressureFlag = false } - await waitWorkerExit - } - - /** @inheritdoc */ - public registerWorkerEventHandler ( - event: string, - handler: EventHandler - ): void { - this.worker.on(event, handler) - } - - /** @inheritdoc */ - public registerOnceWorkerEventHandler ( - event: string, - handler: EventHandler - ): void { - this.worker.once(event, handler) + return tasksQueueSize } /** @inheritdoc */ - public getTaskFunctionWorkerUsage (name: string): WorkerUsage | undefined { + public getTaskFunctionWorkerUsage (name: string): undefined | WorkerUsage { if (!Array.isArray(this.info.taskFunctionsProperties)) { throw new Error( `Cannot get task function worker usage for task function name '${name}' when task function properties list is not yet defined` @@ -198,113 +255,57 @@ export class WorkerNode } /** @inheritdoc */ - public deleteTaskFunctionWorkerUsage (name: string): boolean { - return this.taskFunctionsUsage.delete(name) + public hasBackPressure (): boolean { + return this.tasksQueue.size >= this.tasksQueueBackPressureSize } - 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 - } + /** @inheritdoc */ + public registerOnceWorkerEventHandler ( + event: string, + handler: EventHandler + ): void { + this.worker.once(event, handler) } - private initWorkerInfo (worker: Worker): WorkerInfo { - return { - id: getWorkerId(worker), - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - type: getWorkerType(worker)!, - dynamic: false, - ready: false, - stealing: false, - stolen: false, - continuousStealing: false, - backPressureStealing: false, - backPressure: false, - } + /** @inheritdoc */ + public registerWorkerEventHandler ( + event: string, + handler: EventHandler + ): void { + this.worker.on(event, handler) } - private initWorkerUsage (): WorkerUsage { - const getTasksQueueSize = (): number => { - return this.tasksQueue.size - } - const getTasksQueueMaxSize = (): number => { - return this.tasksQueue.maxSize - } - return { - tasks: { - executed: 0, - executing: 0, - get queued (): number { - return getTasksQueueSize() - }, - get maxQueued (): number { - return getTasksQueueMaxSize() - }, - sequentiallyStolen: 0, - stolen: 0, - failed: 0, - }, - runTime: { - history: new CircularBuffer(MeasurementHistorySize), - }, - waitTime: { - history: new CircularBuffer(MeasurementHistorySize), - }, - elu: { - idle: { - history: new CircularBuffer(MeasurementHistorySize), - }, - active: { - history: new CircularBuffer(MeasurementHistorySize), - }, - }, - } + /** @inheritdoc */ + public setTasksQueuePriority (enablePriority: boolean): void { + this.tasksQueue.enablePriority = enablePriority } - 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 { - tasks: { - executed: 0, - executing: 0, - get queued (): number { - return getTaskFunctionQueueSize() - }, - sequentiallyStolen: 0, - stolen: 0, - failed: 0, - }, - runTime: { - history: new CircularBuffer(MeasurementHistorySize), - }, - waitTime: { - history: new CircularBuffer(MeasurementHistorySize), - }, - elu: { - idle: { - history: new CircularBuffer(MeasurementHistorySize), - }, - active: { - history: new CircularBuffer(MeasurementHistorySize), - }, - }, + /** @inheritdoc */ + public tasksQueueSize (): number { + return this.tasksQueue.size + } + + /** @inheritdoc */ + public async terminate (): Promise { + const waitWorkerExit = new Promise(resolve => { + this.registerOnceWorkerEventHandler('exit', () => { + resolve() + }) + }) + this.closeMessageChannel() + this.removeAllListeners() + switch (this.info.type) { + case WorkerTypes.thread: + this.worker.unref?.() + await this.worker.terminate?.() + break + case WorkerTypes.cluster: + this.registerOnceWorkerEventHandler('disconnect', () => { + this.worker.kill?.() + }) + this.worker.disconnect?.() + break } + await waitWorkerExit } } diff --git a/src/pools/worker.ts b/src/pools/worker.ts index 3c4fe610..f67ec2f6 100644 --- a/src/pools/worker.ts +++ b/src/pools/worker.ts @@ -42,10 +42,10 @@ export type ExitHandler = ( * @typeParam Worker - Type of worker. */ export type EventHandler = - | OnlineHandler - | MessageHandler | ErrorHandler | ExitHandler + | MessageHandler + | OnlineHandler /** * Measurement history size. @@ -62,25 +62,25 @@ export interface MeasurementStatistics { */ aggregate?: number /** - * Measurement minimum. + * Measurement average. */ - minimum?: number + average?: number /** - * Measurement maximum. + * Measurement history. */ - maximum?: number + readonly history: CircularBuffer /** - * Measurement average. + * Measurement maximum. */ - average?: number + maximum?: number /** * Measurement median. */ median?: number /** - * Measurement history. + * Measurement minimum. */ - readonly history: CircularBuffer + minimum?: number } /** @@ -88,8 +88,8 @@ export interface MeasurementStatistics { * @internal */ export interface EventLoopUtilizationMeasurementStatistics { - readonly idle: MeasurementStatistics readonly active: MeasurementStatistics + readonly idle: MeasurementStatistics utilization?: number } @@ -107,13 +107,17 @@ export interface TaskStatistics { */ executing: number /** - * Number of queued tasks. + * Number of failed tasks. */ - readonly queued: number + failed: number /** * Maximum number of queued tasks. */ readonly maxQueued?: number + /** + * Number of queued tasks. + */ + readonly queued: number /** * Number of sequentially stolen tasks. */ @@ -122,19 +126,15 @@ export interface TaskStatistics { * Number of stolen tasks. */ stolen: number - /** - * Number of failed tasks. - */ - failed: number } /** * Enumeration of worker types. */ -export const WorkerTypes: Readonly<{ thread: 'thread'; cluster: 'cluster' }> = +export const WorkerTypes: Readonly<{ cluster: 'cluster'; thread: 'thread' }> = Object.freeze({ - thread: 'thread', cluster: 'cluster', + thread: 'thread', } as const) /** @@ -148,17 +148,28 @@ export type WorkerType = keyof typeof WorkerTypes */ export interface WorkerInfo { /** - * Worker id. + * Back pressure flag. + * This flag is set to `true` when worker node tasks queue has back pressure. */ - readonly id: number | undefined + backPressure: boolean /** - * Worker type. + * Back pressure stealing flag. + * This flag is set to `true` when worker node is stealing one task from another back pressured worker node. */ - readonly type: WorkerType + 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. */ @@ -173,25 +184,14 @@ export interface WorkerInfo { * This flag is set to `true` when worker node has one task stolen from another worker node. */ stolen: boolean - /** - * Continuous stealing flag. - * This flag is set to `true` when worker node is continuously stealing tasks from other worker nodes. - */ - continuousStealing: 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 - /** - * Back pressure flag. - * This flag is set to `true` when worker node tasks queue has back pressure. - */ - backPressure: boolean /** * Task functions properties. */ taskFunctionsProperties?: TaskFunctionProperties[] + /** + * Worker type. + */ + readonly type: WorkerType } /** @@ -200,21 +200,21 @@ export interface WorkerInfo { */ export interface WorkerUsage { /** - * Tasks statistics. + * Tasks event loop utilization statistics. */ - readonly tasks: TaskStatistics + readonly elu: EventLoopUtilizationMeasurementStatistics /** * Tasks runtime statistics. */ readonly runTime: MeasurementStatistics /** - * Tasks wait time statistics. + * Tasks statistics. */ - readonly waitTime: MeasurementStatistics + readonly tasks: TaskStatistics /** - * Tasks event loop utilization statistics. + * Tasks wait time statistics. */ - readonly elu: EventLoopUtilizationMeasurementStatistics + readonly waitTime: MeasurementStatistics } /** @@ -229,14 +229,18 @@ export interface StrategyData { * Worker interface. */ export interface IWorker extends EventEmitter { + /** + * Cluster worker disconnect. + */ + readonly disconnect?: () => void /** * Cluster worker id. */ readonly id?: number /** - * Worker thread worker id. + * Cluster worker kill. */ - readonly threadId?: number + readonly kill?: (signal?: string) => void /** * Registers an event handler. * @param event - The event. @@ -249,25 +253,21 @@ export interface IWorker extends EventEmitter { * @param handler - The event handler. */ readonly once: (event: string, handler: EventHandler) => this - /** - * Calling `unref()` on a worker allows the thread to exit if this is the only - * active handle in the event system. If the worker is already `unref()`ed calling`unref()` again has no effect. - * @since v10.5.0 - */ - readonly unref?: () => void /** * Stop all JavaScript execution in the worker thread as soon as possible. * Returns a Promise for the exit code that is fulfilled when the `'exit' event` is emitted. */ readonly terminate?: () => Promise /** - * Cluster worker disconnect. + * Worker thread worker id. */ - readonly disconnect?: () => void + readonly threadId?: number /** - * Cluster worker kill. + * Calling `unref()` on a worker allows the thread to exit if this is the only + * active handle in the event system. If the worker is already `unref()`ed calling`unref()` again has no effect. + * @since v10.5.0 */ - readonly kill?: (signal?: string) => void + readonly unref?: () => void } /** @@ -275,11 +275,11 @@ export interface IWorker extends EventEmitter { * @internal */ export interface WorkerNodeOptions { - workerOptions?: WorkerOptions env?: Record tasksQueueBackPressureSize: number | undefined tasksQueueBucketSize: number | undefined tasksQueuePriority: boolean | undefined + workerOptions?: WorkerOptions } /** @@ -291,47 +291,20 @@ export interface WorkerNodeOptions { export interface IWorkerNode extends EventEmitter { /** - * Worker. - */ - readonly worker: Worker - /** - * Worker info. - */ - readonly info: WorkerInfo - /** - * Worker usage statistics. - */ - readonly usage: WorkerUsage - /** - * Worker choice strategy data. - * This is used to store data that are specific to the worker choice strategy. - */ - strategyData?: StrategyData - /** - * Message channel (worker thread only). - */ - readonly messageChannel?: MessageChannel - /** - * Tasks queue back pressure size. - * This is the number of tasks that can be enqueued before the worker node has back pressure. - */ - tasksQueueBackPressureSize: number - /** - * Sets tasks queue priority. - * @param enablePriority - Whether to enable tasks queue priority. + * Clears tasks queue. */ - readonly setTasksQueuePriority: (enablePriority: boolean) => void + readonly clearTasksQueue: () => void /** - * Tasks queue size. - * @returns The tasks queue size. + * Deletes task function worker usage statistics. + * @param name - The task function name. + * @returns `true` if the task function worker usage statistics were deleted, `false` otherwise. */ - readonly tasksQueueSize: () => number + readonly deleteTaskFunctionWorkerUsage: (name: string) => boolean /** - * Enqueue task. - * @param task - The task to queue. - * @returns The tasks queue size. + * Dequeue last prioritized task. + * @returns The dequeued task. */ - readonly enqueueTask: (task: Task) => number + readonly dequeueLastPrioritizedTask: () => Task | undefined /** * Dequeue task. * @param bucket - The prioritized bucket to dequeue from. @defaultValue 0 @@ -339,53 +312,80 @@ export interface IWorkerNode */ readonly dequeueTask: (bucket?: number) => Task | undefined /** - * Dequeue last prioritized task. - * @returns The dequeued task. + * Enqueue task. + * @param task - The task to queue. + * @returns The tasks queue size. */ - readonly dequeueLastPrioritizedTask: () => Task | undefined + readonly enqueueTask: (task: Task) => number /** - * Clears tasks queue. + * Gets task function worker usage statistics. + * @param name - The task function name. + * @returns The task function worker usage statistics if the task function worker usage statistics are initialized, `undefined` otherwise. */ - readonly clearTasksQueue: () => void + readonly getTaskFunctionWorkerUsage: (name: string) => undefined | WorkerUsage /** * Whether the worker node has back pressure (i.e. its tasks queue is full). * @returns `true` if the worker node has back pressure, `false` otherwise. */ readonly hasBackPressure: () => boolean /** - * Terminates the worker node. + * Worker info. */ - readonly terminate: () => Promise + readonly info: WorkerInfo /** - * Registers a worker event handler. + * Message channel (worker thread only). + */ + readonly messageChannel?: MessageChannel + /** + * Registers once a worker event handler. * @param event - The event. * @param handler - The event handler. */ - readonly registerWorkerEventHandler: ( + readonly registerOnceWorkerEventHandler: ( event: string, handler: EventHandler ) => void /** - * Registers once a worker event handler. + * Registers a worker event handler. * @param event - The event. * @param handler - The event handler. */ - readonly registerOnceWorkerEventHandler: ( + readonly registerWorkerEventHandler: ( event: string, handler: EventHandler ) => void /** - * Gets task function worker usage statistics. - * @param name - The task function name. - * @returns The task function worker usage statistics if the task function worker usage statistics are initialized, `undefined` otherwise. + * Sets tasks queue priority. + * @param enablePriority - Whether to enable tasks queue priority. */ - readonly getTaskFunctionWorkerUsage: (name: string) => WorkerUsage | undefined + readonly setTasksQueuePriority: (enablePriority: boolean) => void /** - * Deletes task function worker usage statistics. - * @param name - The task function name. - * @returns `true` if the task function worker usage statistics were deleted, `false` otherwise. + * Worker choice strategy data. + * This is used to store data that are specific to the worker choice strategy. */ - readonly deleteTaskFunctionWorkerUsage: (name: string) => boolean + strategyData?: StrategyData + /** + * Tasks queue back pressure size. + * This is the number of tasks that can be enqueued before the worker node has back pressure. + */ + tasksQueueBackPressureSize: number + /** + * Tasks queue size. + * @returns The tasks queue size. + */ + readonly tasksQueueSize: () => number + /** + * Terminates the worker node. + */ + readonly terminate: () => Promise + /** + * Worker usage statistics. + */ + readonly usage: WorkerUsage + /** + * Worker. + */ + readonly worker: Worker } /** diff --git a/src/queues/abstract-fixed-queue.ts b/src/queues/abstract-fixed-queue.ts index 0239153f..00d4ddda 100644 --- a/src/queues/abstract-fixed-queue.ts +++ b/src/queues/abstract-fixed-queue.ts @@ -14,9 +14,9 @@ export abstract class AbstractFixedQueue implements IFixedQueue { /** @inheritdoc */ public readonly capacity: number /** @inheritdoc */ - public size!: number - /** @inheritdoc */ public nodeArray: FixedQueueNode[] + /** @inheritdoc */ + public size!: number /** * Constructs a fixed queue. @@ -30,29 +30,25 @@ export abstract class AbstractFixedQueue implements IFixedQueue { this.clear() } - /** @inheritdoc */ - public empty (): boolean { - return this.size === 0 - } - - /** @inheritdoc */ - public full (): boolean { - return this.size === this.capacity + /** + * 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 abstract enqueue (data: T, priority?: number): number - - /** @inheritdoc */ - public get (index: number): T | undefined { - if (this.empty() || index >= this.size) { - return undefined - } - index += this.start - if (index >= this.capacity) { - index -= this.capacity - } - return this.nodeArray[index].data + public clear (): void { + this.start = 0 + this.size = 0 } /** @inheritdoc */ @@ -70,9 +66,28 @@ export abstract class AbstractFixedQueue implements IFixedQueue { } /** @inheritdoc */ - public clear (): void { - this.start = 0 - this.size = 0 + public empty (): boolean { + return this.size === 0 + } + + /** @inheritdoc */ + public abstract enqueue (data: T, priority?: number): number + + /** @inheritdoc */ + public full (): boolean { + return this.size === this.capacity + } + + /** @inheritdoc */ + public get (index: number): T | undefined { + if (this.empty() || index >= this.size) { + return undefined + } + index += this.start + if (index >= this.capacity) { + index -= this.capacity + } + return this.nodeArray[index].data } /** @inheritdoc */ @@ -83,8 +98,8 @@ export abstract class AbstractFixedQueue implements IFixedQueue { next: () => { if (i >= this.size) { return { - value: undefined, done: true, + value: undefined, } } const value = this.nodeArray[index].data @@ -94,25 +109,10 @@ export abstract class AbstractFixedQueue implements IFixedQueue { index = 0 } return { - value, done: false, + value, } }, } } - - /** - * 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/fixed-priority-queue.ts b/src/queues/fixed-priority-queue.ts index 7a3976d7..275a7840 100644 --- a/src/queues/fixed-priority-queue.ts +++ b/src/queues/fixed-priority-queue.ts @@ -1,6 +1,7 @@ -import { AbstractFixedQueue } from './abstract-fixed-queue.js' import type { IFixedQueue } from './queue-types.js' +import { AbstractFixedQueue } from './abstract-fixed-queue.js' + /** * Fixed priority queue. * @typeParam T - Type of fixed priority queue data. diff --git a/src/queues/fixed-queue.ts b/src/queues/fixed-queue.ts index 366e41f1..736c40fb 100644 --- a/src/queues/fixed-queue.ts +++ b/src/queues/fixed-queue.ts @@ -1,6 +1,7 @@ -import { AbstractFixedQueue } from './abstract-fixed-queue.js' import type { IFixedQueue } from './queue-types.js' +import { AbstractFixedQueue } from './abstract-fixed-queue.js' + /** * Fixed queue. * @typeParam T - Type of fixed queue data. diff --git a/src/queues/priority-queue.ts b/src/queues/priority-queue.ts index 6b1fd308..c8cb84f0 100644 --- a/src/queues/priority-queue.ts +++ b/src/queues/priority-queue.ts @@ -15,10 +15,10 @@ import { * @internal */ export class PriorityQueue { - private head!: PriorityQueueNode - private tail!: PriorityQueueNode private readonly bucketSize: number + private head!: PriorityQueueNode private priorityEnabled: boolean + private tail!: PriorityQueueNode /** The priority queue maximum size. */ public maxSize!: number @@ -45,87 +45,27 @@ export class PriorityQueue { this.clear() } - /** - * 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 - } - - /** - * 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 + private getPriorityQueueNode ( + nodeArray?: FixedQueueNode[] + ): PriorityQueueNode { + let fixedQueue: IFixedQueue + if (this.priorityEnabled) { + fixedQueue = new FixedPriorityQueue(this.bucketSize) + } else { + fixedQueue = new FixedQueue(this.bucketSize) } - 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 + if (nodeArray != null) { + fixedQueue.nodeArray = nodeArray } - // 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 number of filled prioritized buckets. - * @returns The number of filled prioritized buckets. - */ - public get buckets (): number { - return Math.trunc(this.size / this.bucketSize) + return fixedQueue } /** - * Enqueue data into the priority queue. - * @param data - Data to enqueue. - * @param priority - Priority of the data. Lower values have higher priority. - * @returns The new size of the priority queue. + * Clears the priority queue. */ - public enqueue (data: T, priority?: number): number { - if (this.head.full()) { - this.head = this.head.next = this.getPriorityQueueNode() - } - this.head.enqueue(data, priority) - const size = this.size - if (size > this.maxSize) { - this.maxSize = size - } - return size + public clear (): void { + this.head = this.tail = this.getPriorityQueueNode() + this.maxSize = 0 } /** @@ -181,11 +121,21 @@ export class PriorityQueue { } /** - * Clears the priority queue. + * Enqueue data into the priority queue. + * @param data - Data to enqueue. + * @param priority - Priority of the data. Lower values have higher priority. + * @returns The new size of the priority queue. */ - public clear (): void { - this.head = this.tail = this.getPriorityQueueNode() - this.maxSize = 0 + public enqueue (data: T, priority?: number): number { + if (this.head.full()) { + this.head = this.head.next = this.getPriorityQueueNode() + } + this.head.enqueue(data, priority) + const size = this.size + if (size > this.maxSize) { + this.maxSize = size + } + return size } /** @@ -201,8 +151,8 @@ export class PriorityQueue { const value = node.get(index) as T if (value == null) { return { - value: undefined, done: true, + value: undefined, } } ++index @@ -211,25 +161,75 @@ export class PriorityQueue { index = 0 } return { - value, done: false, + value, } }, } } - private getPriorityQueueNode ( - nodeArray?: FixedQueueNode[] - ): PriorityQueueNode { - let fixedQueue: IFixedQueue - if (this.priorityEnabled) { - fixedQueue = new FixedPriorityQueue(this.bucketSize) - } else { - fixedQueue = new FixedQueue(this.bucketSize) + /** + * 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 } - if (nodeArray != null) { - fixedQueue.nodeArray = nodeArray + 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 } - return fixedQueue + // 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 } } diff --git a/src/queues/queue-types.ts b/src/queues/queue-types.ts index 83d7f7f2..1e374b59 100644 --- a/src/queues/queue-types.ts +++ b/src/queues/queue-types.ts @@ -20,22 +20,28 @@ export interface FixedQueueNode { * @internal */ export interface IFixedQueue { + /** + * Returns an iterator for the fixed queue. + * @returns An iterator for the fixed queue. + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols + */ + [Symbol.iterator]: () => Iterator /** The fixed queue capacity. */ readonly capacity: number - /** The fixed queue size. */ - readonly size: number - /** The fixed queue node array. */ - nodeArray: FixedQueueNode[] + /** + * Clears the fixed queue. + */ + clear: () => void + /** + * Dequeue data from the fixed queue. + * @returns The dequeued data or `undefined` if the fixed queue is empty. + */ + dequeue: () => T | undefined /** * Checks if the fixed queue is empty. * @returns `true` if the fixed queue is empty, `false` otherwise. */ empty: () => boolean - /** - * Checks if the fixed queue is full. - * @returns `true` if the fixed queue is full, `false` otherwise. - */ - full: () => boolean /** * Enqueue data into the fixed queue. * @param data - Data to enqueue. @@ -44,27 +50,21 @@ export interface IFixedQueue { * @throws If the fixed queue is full. */ enqueue: (data: T, priority?: number) => number + /** + * Checks if the fixed queue is full. + * @returns `true` if the fixed queue is full, `false` otherwise. + */ + full: () => boolean /** * Gets data from the fixed queue. * @param index - The index of the data to get. * @returns The data at the index or `undefined` if the fixed queue is empty or the index is out of bounds. */ get: (index: number) => T | undefined - /** - * Dequeue data from the fixed queue. - * @returns The dequeued data or `undefined` if the fixed queue is empty. - */ - dequeue: () => T | undefined - /** - * Clears the fixed queue. - */ - clear: () => void - /** - * Returns an iterator for the fixed queue. - * @returns An iterator for the fixed queue. - * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols - */ - [Symbol.iterator]: () => Iterator + /** The fixed queue node array. */ + nodeArray: FixedQueueNode[] + /** The fixed queue size. */ + readonly size: number } /** diff --git a/src/utility-types.ts b/src/utility-types.ts index b29f8012..8ccb1a74 100644 --- a/src/utility-types.ts +++ b/src/utility-types.ts @@ -11,17 +11,17 @@ import type { KillBehavior } from './worker/worker-options.js' */ export interface WorkerError { /** - * Task function name triggering the error. + * Data triggering the error. */ - readonly name: string + readonly data?: Data /** * Error message. */ readonly message: string /** - * Data triggering the error. + * Task function name triggering the error. */ - readonly data?: Data + readonly name: string } /** @@ -30,21 +30,21 @@ export interface WorkerError { */ export interface TaskPerformance { /** - * Task name. + * Task event loop utilization. */ - readonly name: string + readonly elu?: EventLoopUtilization /** - * Task performance timestamp. + * Task name. */ - readonly timestamp: number + readonly name: string /** * Task runtime. */ readonly runTime?: number /** - * Task event loop utilization. + * Task performance timestamp. */ - readonly elu?: EventLoopUtilization + readonly timestamp: number } /** @@ -52,14 +52,14 @@ export interface TaskPerformance { * @internal */ export interface WorkerStatistics { - /** - * Whether the worker computes the task runtime or not. - */ - readonly runTime: boolean /** * Whether the worker computes the task event loop utilization (ELU) or not. */ readonly elu: boolean + /** + * Whether the worker computes the task runtime or not. + */ + readonly runTime: boolean } /** @@ -86,14 +86,14 @@ export interface TaskFunctionProperties { * @internal */ export interface Task { - /** - * Task name. - */ - readonly name?: string /** * Task input data that will be passed to the worker. */ readonly data?: Data + /** + * Task name. + */ + readonly name?: string /** * Task priority. Lower values have higher priority. * @defaultValue 0 @@ -104,17 +104,17 @@ export interface Task { */ readonly strategy?: WorkerChoiceStrategy /** - * Array of transferable objects. + * Task UUID. */ - readonly transferList?: readonly TransferListItem[] + readonly taskId?: `${string}-${string}-${string}-${string}-${string}` /** * Timestamp. */ readonly timestamp?: number /** - * Task UUID. + * Array of transferable objects. */ - readonly taskId?: `${string}-${string}-${string}-${string}-${string}` + readonly transferList?: readonly TransferListItem[] } /** @@ -126,28 +126,36 @@ export interface Task { export interface MessageValue extends Task { /** - * Worker id. + * Whether the worker starts or stops its activity check. */ - readonly workerId?: number + readonly checkActive?: boolean /** * Kill code. */ - readonly kill?: KillBehavior | true | 'success' | 'failure' + readonly kill?: 'failure' | 'success' | KillBehavior | true /** - * Worker error. + * Message port. */ - readonly workerError?: WorkerError + readonly port?: MessagePort /** - * Task performance. + * Whether the worker is ready or not. */ - readonly taskPerformance?: TaskPerformance + readonly ready?: boolean + /** + * Whether the worker computes the given statistics or not. + */ + readonly statistics?: WorkerStatistics + /** + * Task function serialized to string. + */ + readonly taskFunction?: string /** * Task function operation: * - `'add'` - Add a task function. * - `'remove'` - Remove a task function. * - `'default'` - Set a task function as default. */ - readonly taskFunctionOperation?: 'add' | 'remove' | 'default' + readonly taskFunctionOperation?: 'add' | 'default' | 'remove' /** * Whether the task function operation is successful or not. */ @@ -156,30 +164,22 @@ export interface MessageValue * Task function properties. */ readonly taskFunctionProperties?: TaskFunctionProperties - /** - * Task function serialized to string. - */ - readonly taskFunction?: string /** * Task functions properties. */ readonly taskFunctionsProperties?: TaskFunctionProperties[] /** - * Whether the worker computes the given statistics or not. - */ - readonly statistics?: WorkerStatistics - /** - * Whether the worker is ready or not. + * Task performance. */ - readonly ready?: boolean + readonly taskPerformance?: TaskPerformance /** - * Whether the worker starts or stops its activity check. + * Worker error. */ - readonly checkActive?: boolean + readonly workerError?: WorkerError /** - * Message port. + * Worker id. */ - readonly port?: MessagePort + readonly workerId?: number } /** @@ -189,21 +189,21 @@ export interface MessageValue */ export interface PromiseResponseWrapper { /** - * Resolve callback to fulfill the promise. + * The asynchronous resource used to track the task execution. */ - readonly resolve: (value: Response | PromiseLike) => void + readonly asyncResource?: AsyncResource /** * Reject callback to reject the promise. */ readonly reject: (reason?: unknown) => void /** - * The worker node key executing the task. + * Resolve callback to fulfill the promise. */ - readonly workerNodeKey: number + readonly resolve: (value: PromiseLike | Response) => void /** - * The asynchronous resource used to track the task execution. + * The worker node key executing the task. */ - readonly asyncResource?: AsyncResource + readonly workerNodeKey: number } /** diff --git a/src/worker/abstract-worker.ts b/src/worker/abstract-worker.ts index ebbefc75..3b5f11fc 100644 --- a/src/worker/abstract-worker.ts +++ b/src/worker/abstract-worker.ts @@ -1,7 +1,8 @@ import type { Worker } from 'node:cluster' -import { performance } from 'node:perf_hooks' import type { MessagePort } from 'node:worker_threads' +import { performance } from 'node:perf_hooks' + import type { MessageValue, Task, @@ -9,13 +10,6 @@ import type { TaskPerformance, WorkerStatistics, } from '../utility-types.js' -import { - buildTaskFunctionProperties, - DEFAULT_TASK_NAME, - EMPTY_FUNCTION, - isAsyncFunction, - isPlainObject, -} from '../utils.js' import type { TaskAsyncFunction, TaskFunction, @@ -24,6 +18,14 @@ import type { TaskFunctions, TaskSyncFunction, } from './task-functions.js' + +import { + buildTaskFunctionProperties, + DEFAULT_TASK_NAME, + EMPTY_FUNCTION, + isAsyncFunction, + isPlainObject, +} from '../utils.js' import { checkTaskFunctionName, checkValidTaskFunctionObjectEntry, @@ -37,15 +39,15 @@ const DEFAULT_WORKER_OPTIONS: WorkerOptions = { * The kill behavior option on this worker or its default value. */ killBehavior: KillBehaviors.SOFT, + /** + * The function to call when the worker is killed. + */ + killHandler: EMPTY_FUNCTION, /** * The maximum time to keep this worker active while idle. * The pool automatically checks and terminates this worker when the time expires. */ maxInactiveTime: DEFAULT_MAX_INACTIVE_TIME, - /** - * The function to call when the worker is killed. - */ - killHandler: EMPTY_FUNCTION, } /** @@ -55,30 +57,131 @@ const DEFAULT_WORKER_OPTIONS: WorkerOptions = { * @typeParam Response - Type of response the worker sends back to the main worker. This can only be structured-cloneable data. */ export abstract class AbstractWorker< - MainWorker extends Worker | MessagePort, + MainWorker extends MessagePort | Worker, Data = unknown, Response = unknown > { /** - * Worker id. + * Handler id of the `activeInterval` worker activity check. */ - protected abstract id: number + protected activeInterval?: NodeJS.Timeout /** - * Task function object(s) processed by the worker when the pool's `execution` function is invoked. + * Worker id. */ - protected taskFunctions!: Map> + protected abstract id: number /** * Timestamp of the last task processed by this worker. */ 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, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + message: `Task function '${name!}' not found`, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + name: name!, + }, + }) + 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, + message: this.handleError(error as Error | string), + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + name: name!, + }, + }) + }) + .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, + message: this.handleError(error as Error | string), + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + name: name!, + }, + }) + } finally { + this.updateLastTaskTimestamp() + } + } + /** * Performance statistics computation requirements. */ protected statistics?: WorkerStatistics + /** - * Handler id of the `activeInterval` worker activity check. + * Task function object(s) processed by the worker when the pool's `execution` function is invoked. */ - protected activeInterval?: NodeJS.Timeout + protected taskFunctions!: Map> /** * Constructs a new poolifier worker. @@ -89,7 +192,7 @@ export abstract class AbstractWorker< */ public constructor ( protected readonly isMain: boolean | undefined, - private readonly mainWorker: MainWorker | undefined | null, + private readonly mainWorker: MainWorker | null | undefined, taskFunctions: TaskFunction | TaskFunctions, protected opts: WorkerOptions = DEFAULT_WORKER_OPTIONS ) { @@ -104,246 +207,62 @@ export abstract class AbstractWorker< } } - private checkWorkerOptions (opts: WorkerOptions): void { - checkValidWorkerOptions(opts) - this.opts = { ...DEFAULT_WORKER_OPTIONS, ...opts } + /** + * Returns the main worker. + * @returns Reference to the main worker. + * @throws {@link https://nodejs.org/api/errors.html#class-error} If the main worker is not set. + */ + protected getMainWorker (): MainWorker { + if (this.mainWorker == null) { + throw new Error('Main worker not set') + } + return this.mainWorker } /** - * Checks if the `taskFunctions` parameter is passed to the constructor and valid. - * @param taskFunctions - The task function(s) parameter that should be checked. + * Handles an error and convert it to a string so it can be sent back to the main worker. + * @param error - The error raised by the worker. + * @returns The error message. */ - private checkTaskFunctions ( - taskFunctions: - | TaskFunction - | TaskFunctions - | undefined - ): void { - if (taskFunctions == null) { - throw new Error('taskFunctions parameter is mandatory') - } - this.taskFunctions = new Map>() - if (typeof taskFunctions === 'function') { - const fnObj = { taskFunction: taskFunctions.bind(this) } - this.taskFunctions.set(DEFAULT_TASK_NAME, fnObj) - this.taskFunctions.set( - typeof taskFunctions.name === 'string' && - taskFunctions.name.trim().length > 0 - ? taskFunctions.name - : 'fn1', - fnObj - ) - } else if (isPlainObject(taskFunctions)) { - let firstEntry = true - for (let [name, fnObj] of Object.entries(taskFunctions)) { - if (typeof fnObj === 'function') { - fnObj = { taskFunction: fnObj } satisfies TaskFunctionObject< - Data, - Response - > - } - checkValidTaskFunctionObjectEntry(name, fnObj) - fnObj.taskFunction = fnObj.taskFunction.bind(this) - if (firstEntry) { - this.taskFunctions.set(DEFAULT_TASK_NAME, fnObj) - firstEntry = false - } - this.taskFunctions.set(name, fnObj) - } - if (firstEntry) { - throw new Error('taskFunctions parameter object is empty') - } - } else { - throw new TypeError( - 'taskFunctions parameter is not a function or a plain object' - ) - } + protected handleError (error: Error | string): string { + return error instanceof Error ? error.message : error } /** - * 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. + * Handles a kill message sent by the main worker. + * @param message - The kill message. */ - public hasTaskFunction (name: string): TaskFunctionOperationResult { - try { - checkTaskFunctionName(name) - } catch (error) { - return { status: false, error: error as Error } + protected handleKillMessage (message: MessageValue): void { + this.stopCheckActive() + if (isAsyncFunction(this.opts.killHandler)) { + ;(this.opts.killHandler as () => Promise)() + .then(() => { + this.sendToMainWorker({ kill: 'success' }) + return undefined + }) + .catch(() => { + this.sendToMainWorker({ kill: 'failure' }) + }) + } else { + try { + ;(this.opts.killHandler as (() => void) | undefined)?.() + this.sendToMainWorker({ kill: 'success' }) + } catch { + this.sendToMainWorker({ kill: 'failure' }) + } } - return { status: this.taskFunctions.has(name) } } /** - * 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 { status: false, error: error as Error } - } - } - - /** - * 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 { status: false, error: error as Error } - } - } - - /** - * 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, - ] - } - - /** - * 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 { status: false, error: error as Error } - } - } - - /** - * Handles the ready message sent by the main worker. - * @param message - The ready message. + * Handles the ready message sent by the main worker. + * @param message - The ready message. */ protected abstract handleReadyMessage (message: MessageValue): void - /** - * Worker message listener. - * @param message - The received message. - */ - protected messageListener (message: MessageValue): void { - this.checkMessageWorkerId(message) - const { - statistics, - checkActive, - taskFunctionOperation, - taskId, - data, - kill, - } = message - if (statistics != null) { - // Statistics message received - this.statistics = statistics - } else if (checkActive != null) { - // Check active message received - checkActive ? this.startCheckActive() : this.stopCheckActive() - } else if (taskFunctionOperation != null) { - // Task function operation message received - this.handleTaskFunctionOperationMessage(message) - } else if (taskId != null && data != null) { - // Task message received - this.run(message) - } else if (kill === true) { - // Kill message received - this.handleKillMessage(message) - } - } - protected handleTaskFunctionOperationMessage ( message: MessageValue ): void { - const { taskFunctionOperation, taskFunctionProperties, taskFunction } = + const { taskFunction, taskFunctionOperation, taskFunctionProperties } = message if (taskFunctionProperties == null) { throw new Error( @@ -373,11 +292,12 @@ export abstract class AbstractWorker< case 'default': response = this.setDefaultTaskFunction(taskFunctionProperties.name) break + // eslint-disable-next-line perfectionist/sort-switch-case default: - response = { status: false, error: new Error('Unknown task operation') } + response = { error: new Error('Unknown task operation'), status: false } break } - const { status, error } = response + const { error, status } = response this.sendToMainWorker({ taskFunctionOperation, taskFunctionOperationStatus: status, @@ -385,71 +305,72 @@ export abstract class AbstractWorker< ...(!status && error != null && { workerError: { - name: taskFunctionProperties.name, message: this.handleError(error as Error | string), + name: taskFunctionProperties.name, }, }), }) } /** - * Handles a kill message sent by the main worker. - * @param message - The kill message. + * Worker message listener. + * @param message - The received message. */ - protected handleKillMessage (message: MessageValue): void { - this.stopCheckActive() - if (isAsyncFunction(this.opts.killHandler)) { - ;(this.opts.killHandler as () => Promise)() - .then(() => { - this.sendToMainWorker({ kill: 'success' }) - return undefined - }) - .catch(() => { - this.sendToMainWorker({ kill: 'failure' }) - }) - } else { - try { - ;(this.opts.killHandler as (() => void) | undefined)?.() - this.sendToMainWorker({ kill: 'success' }) - } catch { - this.sendToMainWorker({ kill: 'failure' }) - } + protected messageListener (message: MessageValue): void { + this.checkMessageWorkerId(message) + const { + checkActive, + data, + kill, + statistics, + taskFunctionOperation, + taskId, + } = message + if (statistics != null) { + // Statistics message received + this.statistics = statistics + } else if (checkActive != null) { + // Check active message received + checkActive ? this.startCheckActive() : this.stopCheckActive() + } else if (taskFunctionOperation != null) { + // Task function operation message received + this.handleTaskFunctionOperationMessage(message) + } else if (taskId != null && data != null) { + // Task message received + this.run(message) + } else if (kill === true) { + // Kill message received + this.handleKillMessage(message) } } /** - * Check if the message worker id is set and matches the worker id. - * @param message - The message to check. - * @throws {@link https://nodejs.org/api/errors.html#class-error} If the message worker id is not set or does not match the worker id. + * Sends task functions properties to the main worker. */ - private checkMessageWorkerId (message: MessageValue): void { - if (message.workerId == null) { - throw new Error('Message worker id is not set') - } else if (message.workerId !== this.id) { - throw new Error( - `Message worker id ${message.workerId.toString()} does not match the worker id ${this.id.toString()}` - ) - } + protected sendTaskFunctionsPropertiesToMainWorker (): void { + this.sendToMainWorker({ + taskFunctionsProperties: this.listTaskFunctionsProperties(), + }) } /** - * Starts the worker check active interval. + * Sends a message to main worker. + * @param message - The response message. */ - private startCheckActive (): void { - this.lastTaskTimestamp = performance.now() - this.activeInterval = setInterval( - this.checkActive.bind(this), - (this.opts.maxInactiveTime ?? DEFAULT_MAX_INACTIVE_TIME) / 2 - ) - } + protected abstract sendToMainWorker ( + message: MessageValue + ): void - /** - * Stops the worker check active interval. - */ - private stopCheckActive (): void { - if (this.activeInterval != null) { - clearInterval(this.activeInterval) - delete this.activeInterval + private beginTaskPerformance (name?: string): TaskPerformance { + if (this.statistics == null) { + throw new Error('Performance statistics computation requirements not set') + } + return { + name: name ?? DEFAULT_TASK_NAME, + timestamp: performance.now(), + ...(this.statistics.elu && { + elu: performance.eventLoopUtilization(), + }), } } @@ -466,176 +387,259 @@ export abstract class AbstractWorker< } /** - * Returns the main worker. - * @returns Reference to the main worker. - * @throws {@link https://nodejs.org/api/errors.html#class-error} If the main worker is not set. + * Check if the message worker id is set and matches the worker id. + * @param message - The message to check. + * @throws {@link https://nodejs.org/api/errors.html#class-error} If the message worker id is not set or does not match the worker id. */ - protected getMainWorker (): MainWorker { - if (this.mainWorker == null) { - throw new Error('Main worker not set') + private checkMessageWorkerId (message: MessageValue): void { + if (message.workerId == null) { + throw new Error('Message worker id is not set') + } else if (message.workerId !== this.id) { + throw new Error( + `Message worker id ${message.workerId.toString()} does not match the worker id ${this.id.toString()}` + ) } - return this.mainWorker } /** - * Sends a message to main worker. - * @param message - The response message. + * Checks if the `taskFunctions` parameter is passed to the constructor and valid. + * @param taskFunctions - The task function(s) parameter that should be checked. */ - protected abstract sendToMainWorker ( - message: MessageValue - ): void - - /** - * Sends task functions properties to the main worker. - */ - protected sendTaskFunctionsPropertiesToMainWorker (): void { - this.sendToMainWorker({ - taskFunctionsProperties: this.listTaskFunctionsProperties(), - }) + private checkTaskFunctions ( + taskFunctions: + | TaskFunction + | TaskFunctions + | undefined + ): void { + if (taskFunctions == null) { + throw new Error('taskFunctions parameter is mandatory') + } + this.taskFunctions = new Map>() + if (typeof taskFunctions === 'function') { + const fnObj = { taskFunction: taskFunctions.bind(this) } + this.taskFunctions.set(DEFAULT_TASK_NAME, fnObj) + this.taskFunctions.set( + typeof taskFunctions.name === 'string' && + taskFunctions.name.trim().length > 0 + ? taskFunctions.name + : 'fn1', + fnObj + ) + } else if (isPlainObject(taskFunctions)) { + let firstEntry = true + for (let [name, fnObj] of Object.entries(taskFunctions)) { + if (typeof fnObj === 'function') { + fnObj = { taskFunction: fnObj } satisfies TaskFunctionObject< + Data, + Response + > + } + checkValidTaskFunctionObjectEntry(name, fnObj) + fnObj.taskFunction = fnObj.taskFunction.bind(this) + if (firstEntry) { + this.taskFunctions.set(DEFAULT_TASK_NAME, fnObj) + firstEntry = false + } + this.taskFunctions.set(name, fnObj) + } + if (firstEntry) { + throw new Error('taskFunctions parameter object is empty') + } + } else { + throw new TypeError( + 'taskFunctions parameter is not a function or a plain object' + ) + } + } + + private checkWorkerOptions (opts: WorkerOptions): void { + checkValidWorkerOptions(opts) + this.opts = { ...DEFAULT_WORKER_OPTIONS, ...opts } + } + + private endTaskPerformance ( + taskPerformance: TaskPerformance + ): TaskPerformance { + if (this.statistics == null) { + throw new Error('Performance statistics computation requirements not set') + } + return { + ...taskPerformance, + ...(this.statistics.runTime && { + runTime: performance.now() - taskPerformance.timestamp, + }), + ...(this.statistics.elu && { + elu: performance.eventLoopUtilization(taskPerformance.elu), + }), + } } /** - * Handles an error and convert it to a string so it can be sent back to the main worker. - * @param error - The error raised by the worker. - * @returns The error message. + * Starts the worker check active interval. */ - protected handleError (error: Error | string): string { - return error instanceof Error ? error.message : error + private startCheckActive (): void { + this.lastTaskTimestamp = performance.now() + this.activeInterval = setInterval( + this.checkActive.bind(this), + (this.opts.maxInactiveTime ?? DEFAULT_MAX_INACTIVE_TIME) / 2 + ) } /** - * Runs the given task. - * @param task - The task to execute. + * Stops the worker check active interval. */ - protected readonly run = (task: Task): void => { - const { name, taskId, data } = task - const taskFunctionName = name ?? DEFAULT_TASK_NAME - if (!this.taskFunctions.has(taskFunctionName)) { - this.sendToMainWorker({ - workerError: { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - name: name!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - message: `Task function '${name!}' not found`, - data, - }, - taskId, - }) - return + private stopCheckActive (): void { + if (this.activeInterval != null) { + clearInterval(this.activeInterval) + delete this.activeInterval } - const fn = this.taskFunctions.get(taskFunctionName)?.taskFunction - if (isAsyncFunction(fn)) { - this.runAsync(fn as TaskAsyncFunction, task) - } else { - this.runSync(fn as TaskSyncFunction, task) + } + + private updateLastTaskTimestamp (): void { + if (this.activeInterval != null) { + this.lastTaskTimestamp = performance.now() } } /** - * Runs the given task function synchronously. - * @param fn - Task function that will be executed. - * @param task - Input data for the task function. + * 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. */ - protected readonly runSync = ( - fn: TaskSyncFunction, - task: Task - ): void => { - const { name, taskId, data } = task + public addTaskFunction ( + name: string, + fn: TaskFunction | TaskFunctionObject + ): TaskFunctionOperationResult { try { - let taskPerformance = this.beginTaskPerformance(name) - const res = fn(data) - taskPerformance = this.endTaskPerformance(taskPerformance) - this.sendToMainWorker({ - data: res, - taskPerformance, - taskId, - }) + 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) { - this.sendToMainWorker({ - workerError: { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - name: name!, - message: this.handleError(error as Error | string), - data, - }, - taskId, - }) - } finally { - this.updateLastTaskTimestamp() + return { error: error as Error, status: false } } } /** - * Runs the given task function asynchronously. - * @param fn - Task function that will be executed. - * @param task - Input data for the task function. + * 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. */ - protected readonly runAsync = ( - fn: TaskAsyncFunction, - task: Task - ): void => { - const { name, taskId, data } = task - let taskPerformance = this.beginTaskPerformance(name) - fn(data) - .then(res => { - taskPerformance = this.endTaskPerformance(taskPerformance) - this.sendToMainWorker({ - data: res, - taskPerformance, - taskId, - }) - return undefined - }) - .catch((error: unknown) => { - this.sendToMainWorker({ - workerError: { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - name: name!, - message: this.handleError(error as Error | string), - data, - }, - taskId, - }) - }) - .finally(() => { - this.updateLastTaskTimestamp() - }) - .catch(EMPTY_FUNCTION) + public hasTaskFunction (name: string): TaskFunctionOperationResult { + try { + checkTaskFunctionName(name) + } catch (error) { + return { error: error as Error, status: false } + } + return { status: this.taskFunctions.has(name) } } - private beginTaskPerformance (name?: string): TaskPerformance { - if (this.statistics == null) { - throw new Error('Performance statistics computation requirements not set') + /** + * 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 + } } - return { - name: name ?? DEFAULT_TASK_NAME, - timestamp: performance.now(), - ...(this.statistics.elu && { - elu: performance.eventLoopUtilization(), - }), + 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, + ] } - private endTaskPerformance ( - taskPerformance: TaskPerformance - ): TaskPerformance { - if (this.statistics == null) { - throw new Error('Performance statistics computation requirements not set') - } - return { - ...taskPerformance, - ...(this.statistics.runTime && { - runTime: performance.now() - taskPerformance.timestamp, - }), - ...(this.statistics.elu && { - elu: performance.eventLoopUtilization(taskPerformance.elu), - }), + /** + * 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 } } } - private updateLastTaskTimestamp (): void { - if (this.activeInterval != null) { - this.lastTaskTimestamp = performance.now() + /** + * 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 d9f6e647..e6ffe0e2 100644 --- a/src/worker/cluster-worker.ts +++ b/src/worker/cluster-worker.ts @@ -1,10 +1,11 @@ import cluster, { type Worker } from 'node:cluster' import type { MessageValue } from '../utility-types.js' -import { AbstractWorker } from './abstract-worker.js' import type { TaskFunction, TaskFunctions } from './task-functions.js' import type { WorkerOptions } from './worker-options.js' +import { AbstractWorker } from './abstract-worker.js' + /** * A cluster worker used by a poolifier `ClusterPool`. * @@ -22,6 +23,16 @@ export class ClusterWorker< Data = unknown, Response = unknown > extends AbstractWorker { + /** @inheritDoc */ + protected readonly sendToMainWorker = ( + message: MessageValue + ): void => { + this.getMainWorker().send({ + ...message, + workerId: this.id, + } satisfies MessageValue) + } + /** * Constructs a new poolifier cluster worker. * @param taskFunctions - Task function(s) processed by the worker when the pool's `execution` function is invoked. @@ -56,14 +67,4 @@ export class ClusterWorker< protected get id (): number { return this.getMainWorker().id } - - /** @inheritDoc */ - 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 2fd951fb..9e406730 100644 --- a/src/worker/task-functions.ts +++ b/src/worker/task-functions.ts @@ -30,8 +30,8 @@ export type TaskAsyncFunction = ( * @typeParam Response - Type of execution response. This can only be structured-cloneable data. */ export type TaskFunction = - | TaskSyncFunction | TaskAsyncFunction + | TaskSyncFunction /** * Task function object. @@ -39,10 +39,6 @@ export type TaskFunction = * @typeParam Response - Type of execution response. This can only be structured-cloneable data. */ export interface TaskFunctionObject { - /** - * Task function. - */ - taskFunction: TaskFunction /** * Task function priority. Lower values have higher priority. */ @@ -51,6 +47,10 @@ export interface TaskFunctionObject { * Task function worker choice strategy. */ strategy?: WorkerChoiceStrategy + /** + * Task function. + */ + taskFunction: TaskFunction } /** @@ -69,6 +69,6 @@ export type TaskFunctions = Record< * Task function operation result. */ export interface TaskFunctionOperationResult { - status: boolean error?: Error + status: boolean } diff --git a/src/worker/thread-worker.ts b/src/worker/thread-worker.ts index 8057d6f3..cd94753b 100644 --- a/src/worker/thread-worker.ts +++ b/src/worker/thread-worker.ts @@ -6,10 +6,11 @@ import { } from 'node:worker_threads' import type { MessageValue } from '../utility-types.js' -import { AbstractWorker } from './abstract-worker.js' import type { TaskFunction, TaskFunctions } from './task-functions.js' import type { WorkerOptions } from './worker-options.js' +import { AbstractWorker } from './abstract-worker.js' + /** * A thread worker used by a poolifier `ThreadPool`. * @@ -27,6 +28,16 @@ export class ThreadWorker< Data = unknown, Response = unknown > extends AbstractWorker { + /** @inheritDoc */ + protected readonly sendToMainWorker = ( + message: MessageValue + ): void => { + this.port?.postMessage({ + ...message, + workerId: this.id, + } satisfies MessageValue) + } + /** * Message port used to communicate with the main worker. */ @@ -44,6 +55,20 @@ export class ThreadWorker< super(isMainThread, parentPort, taskFunctions, opts) } + /** + * @inheritDoc + */ + protected handleError (error: Error | string): string { + return error as string + } + + /** @inheritDoc */ + protected handleKillMessage (message: MessageValue): void { + super.handleKillMessage(message) + this.port?.unref() + this.port?.close() + } + /** @inheritDoc */ protected handleReadyMessage (message: MessageValue): void { if ( @@ -67,32 +92,8 @@ export class ThreadWorker< } } - /** @inheritDoc */ - protected handleKillMessage (message: MessageValue): void { - super.handleKillMessage(message) - this.port?.unref() - this.port?.close() - } - /** @inheritDoc */ protected get id (): number { return threadId } - - /** @inheritDoc */ - protected readonly sendToMainWorker = ( - message: MessageValue - ): void => { - this.port?.postMessage({ - ...message, - workerId: this.id, - } satisfies MessageValue) - } - - /** - * @inheritDoc - */ - protected handleError (error: Error | string): string { - return error as string - } } diff --git a/src/worker/utils.ts b/src/worker/utils.ts index 59a0289b..1b0ef6f9 100644 --- a/src/worker/utils.ts +++ b/src/worker/utils.ts @@ -1,13 +1,14 @@ +import type { TaskFunctionObject } from './task-functions.js' + import { checkValidPriority, checkValidWorkerChoiceStrategy, } from '../pools/utils.js' import { isPlainObject } from '../utils.js' -import type { TaskFunctionObject } from './task-functions.js' import { KillBehaviors, type WorkerOptions } from './worker-options.js' export const checkValidWorkerOptions = ( - opts: WorkerOptions | undefined + opts: undefined | WorkerOptions ): void => { if (opts != null && !isPlainObject(opts)) { throw new TypeError('opts worker options parameter is not a plain object') diff --git a/src/worker/worker-options.ts b/src/worker/worker-options.ts index 92761840..d2b8b93a 100644 --- a/src/worker/worker-options.ts +++ b/src/worker/worker-options.ts @@ -1,16 +1,16 @@ /** * Enumeration of kill behaviors. */ -export const KillBehaviors: Readonly<{ SOFT: 'SOFT'; HARD: 'HARD' }> = +export const KillBehaviors: Readonly<{ HARD: 'HARD'; SOFT: 'SOFT' }> = Object.freeze({ - /** - * If `currentTime - lastActiveTime` is greater than `maxInactiveTime` but the worker is stealing tasks or a task is executing or queued, then the worker **wont** be deleted. - */ - SOFT: 'SOFT', /** * If `currentTime - lastActiveTime` is greater than `maxInactiveTime` but the worker is stealing tasks or a task is executing or queued, then the worker will be deleted. */ HARD: 'HARD', + /** + * If `currentTime - lastActiveTime` is greater than `maxInactiveTime` but the worker is stealing tasks or a task is executing or queued, then the worker **wont** be deleted. + */ + SOFT: 'SOFT', } as const) /** @@ -21,7 +21,7 @@ export type KillBehavior = keyof typeof KillBehaviors /** * Handler called when a worker is killed. */ -export type KillHandler = () => void | Promise +export type KillHandler = () => Promise | void /** * Options for workers. @@ -37,6 +37,11 @@ export interface WorkerOptions { * @defaultValue KillBehaviors.SOFT */ killBehavior?: KillBehavior + /** + * The function to call when a worker is killed. + * @defaultValue `() => {}` + */ + killHandler?: KillHandler /** * Maximum waiting time in milliseconds for tasks on newly created workers. It must be greater or equal than 5. * @@ -49,9 +54,4 @@ export interface WorkerOptions { * @defaultValue 60000 */ maxInactiveTime?: number - /** - * The function to call when a worker is killed. - * @defaultValue `() => {}` - */ - killHandler?: KillHandler } diff --git a/tests/pools/abstract-pool.test.mjs b/tests/pools/abstract-pool.test.mjs index ed4543dd..0e933d7a 100644 --- a/tests/pools/abstract-pool.test.mjs +++ b/tests/pools/abstract-pool.test.mjs @@ -1,11 +1,10 @@ +import { expect } from 'expect' // eslint-disable-next-line n/no-unsupported-features/node-builtins import { createHook, executionAsyncId } from 'node:async_hooks' import { EventEmitterAsyncResource } from 'node:events' import { readFileSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' - -import { expect } from 'expect' import { restore, stub } from 'sinon' import { CircularBuffer } from '../../lib/circular-buffer.cjs' @@ -227,18 +226,18 @@ describe('Abstract pool test suite', () => { expect(pool.emitter).toBeInstanceOf(EventEmitterAsyncResource) expect(pool.emitter.eventNames()).toStrictEqual([]) expect(pool.opts).toStrictEqual({ - startWorkers: true, enableEvents: true, - restartWorkerOnError: true, enableTasksQueue: false, + restartWorkerOnError: true, + startWorkers: true, workerChoiceStrategy: WorkerChoiceStrategies.ROUND_ROBIN, }) for (const [, workerChoiceStrategy] of pool.workerChoiceStrategiesContext .workerChoiceStrategies) { expect(workerChoiceStrategy.opts).toStrictEqual({ + elu: { median: false }, runTime: { median: false }, waitTime: { median: false }, - elu: { median: false }, weights: expect.objectContaining({ 0: expect.any(Number), [pool.info.maxSize - 1]: expect.any(Number), @@ -251,51 +250,51 @@ describe('Abstract pool test suite', () => { numberOfWorkers, './tests/worker-files/thread/testWorker.mjs', { + enableEvents: false, + enableTasksQueue: true, + errorHandler: testHandler, + exitHandler: testHandler, + messageHandler: testHandler, + onlineHandler: testHandler, + restartWorkerOnError: false, + tasksQueueOptions: { concurrency: 2 }, workerChoiceStrategy: WorkerChoiceStrategies.LEAST_USED, workerChoiceStrategyOptions: { runTime: { median: true }, weights: { 0: 300, 1: 200 }, }, - enableEvents: false, - restartWorkerOnError: false, - enableTasksQueue: true, - tasksQueueOptions: { concurrency: 2 }, - messageHandler: testHandler, - errorHandler: testHandler, - onlineHandler: testHandler, - exitHandler: testHandler, } ) expect(pool.emitter).toBeUndefined() expect(pool.opts).toStrictEqual({ - startWorkers: true, enableEvents: false, - restartWorkerOnError: false, enableTasksQueue: true, + errorHandler: testHandler, + exitHandler: testHandler, + messageHandler: testHandler, + onlineHandler: testHandler, + restartWorkerOnError: false, + startWorkers: true, tasksQueueOptions: { concurrency: 2, size: Math.pow(numberOfWorkers, 2), - taskStealing: true, + tasksFinishedTimeout: 2000, tasksStealingOnBackPressure: true, tasksStealingRatio: 0.6, - tasksFinishedTimeout: 2000, + taskStealing: true, }, workerChoiceStrategy: WorkerChoiceStrategies.LEAST_USED, workerChoiceStrategyOptions: { runTime: { median: true }, weights: { 0: 300, 1: 200 }, }, - onlineHandler: testHandler, - messageHandler: testHandler, - errorHandler: testHandler, - exitHandler: testHandler, }) for (const [, workerChoiceStrategy] of pool.workerChoiceStrategiesContext .workerChoiceStrategies) { expect(workerChoiceStrategy.opts).toStrictEqual({ + elu: { median: false }, runTime: { median: true }, waitTime: { median: false }, - elu: { median: false }, weights: { 0: 300, 1: 200 }, }) } @@ -482,9 +481,9 @@ describe('Abstract pool test suite', () => { for (const [, workerChoiceStrategy] of pool.workerChoiceStrategiesContext .workerChoiceStrategies) { expect(workerChoiceStrategy.opts).toStrictEqual({ + elu: { median: false }, runTime: { median: false }, waitTime: { median: false }, - elu: { median: false }, weights: expect.objectContaining({ 0: expect.any(Number), [pool.info.maxSize - 1]: expect.any(Number), @@ -494,36 +493,36 @@ describe('Abstract pool test suite', () => { expect( pool.workerChoiceStrategiesContext.getTaskStatisticsRequirements() ).toStrictEqual({ - runTime: { + elu: { aggregate: true, average: true, median: false, }, - waitTime: { + runTime: { aggregate: true, average: true, median: false, }, - elu: { + waitTime: { aggregate: true, average: true, median: false, }, }) pool.setWorkerChoiceStrategyOptions({ - runTime: { median: true }, elu: { median: true }, + runTime: { median: true }, }) expect(pool.opts.workerChoiceStrategyOptions).toStrictEqual({ - runTime: { median: true }, elu: { median: true }, + runTime: { median: true }, }) for (const [, workerChoiceStrategy] of pool.workerChoiceStrategiesContext .workerChoiceStrategies) { expect(workerChoiceStrategy.opts).toStrictEqual({ + elu: { median: true }, runTime: { median: true }, waitTime: { median: false }, - elu: { median: true }, weights: expect.objectContaining({ 0: expect.any(Number), [pool.info.maxSize - 1]: expect.any(Number), @@ -533,6 +532,11 @@ describe('Abstract pool test suite', () => { expect( pool.workerChoiceStrategiesContext.getTaskStatisticsRequirements() ).toStrictEqual({ + elu: { + aggregate: true, + average: false, + median: true, + }, runTime: { aggregate: true, average: false, @@ -543,26 +547,21 @@ describe('Abstract pool test suite', () => { average: true, median: false, }, - elu: { - aggregate: true, - average: false, - median: true, - }, }) pool.setWorkerChoiceStrategyOptions({ - runTime: { median: false }, elu: { median: false }, + runTime: { median: false }, }) expect(pool.opts.workerChoiceStrategyOptions).toStrictEqual({ - runTime: { median: false }, elu: { median: false }, + runTime: { median: false }, }) for (const [, workerChoiceStrategy] of pool.workerChoiceStrategiesContext .workerChoiceStrategies) { expect(workerChoiceStrategy.opts).toStrictEqual({ + elu: { median: false }, runTime: { median: false }, waitTime: { median: false }, - elu: { median: false }, weights: expect.objectContaining({ 0: expect.any(Number), [pool.info.maxSize - 1]: expect.any(Number), @@ -572,17 +571,17 @@ describe('Abstract pool test suite', () => { expect( pool.workerChoiceStrategiesContext.getTaskStatisticsRequirements() ).toStrictEqual({ - runTime: { + elu: { aggregate: true, average: true, median: false, }, - waitTime: { + runTime: { aggregate: true, average: true, median: false, }, - elu: { + waitTime: { aggregate: true, average: true, median: false, @@ -622,20 +621,20 @@ describe('Abstract pool test suite', () => { expect(pool.opts.tasksQueueOptions).toStrictEqual({ concurrency: 1, size: Math.pow(numberOfWorkers, 2), - taskStealing: true, + tasksFinishedTimeout: 2000, tasksStealingOnBackPressure: true, tasksStealingRatio: 0.6, - tasksFinishedTimeout: 2000, + taskStealing: true, }) pool.enableTasksQueue(true, { concurrency: 2 }) expect(pool.opts.enableTasksQueue).toBe(true) expect(pool.opts.tasksQueueOptions).toStrictEqual({ concurrency: 2, size: Math.pow(numberOfWorkers, 2), - taskStealing: true, + tasksFinishedTimeout: 2000, tasksStealingOnBackPressure: true, tasksStealingRatio: 0.6, - tasksFinishedTimeout: 2000, + taskStealing: true, }) pool.enableTasksQueue(false) expect(pool.opts.enableTasksQueue).toBe(false) @@ -652,10 +651,10 @@ describe('Abstract pool test suite', () => { expect(pool.opts.tasksQueueOptions).toStrictEqual({ concurrency: 1, size: Math.pow(numberOfWorkers, 2), - taskStealing: true, + tasksFinishedTimeout: 2000, tasksStealingOnBackPressure: true, tasksStealingRatio: 0.6, - tasksFinishedTimeout: 2000, + taskStealing: true, }) for (const workerNode of pool.workerNodes) { expect(workerNode.tasksQueueBackPressureSize).toBe( @@ -665,18 +664,18 @@ describe('Abstract pool test suite', () => { pool.setTasksQueueOptions({ concurrency: 2, size: 2, - taskStealing: false, + tasksFinishedTimeout: 3000, tasksStealingOnBackPressure: false, tasksStealingRatio: 0.5, - tasksFinishedTimeout: 3000, + taskStealing: false, }) expect(pool.opts.tasksQueueOptions).toStrictEqual({ concurrency: 2, size: 2, - taskStealing: false, + tasksFinishedTimeout: 3000, tasksStealingOnBackPressure: false, tasksStealingRatio: 0.5, - tasksFinishedTimeout: 3000, + taskStealing: false, }) for (const workerNode of pool.workerNodes) { expect(workerNode.tasksQueueBackPressureSize).toBe( @@ -685,16 +684,16 @@ describe('Abstract pool test suite', () => { } pool.setTasksQueueOptions({ concurrency: 1, - taskStealing: true, tasksStealingOnBackPressure: true, + taskStealing: true, }) expect(pool.opts.tasksQueueOptions).toStrictEqual({ concurrency: 1, size: 2, - taskStealing: true, + tasksFinishedTimeout: 3000, tasksStealingOnBackPressure: true, tasksStealingRatio: 0.5, - tasksFinishedTimeout: 3000, + taskStealing: true, }) for (const workerNode of pool.workerNodes) { expect(workerNode.tasksQueueBackPressureSize).toBe( @@ -751,21 +750,21 @@ describe('Abstract pool test suite', () => { './tests/worker-files/thread/testWorker.mjs' ) expect(pool.info).toStrictEqual({ - version, - type: PoolTypes.fixed, - worker: WorkerTypes.thread, - started: true, - ready: true, - defaultStrategy: WorkerChoiceStrategies.ROUND_ROBIN, - strategyRetries: 0, - minSize: numberOfWorkers, - maxSize: numberOfWorkers, - workerNodes: numberOfWorkers, - idleWorkerNodes: numberOfWorkers, busyWorkerNodes: 0, + defaultStrategy: WorkerChoiceStrategies.ROUND_ROBIN, executedTasks: 0, executingTasks: 0, failedTasks: 0, + idleWorkerNodes: numberOfWorkers, + maxSize: numberOfWorkers, + minSize: numberOfWorkers, + ready: true, + started: true, + strategyRetries: 0, + type: PoolTypes.fixed, + version, + worker: WorkerTypes.thread, + workerNodes: numberOfWorkers, }) await pool.destroy() pool = new DynamicClusterPool( @@ -774,21 +773,21 @@ describe('Abstract pool test suite', () => { './tests/worker-files/cluster/testWorker.cjs' ) expect(pool.info).toStrictEqual({ - version, - type: PoolTypes.dynamic, - worker: WorkerTypes.cluster, - started: true, - ready: true, - defaultStrategy: WorkerChoiceStrategies.ROUND_ROBIN, - strategyRetries: 0, - minSize: Math.floor(numberOfWorkers / 2), - maxSize: numberOfWorkers, - workerNodes: Math.floor(numberOfWorkers / 2), - idleWorkerNodes: Math.floor(numberOfWorkers / 2), busyWorkerNodes: 0, + defaultStrategy: WorkerChoiceStrategies.ROUND_ROBIN, executedTasks: 0, executingTasks: 0, failedTasks: 0, + idleWorkerNodes: Math.floor(numberOfWorkers / 2), + maxSize: numberOfWorkers, + minSize: Math.floor(numberOfWorkers / 2), + ready: true, + started: true, + strategyRetries: 0, + type: PoolTypes.dynamic, + version, + worker: WorkerTypes.cluster, + workerNodes: Math.floor(numberOfWorkers / 2), }) await pool.destroy() }) @@ -801,29 +800,29 @@ describe('Abstract pool test suite', () => { for (const workerNode of pool.workerNodes) { expect(workerNode).toBeInstanceOf(WorkerNode) expect(workerNode.usage).toStrictEqual({ + elu: { + active: { + history: expect.any(CircularBuffer), + }, + idle: { + history: expect.any(CircularBuffer), + }, + }, + runTime: { + history: expect.any(CircularBuffer), + }, tasks: { executed: 0, executing: 0, - queued: 0, + failed: 0, maxQueued: 0, + queued: 0, sequentiallyStolen: 0, stolen: 0, - failed: 0, - }, - runTime: { - history: expect.any(CircularBuffer), }, waitTime: { history: expect.any(CircularBuffer), }, - elu: { - idle: { - history: expect.any(CircularBuffer), - }, - active: { - history: expect.any(CircularBuffer), - }, - }, }) } await pool.destroy() @@ -867,15 +866,15 @@ describe('Abstract pool test suite', () => { for (const workerNode of pool.workerNodes) { expect(workerNode).toBeInstanceOf(WorkerNode) expect(workerNode.info).toStrictEqual({ - id: expect.any(Number), - type: WorkerTypes.cluster, + backPressure: false, + backPressureStealing: false, + continuousStealing: false, dynamic: false, + id: expect.any(Number), ready: true, stealing: false, stolen: false, - continuousStealing: false, - backPressureStealing: false, - backPressure: false, + type: WorkerTypes.cluster, }) } await pool.destroy() @@ -887,15 +886,15 @@ describe('Abstract pool test suite', () => { for (const workerNode of pool.workerNodes) { expect(workerNode).toBeInstanceOf(WorkerNode) expect(workerNode.info).toStrictEqual({ - id: expect.any(Number), - type: WorkerTypes.thread, + backPressure: false, + backPressureStealing: false, + continuousStealing: false, dynamic: false, + id: expect.any(Number), ready: true, stealing: false, stolen: false, - continuousStealing: false, - backPressureStealing: false, - backPressure: false, + type: WorkerTypes.thread, }) } await pool.destroy() @@ -978,57 +977,57 @@ describe('Abstract pool test suite', () => { } for (const workerNode of pool.workerNodes) { expect(workerNode.usage).toStrictEqual({ + elu: { + active: { + history: expect.any(CircularBuffer), + }, + idle: { + history: expect.any(CircularBuffer), + }, + }, + runTime: { + history: expect.any(CircularBuffer), + }, tasks: { executed: 0, executing: maxMultiplier, - queued: 0, + failed: 0, maxQueued: 0, + queued: 0, sequentiallyStolen: 0, stolen: 0, - failed: 0, - }, - runTime: { - history: expect.any(CircularBuffer), }, waitTime: { history: expect.any(CircularBuffer), }, + }) + } + await Promise.all(promises) + for (const workerNode of pool.workerNodes) { + expect(workerNode.usage).toStrictEqual({ elu: { - idle: { + active: { history: expect.any(CircularBuffer), }, - active: { + idle: { history: expect.any(CircularBuffer), }, }, - }) - } - await Promise.all(promises) - for (const workerNode of pool.workerNodes) { - expect(workerNode.usage).toStrictEqual({ + runTime: { + history: expect.any(CircularBuffer), + }, tasks: { executed: maxMultiplier, executing: 0, - queued: 0, + failed: 0, maxQueued: 0, + queued: 0, sequentiallyStolen: 0, stolen: 0, - failed: 0, - }, - runTime: { - history: expect.any(CircularBuffer), }, waitTime: { history: expect.any(CircularBuffer), }, - elu: { - idle: { - history: expect.any(CircularBuffer), - }, - active: { - history: expect.any(CircularBuffer), - }, - }, }) } await pool.destroy() @@ -1048,29 +1047,29 @@ describe('Abstract pool test suite', () => { await Promise.all(promises) for (const workerNode of pool.workerNodes) { expect(workerNode.usage).toStrictEqual({ + elu: { + active: { + history: expect.any(CircularBuffer), + }, + idle: { + history: expect.any(CircularBuffer), + }, + }, + runTime: { + history: expect.any(CircularBuffer), + }, tasks: { executed: expect.any(Number), executing: 0, - queued: 0, + failed: 0, maxQueued: 0, + queued: 0, sequentiallyStolen: 0, stolen: 0, - failed: 0, - }, - runTime: { - history: expect.any(CircularBuffer), }, waitTime: { history: expect.any(CircularBuffer), }, - elu: { - idle: { - history: expect.any(CircularBuffer), - }, - active: { - history: expect.any(CircularBuffer), - }, - }, }) expect(workerNode.usage.tasks.executed).toBeGreaterThan(0) expect(workerNode.usage.tasks.executed).toBeLessThanOrEqual( @@ -1080,29 +1079,29 @@ describe('Abstract pool test suite', () => { pool.setWorkerChoiceStrategy(WorkerChoiceStrategies.FAIR_SHARE) for (const workerNode of pool.workerNodes) { expect(workerNode.usage).toStrictEqual({ + elu: { + active: { + history: expect.any(CircularBuffer), + }, + idle: { + history: expect.any(CircularBuffer), + }, + }, + runTime: { + history: expect.any(CircularBuffer), + }, tasks: { executed: expect.any(Number), executing: 0, - queued: 0, + failed: 0, maxQueued: 0, + queued: 0, sequentiallyStolen: 0, stolen: 0, - failed: 0, - }, - runTime: { - history: expect.any(CircularBuffer), }, waitTime: { history: expect.any(CircularBuffer), }, - elu: { - idle: { - history: expect.any(CircularBuffer), - }, - active: { - history: expect.any(CircularBuffer), - }, - }, }) expect(workerNode.usage.tasks.executed).toBeGreaterThan(0) expect(workerNode.usage.tasks.executed).toBeLessThanOrEqual( @@ -1129,21 +1128,21 @@ describe('Abstract pool test suite', () => { expect(pool.emitter.eventNames()).toStrictEqual([PoolEvents.ready]) expect(poolReady).toBe(1) expect(poolInfo).toStrictEqual({ - version, - type: PoolTypes.dynamic, - worker: WorkerTypes.cluster, - started: true, - ready: true, - defaultStrategy: WorkerChoiceStrategies.ROUND_ROBIN, - strategyRetries: expect.any(Number), - minSize: expect.any(Number), - maxSize: expect.any(Number), - workerNodes: expect.any(Number), - idleWorkerNodes: expect.any(Number), busyWorkerNodes: expect.any(Number), + defaultStrategy: WorkerChoiceStrategies.ROUND_ROBIN, executedTasks: expect.any(Number), executingTasks: expect.any(Number), failedTasks: expect.any(Number), + idleWorkerNodes: expect.any(Number), + maxSize: expect.any(Number), + minSize: expect.any(Number), + ready: true, + started: true, + strategyRetries: expect.any(Number), + type: PoolTypes.dynamic, + version, + worker: WorkerTypes.cluster, + workerNodes: expect.any(Number), }) await pool.destroy() }) @@ -1170,21 +1169,21 @@ describe('Abstract pool test suite', () => { // So in total numberOfWorkers + 1 times for a loop submitting up to numberOfWorkers * 2 tasks to the fixed pool. expect(poolBusy).toBe(numberOfWorkers + 1) expect(poolInfo).toStrictEqual({ - version, - type: PoolTypes.fixed, - worker: WorkerTypes.thread, - started: true, - ready: true, - defaultStrategy: WorkerChoiceStrategies.ROUND_ROBIN, - strategyRetries: expect.any(Number), - minSize: expect.any(Number), - maxSize: expect.any(Number), - workerNodes: expect.any(Number), - idleWorkerNodes: expect.any(Number), busyWorkerNodes: expect.any(Number), + defaultStrategy: WorkerChoiceStrategies.ROUND_ROBIN, executedTasks: expect.any(Number), executingTasks: expect.any(Number), failedTasks: expect.any(Number), + idleWorkerNodes: expect.any(Number), + maxSize: expect.any(Number), + minSize: expect.any(Number), + ready: true, + started: true, + strategyRetries: expect.any(Number), + type: PoolTypes.fixed, + version, + worker: WorkerTypes.thread, + workerNodes: expect.any(Number), }) await pool.destroy() }) @@ -1210,21 +1209,21 @@ describe('Abstract pool test suite', () => { await Promise.all(promises) expect(poolFull).toBe(1) expect(poolInfo).toStrictEqual({ - version, - type: PoolTypes.dynamic, - worker: WorkerTypes.thread, - started: true, - ready: true, - defaultStrategy: WorkerChoiceStrategies.ROUND_ROBIN, - strategyRetries: expect.any(Number), - minSize: expect.any(Number), - maxSize: expect.any(Number), - workerNodes: expect.any(Number), - idleWorkerNodes: expect.any(Number), busyWorkerNodes: expect.any(Number), + defaultStrategy: WorkerChoiceStrategies.ROUND_ROBIN, executedTasks: expect.any(Number), executingTasks: expect.any(Number), failedTasks: expect.any(Number), + idleWorkerNodes: expect.any(Number), + maxSize: expect.any(Number), + minSize: expect.any(Number), + ready: true, + started: true, + strategyRetries: expect.any(Number), + type: PoolTypes.dynamic, + version, + worker: WorkerTypes.thread, + workerNodes: expect.any(Number), }) await pool.destroy() }) @@ -1253,27 +1252,27 @@ describe('Abstract pool test suite', () => { await Promise.all(promises) expect(poolBackPressure).toBe(1) expect(poolInfo).toStrictEqual({ - version, - type: PoolTypes.fixed, - worker: WorkerTypes.thread, - started: true, - ready: true, - defaultStrategy: WorkerChoiceStrategies.ROUND_ROBIN, - strategyRetries: expect.any(Number), - minSize: expect.any(Number), - maxSize: expect.any(Number), - workerNodes: expect.any(Number), - idleWorkerNodes: expect.any(Number), - busyWorkerNodes: expect.any(Number), - stealingWorkerNodes: expect.any(Number), + backPressure: true, backPressureWorkerNodes: expect.any(Number), + busyWorkerNodes: expect.any(Number), + defaultStrategy: WorkerChoiceStrategies.ROUND_ROBIN, executedTasks: expect.any(Number), executingTasks: expect.any(Number), + failedTasks: expect.any(Number), + idleWorkerNodes: expect.any(Number), maxQueuedTasks: expect.any(Number), + maxSize: expect.any(Number), + minSize: expect.any(Number), queuedTasks: expect.any(Number), - backPressure: true, + ready: true, + started: true, + stealingWorkerNodes: expect.any(Number), stolenTasks: expect.any(Number), - failedTasks: expect.any(Number), + strategyRetries: expect.any(Number), + type: PoolTypes.fixed, + version, + worker: WorkerTypes.thread, + workerNodes: expect.any(Number), }) expect(pool.hasBackPressure.callCount).toBeGreaterThanOrEqual(7) await pool.destroy() @@ -1343,18 +1342,18 @@ describe('Abstract pool test suite', () => { let afterCalls = 0 let resolveCalls = 0 const hook = createHook({ + after (asyncId) { + if (asyncId === taskAsyncId) afterCalls++ + }, + before (asyncId) { + if (asyncId === taskAsyncId) beforeCalls++ + }, init (asyncId, type) { if (type === 'poolifier:task') { initCalls++ taskAsyncId = asyncId } }, - before (asyncId) { - if (asyncId === taskAsyncId) beforeCalls++ - }, - after (asyncId) { - if (asyncId === taskAsyncId) afterCalls++ - }, promiseResolve () { if (executionAsyncId() === taskAsyncId) resolveCalls++ }, @@ -1432,24 +1431,24 @@ describe('Abstract pool test suite', () => { ).rejects.toThrow(new TypeError('taskFunction property must be a function')) await expect( dynamicThreadPool.addTaskFunction('test', { - taskFunction: () => {}, priority: -21, + taskFunction: () => {}, }) ).rejects.toThrow( new RangeError("Property 'priority' must be between -20 and 19") ) await expect( dynamicThreadPool.addTaskFunction('test', { - taskFunction: () => {}, priority: 20, + taskFunction: () => {}, }) ).rejects.toThrow( new RangeError("Property 'priority' must be between -20 and 19") ) await expect( dynamicThreadPool.addTaskFunction('test', { - taskFunction: () => {}, strategy: 'invalidStrategy', + taskFunction: () => {}, }) ).rejects.toThrow( new Error("Invalid worker choice strategy 'invalidStrategy'") @@ -1466,14 +1465,14 @@ describe('Abstract pool test suite', () => { } await expect( dynamicThreadPool.addTaskFunction('echo', { - taskFunction: echoTaskFunction, strategy: WorkerChoiceStrategies.LEAST_ELU, + taskFunction: echoTaskFunction, }) ).resolves.toBe(true) expect(dynamicThreadPool.taskFunctions.size).toBe(1) expect(dynamicThreadPool.taskFunctions.get('echo')).toStrictEqual({ - taskFunction: echoTaskFunction, strategy: WorkerChoiceStrategies.LEAST_ELU, + taskFunction: echoTaskFunction, }) expect([ ...dynamicThreadPool.workerChoiceStrategiesContext.workerChoiceStrategies.keys(), @@ -1491,28 +1490,28 @@ describe('Abstract pool test suite', () => { expect(echoResult).toStrictEqual(taskFunctionData) for (const workerNode of dynamicThreadPool.workerNodes) { expect(workerNode.getTaskFunctionWorkerUsage('echo')).toStrictEqual({ + elu: expect.objectContaining({ + active: expect.objectContaining({ + history: expect.any(CircularBuffer), + }), + idle: expect.objectContaining({ + history: expect.any(CircularBuffer), + }), + }), + runTime: { + history: expect.any(CircularBuffer), + }, tasks: { executed: expect.any(Number), executing: 0, + failed: 0, queued: 0, sequentiallyStolen: 0, stolen: 0, - failed: 0, - }, - runTime: { - history: expect.any(CircularBuffer), }, waitTime: { history: expect.any(CircularBuffer), }, - elu: expect.objectContaining({ - idle: expect.objectContaining({ - history: expect.any(CircularBuffer), - }), - active: expect.objectContaining({ - history: expect.any(CircularBuffer), - }), - }), }) expect( workerNode.getTaskFunctionWorkerUsage('echo').tasks.executed @@ -1576,13 +1575,13 @@ describe('Abstract pool test suite', () => { return data } await dynamicThreadPool.addTaskFunction('echo', { - taskFunction: echoTaskFunction, strategy: WorkerChoiceStrategies.LEAST_ELU, + taskFunction: echoTaskFunction, }) expect(dynamicThreadPool.taskFunctions.size).toBe(1) expect(dynamicThreadPool.taskFunctions.get('echo')).toStrictEqual({ - taskFunction: echoTaskFunction, strategy: WorkerChoiceStrategies.LEAST_ELU, + taskFunction: echoTaskFunction, }) expect([ ...dynamicThreadPool.workerChoiceStrategiesContext.workerChoiceStrategies.keys(), @@ -1619,9 +1618,9 @@ describe('Abstract pool test suite', () => { await waitPoolEvents(dynamicThreadPool, PoolEvents.ready, 1) expect(dynamicThreadPool.listTaskFunctionsProperties()).toStrictEqual([ { name: DEFAULT_TASK_NAME }, - { name: 'jsonIntegerSerialization' }, { name: 'factorial' }, { name: 'fibonacci' }, + { name: 'jsonIntegerSerialization' }, ]) await dynamicThreadPool.destroy() const fixedClusterPool = new FixedClusterPool( @@ -1631,9 +1630,9 @@ describe('Abstract pool test suite', () => { await waitPoolEvents(fixedClusterPool, PoolEvents.ready, 1) expect(fixedClusterPool.listTaskFunctionsProperties()).toStrictEqual([ { name: DEFAULT_TASK_NAME }, - { name: 'jsonIntegerSerialization' }, { name: 'factorial' }, { name: 'fibonacci' }, + { name: 'jsonIntegerSerialization' }, ]) await fixedClusterPool.destroy() }) @@ -1667,9 +1666,9 @@ describe('Abstract pool test suite', () => { ) expect(dynamicThreadPool.listTaskFunctionsProperties()).toStrictEqual([ { name: DEFAULT_TASK_NAME }, - { name: 'jsonIntegerSerialization' }, { name: 'factorial' }, { name: 'fibonacci' }, + { name: 'jsonIntegerSerialization' }, ]) await expect( dynamicThreadPool.setDefaultTaskFunction('factorial') @@ -1677,8 +1676,8 @@ describe('Abstract pool test suite', () => { expect(dynamicThreadPool.listTaskFunctionsProperties()).toStrictEqual([ { name: DEFAULT_TASK_NAME }, { name: 'factorial' }, - { name: 'jsonIntegerSerialization' }, { name: 'fibonacci' }, + { name: 'jsonIntegerSerialization' }, ]) await expect( dynamicThreadPool.setDefaultTaskFunction('fibonacci') @@ -1686,8 +1685,8 @@ describe('Abstract pool test suite', () => { expect(dynamicThreadPool.listTaskFunctionsProperties()).toStrictEqual([ { name: DEFAULT_TASK_NAME }, { name: 'fibonacci' }, - { name: 'jsonIntegerSerialization' }, { name: 'factorial' }, + { name: 'jsonIntegerSerialization' }, ]) await dynamicThreadPool.destroy() }) @@ -1700,7 +1699,7 @@ describe('Abstract pool test suite', () => { ) const data = { n: 10 } const result0 = await pool.execute(data) - expect(result0).toStrictEqual({ ok: 1 }) + expect(result0).toStrictEqual(3628800) const result1 = await pool.execute(data, 'jsonIntegerSerialization') expect(result1).toStrictEqual({ ok: 1 }) const result2 = await pool.execute(data, 'factorial') @@ -1712,9 +1711,9 @@ describe('Abstract pool test suite', () => { for (const workerNode of pool.workerNodes) { expect(workerNode.info.taskFunctionsProperties).toStrictEqual([ { name: DEFAULT_TASK_NAME }, - { name: 'jsonIntegerSerialization' }, { name: 'factorial' }, { name: 'fibonacci' }, + { name: 'jsonIntegerSerialization' }, ]) expect(workerNode.taskFunctionsUsage.size).toBe(3) expect(workerNode.usage.tasks.executed).toBeGreaterThan(0) @@ -1723,6 +1722,17 @@ describe('Abstract pool test suite', () => { expect( workerNode.getTaskFunctionWorkerUsage(taskFunctionProperties.name) ).toStrictEqual({ + elu: { + active: { + history: expect.any(CircularBuffer), + }, + idle: { + history: expect.any(CircularBuffer), + }, + }, + runTime: { + history: expect.any(CircularBuffer), + }, tasks: { executed: expect.any(Number), executing: 0, @@ -1731,20 +1741,9 @@ describe('Abstract pool test suite', () => { sequentiallyStolen: 0, stolen: 0, }, - runTime: { - history: expect.any(CircularBuffer), - }, waitTime: { history: expect.any(CircularBuffer), }, - elu: { - idle: { - history: expect.any(CircularBuffer), - }, - active: { - history: expect.any(CircularBuffer), - }, - }, }) expect( workerNode.getTaskFunctionWorkerUsage(taskFunctionProperties.name) @@ -1786,7 +1785,10 @@ describe('Abstract pool test suite', () => { await expect(pool.mapExecute([undefined], 'unknown')).rejects.toBe( "Task function 'unknown' not found" ) - let results = await pool.mapExecute([{}, {}, {}, {}]) + let results = await pool.mapExecute( + [{}, {}, {}, {}], + 'jsonIntegerSerialization' + ) expect(results).toStrictEqual([{ ok: 1 }, { ok: 1 }, { ok: 1 }, { ok: 1 }]) expect(pool.info.executingTasks).toBe(0) expect(pool.info.executedTasks).toBe(4) @@ -1822,7 +1824,7 @@ describe('Abstract pool test suite', () => { ) const data = { n: 10 } const result0 = await pool.execute(data) - expect(result0).toStrictEqual({ ok: 1 }) + expect(result0).toStrictEqual(3628800) const result1 = await pool.execute(data, 'jsonIntegerSerialization') expect(result1).toStrictEqual({ ok: 1 }) const result2 = await pool.execute(data, 'factorial') @@ -1834,9 +1836,9 @@ describe('Abstract pool test suite', () => { for (const workerNode of pool.workerNodes) { expect(workerNode.info.taskFunctionsProperties).toStrictEqual([ { name: DEFAULT_TASK_NAME }, - { name: 'jsonIntegerSerialization' }, { name: 'factorial' }, { name: 'fibonacci', priority: -5 }, + { name: 'jsonIntegerSerialization' }, ]) expect(workerNode.taskFunctionsUsage.size).toBe(3) expect(workerNode.usage.tasks.executed).toBeGreaterThan(0) @@ -1845,6 +1847,17 @@ describe('Abstract pool test suite', () => { expect( workerNode.getTaskFunctionWorkerUsage(taskFunctionProperties.name) ).toStrictEqual({ + elu: { + active: { + history: expect.any(CircularBuffer), + }, + idle: { + history: expect.any(CircularBuffer), + }, + }, + runTime: { + history: expect.any(CircularBuffer), + }, tasks: { executed: expect.any(Number), executing: 0, @@ -1853,20 +1866,9 @@ describe('Abstract pool test suite', () => { sequentiallyStolen: 0, stolen: 0, }, - runTime: { - history: expect.any(CircularBuffer), - }, waitTime: { history: expect.any(CircularBuffer), }, - elu: { - idle: { - history: expect.any(CircularBuffer), - }, - active: { - history: expect.any(CircularBuffer), - }, - }, }) expect( workerNode.getTaskFunctionWorkerUsage(taskFunctionProperties.name) @@ -1906,9 +1908,9 @@ describe('Abstract pool test suite', () => { const workerNodeKey = 0 await expect( pool.sendTaskFunctionOperationToWorker(workerNodeKey, { + taskFunction: (() => {}).toString(), taskFunctionOperation: 'add', taskFunctionProperties: { name: 'empty' }, - taskFunction: (() => {}).toString(), }) ).resolves.toBe(true) expect( @@ -1929,9 +1931,9 @@ describe('Abstract pool test suite', () => { ) await expect( pool.sendTaskFunctionOperationToWorkers({ + taskFunction: (() => {}).toString(), taskFunctionOperation: 'add', taskFunctionProperties: { name: 'empty' }, - taskFunction: (() => {}).toString(), }) ).resolves.toBe(true) for (const workerNode of pool.workerNodes) { diff --git a/tests/pools/cluster/dynamic.test.mjs b/tests/pools/cluster/dynamic.test.mjs index c93a6711..2ac250ca 100644 --- a/tests/pools/cluster/dynamic.test.mjs +++ b/tests/pools/cluster/dynamic.test.mjs @@ -99,8 +99,8 @@ describe('Dynamic cluster pool test suite', () => { './tests/worker-files/cluster/longRunningWorkerHardBehavior.cjs', { errorHandler: e => console.error(e), - onlineHandler: () => console.info('long executing worker is online'), exitHandler: () => console.info('long executing worker exited'), + onlineHandler: () => console.info('long executing worker is online'), } ) expect(longRunningPool.workerNodes.length).toBe(min) @@ -127,8 +127,8 @@ describe('Dynamic cluster pool test suite', () => { './tests/worker-files/cluster/longRunningWorkerSoftBehavior.cjs', { errorHandler: e => console.error(e), - onlineHandler: () => console.info('long executing worker is online'), exitHandler: () => console.info('long executing worker exited'), + onlineHandler: () => console.info('long executing worker is online'), } ) expect(longRunningPool.workerNodes.length).toBe(min) diff --git a/tests/pools/cluster/fixed.test.mjs b/tests/pools/cluster/fixed.test.mjs index 68808f60..30bce79e 100644 --- a/tests/pools/cluster/fixed.test.mjs +++ b/tests/pools/cluster/fixed.test.mjs @@ -1,6 +1,5 @@ -import cluster from 'node:cluster' - import { expect } from 'expect' +import cluster from 'node:cluster' import { FixedClusterPool, PoolEvents } from '../../../lib/index.cjs' import { DEFAULT_TASK_NAME } from '../../../lib/utils.cjs' @@ -10,7 +9,7 @@ import { waitPoolEvents, waitWorkerEvents } from '../../test-utils.cjs' describe('Fixed cluster pool test suite', () => { const numberOfWorkers = 8 const tasksConcurrency = 2 - let pool, queuePool, emptyPool, echoPool, errorPool, asyncErrorPool, asyncPool + let asyncErrorPool, asyncPool, echoPool, emptyPool, errorPool, pool, queuePool before('Create pools', () => { pool = new FixedClusterPool( @@ -25,10 +24,10 @@ describe('Fixed cluster pool test suite', () => { './tests/worker-files/cluster/testWorker.cjs', { enableTasksQueue: true, + errorHandler: e => console.error(e), tasksQueueOptions: { concurrency: tasksConcurrency, }, - errorHandler: e => console.error(e), } ) emptyPool = new FixedClusterPool( @@ -213,9 +212,9 @@ describe('Fixed cluster pool test suite', () => { expect(typeof inError === 'string').toBe(true) expect(inError).toBe('Error Message from ClusterWorker') expect(taskError).toStrictEqual({ - name: DEFAULT_TASK_NAME, - message: 'Error Message from ClusterWorker', data, + message: 'Error Message from ClusterWorker', + name: DEFAULT_TASK_NAME, }) expect( errorPool.workerNodes.some( @@ -244,9 +243,9 @@ describe('Fixed cluster pool test suite', () => { expect(typeof inError === 'string').toBe(true) expect(inError).toBe('Error Message from ClusterWorker:async') expect(taskError).toStrictEqual({ - name: DEFAULT_TASK_NAME, - message: 'Error Message from ClusterWorker:async', data, + message: 'Error Message from ClusterWorker:async', + name: DEFAULT_TASK_NAME, }) expect( asyncErrorPool.workerNodes.some( @@ -307,8 +306,8 @@ describe('Fixed cluster pool test suite', () => { }) expect(cluster.settings).toMatchObject({ args: ['--use', 'http'], - silent: true, exec: workerFilePath, + silent: true, }) await pool.destroy() }) diff --git a/tests/pools/selection-strategies/selection-strategies-utils.test.mjs b/tests/pools/selection-strategies/selection-strategies-utils.test.mjs index f2223b83..2968f894 100644 --- a/tests/pools/selection-strategies/selection-strategies-utils.test.mjs +++ b/tests/pools/selection-strategies/selection-strategies-utils.test.mjs @@ -29,18 +29,18 @@ describe('Selection strategies utils test suite', () => { it('Verify buildWorkerChoiceStrategyOptions() behavior', async () => { expect(buildWorkerChoiceStrategyOptions(clusterFixedPool)).toStrictEqual({ + elu: { median: false }, runTime: { median: false }, waitTime: { median: false }, - elu: { median: false }, weights: expect.objectContaining({ 0: expect.any(Number), [clusterFixedPool.info.maxSize - 1]: expect.any(Number), }), }) const workerChoiceStrategyOptions = { + elu: { median: true }, runTime: { median: true }, waitTime: { median: true }, - elu: { median: true }, weights: { 0: 100, 1: 100, @@ -59,9 +59,9 @@ describe('Selection strategies utils test suite', () => { threadFixedPool.info.maxSize * 2 ) const workerChoiceStrategyOptions = { + elu: { median: true }, runTime: { median: true }, waitTime: { median: true }, - elu: { median: true }, weights: { 0: 100, 1: 100, diff --git a/tests/pools/selection-strategies/selection-strategies.test.mjs b/tests/pools/selection-strategies/selection-strategies.test.mjs index e8df7e1a..6a1aab6b 100644 --- a/tests/pools/selection-strategies/selection-strategies.test.mjs +++ b/tests/pools/selection-strategies/selection-strategies.test.mjs @@ -1,6 +1,5 @@ -import { randomInt } from 'node:crypto' - import { expect } from 'expect' +import { randomInt } from 'node:crypto' import { CircularBuffer } from '../../../lib/circular-buffer.cjs' import { @@ -158,8 +157,8 @@ describe('Selection strategies test suite', () => { { workerChoiceStrategy } ) expect(pool.workerChoiceStrategiesContext.getPolicy()).toStrictEqual({ - dynamicWorkerUsage: false, dynamicWorkerReady: true, + dynamicWorkerUsage: false, }) await pool.destroy() pool = new DynamicThreadPool( @@ -169,8 +168,8 @@ describe('Selection strategies test suite', () => { { workerChoiceStrategy } ) expect(pool.workerChoiceStrategiesContext.getPolicy()).toStrictEqual({ - dynamicWorkerUsage: false, dynamicWorkerReady: true, + dynamicWorkerUsage: false, }) // We need to clean up the resources after our test await pool.destroy() @@ -186,17 +185,17 @@ describe('Selection strategies test suite', () => { expect( pool.workerChoiceStrategiesContext.getTaskStatisticsRequirements() ).toStrictEqual({ - runTime: { + elu: { aggregate: false, average: false, median: false, }, - waitTime: { + runTime: { aggregate: false, average: false, median: false, }, - elu: { + waitTime: { aggregate: false, average: false, median: false, @@ -212,17 +211,17 @@ describe('Selection strategies test suite', () => { expect( pool.workerChoiceStrategiesContext.getTaskStatisticsRequirements() ).toStrictEqual({ - runTime: { + elu: { aggregate: false, average: false, median: false, }, - waitTime: { + runTime: { aggregate: false, average: false, median: false, }, - elu: { + waitTime: { aggregate: false, average: false, median: false, @@ -248,29 +247,29 @@ describe('Selection strategies test suite', () => { await Promise.all(promises) for (const workerNode of pool.workerNodes) { expect(workerNode.usage).toStrictEqual({ + elu: { + active: { + history: expect.any(CircularBuffer), + }, + idle: { + history: expect.any(CircularBuffer), + }, + }, + runTime: { + history: expect.any(CircularBuffer), + }, tasks: { executed: maxMultiplier, executing: 0, - queued: 0, + failed: 0, maxQueued: 0, + queued: 0, sequentiallyStolen: 0, stolen: 0, - failed: 0, - }, - runTime: { - history: expect.any(CircularBuffer), }, waitTime: { history: expect.any(CircularBuffer), }, - elu: { - idle: { - history: expect.any(CircularBuffer), - }, - active: { - history: expect.any(CircularBuffer), - }, - }, }) } expect( @@ -304,29 +303,29 @@ describe('Selection strategies test suite', () => { await Promise.all(promises) for (const workerNode of pool.workerNodes) { expect(workerNode.usage).toStrictEqual({ + elu: { + active: { + history: expect.any(CircularBuffer), + }, + idle: { + history: expect.any(CircularBuffer), + }, + }, + runTime: { + history: expect.any(CircularBuffer), + }, tasks: { executed: expect.any(Number), executing: 0, - queued: 0, + failed: 0, maxQueued: 0, + queued: 0, sequentiallyStolen: 0, stolen: 0, - failed: 0, - }, - runTime: { - history: expect.any(CircularBuffer), }, waitTime: { history: expect.any(CircularBuffer), }, - elu: { - idle: { - history: expect.any(CircularBuffer), - }, - active: { - history: expect.any(CircularBuffer), - }, - }, }) expect(workerNode.usage.tasks.executed).toBeGreaterThanOrEqual(0) expect(workerNode.usage.tasks.executed).toBeLessThanOrEqual( @@ -433,8 +432,8 @@ describe('Selection strategies test suite', () => { { workerChoiceStrategy } ) expect(pool.workerChoiceStrategiesContext.getPolicy()).toStrictEqual({ - dynamicWorkerUsage: false, dynamicWorkerReady: true, + dynamicWorkerUsage: false, }) await pool.destroy() pool = new DynamicThreadPool( @@ -444,8 +443,8 @@ describe('Selection strategies test suite', () => { { workerChoiceStrategy } ) expect(pool.workerChoiceStrategiesContext.getPolicy()).toStrictEqual({ - dynamicWorkerUsage: false, dynamicWorkerReady: true, + dynamicWorkerUsage: false, }) // We need to clean up the resources after our test await pool.destroy() @@ -461,17 +460,17 @@ describe('Selection strategies test suite', () => { expect( pool.workerChoiceStrategiesContext.getTaskStatisticsRequirements() ).toStrictEqual({ - runTime: { + elu: { aggregate: false, average: false, median: false, }, - waitTime: { + runTime: { aggregate: false, average: false, median: false, }, - elu: { + waitTime: { aggregate: false, average: false, median: false, @@ -487,17 +486,17 @@ describe('Selection strategies test suite', () => { expect( pool.workerChoiceStrategiesContext.getTaskStatisticsRequirements() ).toStrictEqual({ - runTime: { + elu: { aggregate: false, average: false, median: false, }, - waitTime: { + runTime: { aggregate: false, average: false, median: false, }, - elu: { + waitTime: { aggregate: false, average: false, median: false, @@ -522,29 +521,29 @@ describe('Selection strategies test suite', () => { await Promise.all(promises) for (const workerNode of pool.workerNodes) { expect(workerNode.usage).toStrictEqual({ + elu: { + active: { + history: expect.any(CircularBuffer), + }, + idle: { + history: expect.any(CircularBuffer), + }, + }, + runTime: { + history: expect.any(CircularBuffer), + }, tasks: { executed: expect.any(Number), executing: 0, - queued: 0, + failed: 0, maxQueued: 0, + queued: 0, sequentiallyStolen: 0, stolen: 0, - failed: 0, - }, - runTime: { - history: expect.any(CircularBuffer), }, waitTime: { history: expect.any(CircularBuffer), }, - elu: { - idle: { - history: expect.any(CircularBuffer), - }, - active: { - history: expect.any(CircularBuffer), - }, - }, }) expect(workerNode.usage.tasks.executed).toBeGreaterThanOrEqual(0) expect(workerNode.usage.tasks.executed).toBeLessThanOrEqual( @@ -581,29 +580,29 @@ describe('Selection strategies test suite', () => { await Promise.all(promises) for (const workerNode of pool.workerNodes) { expect(workerNode.usage).toStrictEqual({ + elu: { + active: { + history: expect.any(CircularBuffer), + }, + idle: { + history: expect.any(CircularBuffer), + }, + }, + runTime: { + history: expect.any(CircularBuffer), + }, tasks: { executed: expect.any(Number), executing: 0, - queued: 0, + failed: 0, maxQueued: 0, + queued: 0, sequentiallyStolen: 0, stolen: 0, - failed: 0, - }, - runTime: { - history: expect.any(CircularBuffer), }, waitTime: { history: expect.any(CircularBuffer), }, - elu: { - idle: { - history: expect.any(CircularBuffer), - }, - active: { - history: expect.any(CircularBuffer), - }, - }, }) expect(workerNode.usage.tasks.executed).toBeGreaterThanOrEqual(0) expect(workerNode.usage.tasks.executed).toBeLessThanOrEqual( @@ -632,8 +631,8 @@ describe('Selection strategies test suite', () => { { workerChoiceStrategy } ) expect(pool.workerChoiceStrategiesContext.getPolicy()).toStrictEqual({ - dynamicWorkerUsage: false, dynamicWorkerReady: true, + dynamicWorkerUsage: false, }) await pool.destroy() pool = new DynamicThreadPool( @@ -643,8 +642,8 @@ describe('Selection strategies test suite', () => { { workerChoiceStrategy } ) expect(pool.workerChoiceStrategiesContext.getPolicy()).toStrictEqual({ - dynamicWorkerUsage: false, dynamicWorkerReady: true, + dynamicWorkerUsage: false, }) // We need to clean up the resources after our test await pool.destroy() @@ -660,18 +659,18 @@ describe('Selection strategies test suite', () => { expect( pool.workerChoiceStrategiesContext.getTaskStatisticsRequirements() ).toStrictEqual({ - runTime: { - aggregate: true, + elu: { + aggregate: false, average: false, median: false, }, - waitTime: { + runTime: { aggregate: true, average: false, median: false, }, - elu: { - aggregate: false, + waitTime: { + aggregate: true, average: false, median: false, }, @@ -686,18 +685,18 @@ describe('Selection strategies test suite', () => { expect( pool.workerChoiceStrategiesContext.getTaskStatisticsRequirements() ).toStrictEqual({ - runTime: { - aggregate: true, + elu: { + aggregate: false, average: false, median: false, }, - waitTime: { + runTime: { aggregate: true, average: false, median: false, }, - elu: { - aggregate: false, + waitTime: { + aggregate: true, average: false, median: false, }, @@ -721,29 +720,29 @@ describe('Selection strategies test suite', () => { await Promise.all(promises) for (const workerNode of pool.workerNodes) { expect(workerNode.usage).toStrictEqual({ + elu: { + active: { + history: expect.any(CircularBuffer), + }, + idle: { + history: expect.any(CircularBuffer), + }, + }, + runTime: expect.objectContaining({ + history: expect.any(CircularBuffer), + }), tasks: { executed: expect.any(Number), executing: 0, - queued: 0, + failed: 0, maxQueued: 0, + queued: 0, sequentiallyStolen: 0, stolen: 0, - failed: 0, }, - runTime: expect.objectContaining({ - history: expect.any(CircularBuffer), - }), waitTime: expect.objectContaining({ history: expect.any(CircularBuffer), }), - elu: { - idle: { - history: expect.any(CircularBuffer), - }, - active: { - history: expect.any(CircularBuffer), - }, - }, }) expect(workerNode.usage.tasks.executed).toBeGreaterThanOrEqual(0) expect(workerNode.usage.tasks.executed).toBeLessThanOrEqual( @@ -790,29 +789,29 @@ describe('Selection strategies test suite', () => { await Promise.all(promises) for (const workerNode of pool.workerNodes) { expect(workerNode.usage).toStrictEqual({ + elu: { + active: { + history: expect.any(CircularBuffer), + }, + idle: { + history: expect.any(CircularBuffer), + }, + }, + runTime: expect.objectContaining({ + history: expect.any(CircularBuffer), + }), tasks: { executed: expect.any(Number), executing: 0, - queued: 0, + failed: 0, maxQueued: 0, + queued: 0, sequentiallyStolen: 0, stolen: 0, - failed: 0, }, - runTime: expect.objectContaining({ - history: expect.any(CircularBuffer), - }), waitTime: expect.objectContaining({ history: expect.any(CircularBuffer), }), - elu: { - idle: { - history: expect.any(CircularBuffer), - }, - active: { - history: expect.any(CircularBuffer), - }, - }, }) expect(workerNode.usage.tasks.executed).toBeGreaterThanOrEqual(0) expect(workerNode.usage.tasks.executed).toBeLessThanOrEqual( @@ -851,8 +850,8 @@ describe('Selection strategies test suite', () => { { workerChoiceStrategy } ) expect(pool.workerChoiceStrategiesContext.getPolicy()).toStrictEqual({ - dynamicWorkerUsage: false, dynamicWorkerReady: true, + dynamicWorkerUsage: false, }) await pool.destroy() pool = new DynamicThreadPool( @@ -862,8 +861,8 @@ describe('Selection strategies test suite', () => { { workerChoiceStrategy } ) expect(pool.workerChoiceStrategiesContext.getPolicy()).toStrictEqual({ - dynamicWorkerUsage: false, dynamicWorkerReady: true, + dynamicWorkerUsage: false, }) // We need to clean up the resources after our test await pool.destroy() @@ -879,18 +878,18 @@ describe('Selection strategies test suite', () => { expect( pool.workerChoiceStrategiesContext.getTaskStatisticsRequirements() ).toStrictEqual({ - runTime: { - aggregate: false, + elu: { + aggregate: true, average: false, median: false, }, - waitTime: { + runTime: { aggregate: false, average: false, median: false, }, - elu: { - aggregate: true, + waitTime: { + aggregate: false, average: false, median: false, }, @@ -905,18 +904,18 @@ describe('Selection strategies test suite', () => { expect( pool.workerChoiceStrategiesContext.getTaskStatisticsRequirements() ).toStrictEqual({ - runTime: { - aggregate: false, + elu: { + aggregate: true, average: false, median: false, }, - waitTime: { + runTime: { aggregate: false, average: false, median: false, }, - elu: { - aggregate: true, + waitTime: { + aggregate: false, average: false, median: false, }, @@ -940,29 +939,29 @@ describe('Selection strategies test suite', () => { await Promise.all(promises) for (const workerNode of pool.workerNodes) { expect(workerNode.usage).toStrictEqual({ + elu: expect.objectContaining({ + active: expect.objectContaining({ + history: expect.any(CircularBuffer), + }), + idle: expect.objectContaining({ + history: expect.any(CircularBuffer), + }), + }), + runTime: { + history: expect.any(CircularBuffer), + }, tasks: { executed: expect.any(Number), executing: 0, - queued: 0, + failed: 0, maxQueued: 0, + queued: 0, sequentiallyStolen: 0, stolen: 0, - failed: 0, - }, - runTime: { - history: expect.any(CircularBuffer), }, waitTime: { history: expect.any(CircularBuffer), }, - elu: expect.objectContaining({ - idle: expect.objectContaining({ - history: expect.any(CircularBuffer), - }), - active: expect.objectContaining({ - history: expect.any(CircularBuffer), - }), - }), }) expect(workerNode.usage.tasks.executed).toBeGreaterThanOrEqual(0) expect(workerNode.usage.tasks.executed).toBeLessThanOrEqual( @@ -1015,29 +1014,29 @@ describe('Selection strategies test suite', () => { await Promise.all(promises) for (const workerNode of pool.workerNodes) { expect(workerNode.usage).toStrictEqual({ + elu: expect.objectContaining({ + active: expect.objectContaining({ + history: expect.any(CircularBuffer), + }), + idle: expect.objectContaining({ + history: expect.any(CircularBuffer), + }), + }), + runTime: { + history: expect.any(CircularBuffer), + }, tasks: { executed: expect.any(Number), executing: 0, - queued: 0, + failed: 0, maxQueued: 0, + queued: 0, sequentiallyStolen: 0, stolen: 0, - failed: 0, - }, - runTime: { - history: expect.any(CircularBuffer), }, waitTime: { history: expect.any(CircularBuffer), }, - elu: expect.objectContaining({ - idle: expect.objectContaining({ - history: expect.any(CircularBuffer), - }), - active: expect.objectContaining({ - history: expect.any(CircularBuffer), - }), - }), }) expect(workerNode.usage.tasks.executed).toBeGreaterThanOrEqual(0) expect(workerNode.usage.tasks.executed).toBeLessThanOrEqual( @@ -1082,8 +1081,8 @@ describe('Selection strategies test suite', () => { { workerChoiceStrategy } ) expect(pool.workerChoiceStrategiesContext.getPolicy()).toStrictEqual({ - dynamicWorkerUsage: false, dynamicWorkerReady: true, + dynamicWorkerUsage: false, }) await pool.destroy() pool = new DynamicThreadPool( @@ -1093,8 +1092,8 @@ describe('Selection strategies test suite', () => { { workerChoiceStrategy } ) expect(pool.workerChoiceStrategiesContext.getPolicy()).toStrictEqual({ - dynamicWorkerUsage: false, dynamicWorkerReady: true, + dynamicWorkerUsage: false, }) // We need to clean up the resources after our test await pool.destroy() @@ -1110,17 +1109,17 @@ describe('Selection strategies test suite', () => { expect( pool.workerChoiceStrategiesContext.getTaskStatisticsRequirements() ).toStrictEqual({ - runTime: { + elu: { aggregate: true, average: true, median: false, }, - waitTime: { + runTime: { aggregate: true, average: true, median: false, }, - elu: { + waitTime: { aggregate: true, average: true, median: false, @@ -1136,17 +1135,17 @@ describe('Selection strategies test suite', () => { expect( pool.workerChoiceStrategiesContext.getTaskStatisticsRequirements() ).toStrictEqual({ - runTime: { + elu: { aggregate: true, average: true, median: false, }, - waitTime: { + runTime: { aggregate: true, average: true, median: false, }, - elu: { + waitTime: { aggregate: true, average: true, median: false, @@ -1171,29 +1170,29 @@ describe('Selection strategies test suite', () => { await Promise.all(promises) for (const workerNode of pool.workerNodes) { expect(workerNode.usage).toStrictEqual({ + elu: expect.objectContaining({ + active: expect.objectContaining({ + history: expect.any(CircularBuffer), + }), + idle: expect.objectContaining({ + history: expect.any(CircularBuffer), + }), + }), + runTime: expect.objectContaining({ + history: expect.any(CircularBuffer), + }), tasks: { executed: expect.any(Number), executing: 0, - queued: 0, + failed: 0, maxQueued: 0, + queued: 0, sequentiallyStolen: 0, stolen: 0, - failed: 0, }, - runTime: expect.objectContaining({ - history: expect.any(CircularBuffer), - }), waitTime: expect.objectContaining({ history: expect.any(CircularBuffer), }), - elu: expect.objectContaining({ - idle: expect.objectContaining({ - history: expect.any(CircularBuffer), - }), - active: expect.objectContaining({ - history: expect.any(CircularBuffer), - }), - }), }) expect(workerNode.usage.tasks.executed).toBeGreaterThanOrEqual(0) expect(workerNode.usage.tasks.executed).toBeLessThanOrEqual( @@ -1267,29 +1266,29 @@ describe('Selection strategies test suite', () => { await Promise.all(promises) for (const workerNode of pool.workerNodes) { expect(workerNode.usage).toStrictEqual({ + elu: expect.objectContaining({ + active: expect.objectContaining({ + history: expect.any(CircularBuffer), + }), + idle: expect.objectContaining({ + history: expect.any(CircularBuffer), + }), + }), + runTime: expect.objectContaining({ + history: expect.any(CircularBuffer), + }), tasks: { executed: expect.any(Number), executing: 0, - queued: 0, + failed: 0, maxQueued: 0, + queued: 0, sequentiallyStolen: 0, stolen: 0, - failed: 0, }, - runTime: expect.objectContaining({ - history: expect.any(CircularBuffer), - }), waitTime: expect.objectContaining({ history: expect.any(CircularBuffer), }), - elu: expect.objectContaining({ - idle: expect.objectContaining({ - history: expect.any(CircularBuffer), - }), - active: expect.objectContaining({ - history: expect.any(CircularBuffer), - }), - }), }) expect(workerNode.usage.tasks.executed).toBeGreaterThanOrEqual(0) expect(workerNode.usage.tasks.executed).toBeLessThanOrEqual( @@ -1368,29 +1367,29 @@ describe('Selection strategies test suite', () => { await Promise.all(promises) for (const workerNode of pool.workerNodes) { expect(workerNode.usage).toStrictEqual({ + elu: expect.objectContaining({ + active: expect.objectContaining({ + history: expect.any(CircularBuffer), + }), + idle: expect.objectContaining({ + history: expect.any(CircularBuffer), + }), + }), + runTime: expect.objectContaining({ + history: expect.any(CircularBuffer), + }), tasks: { executed: expect.any(Number), executing: 0, - queued: 0, + failed: 0, maxQueued: 0, + queued: 0, sequentiallyStolen: 0, stolen: 0, - failed: 0, }, - runTime: expect.objectContaining({ - history: expect.any(CircularBuffer), - }), waitTime: expect.objectContaining({ history: expect.any(CircularBuffer), }), - elu: expect.objectContaining({ - idle: expect.objectContaining({ - history: expect.any(CircularBuffer), - }), - active: expect.objectContaining({ - history: expect.any(CircularBuffer), - }), - }), }) expect(workerNode.usage.tasks.executed).toBeGreaterThanOrEqual(0) expect(workerNode.usage.tasks.executed).toBeLessThanOrEqual( @@ -1490,8 +1489,8 @@ describe('Selection strategies test suite', () => { { workerChoiceStrategy } ) expect(pool.workerChoiceStrategiesContext.getPolicy()).toStrictEqual({ - dynamicWorkerUsage: false, dynamicWorkerReady: true, + dynamicWorkerUsage: false, }) await pool.destroy() pool = new DynamicThreadPool( @@ -1501,8 +1500,8 @@ describe('Selection strategies test suite', () => { { workerChoiceStrategy } ) expect(pool.workerChoiceStrategiesContext.getPolicy()).toStrictEqual({ - dynamicWorkerUsage: false, dynamicWorkerReady: true, + dynamicWorkerUsage: false, }) // We need to clean up the resources after our test await pool.destroy() @@ -1518,6 +1517,11 @@ describe('Selection strategies test suite', () => { expect( pool.workerChoiceStrategiesContext.getTaskStatisticsRequirements() ).toStrictEqual({ + elu: { + aggregate: false, + average: false, + median: false, + }, runTime: { aggregate: true, average: true, @@ -1528,11 +1532,6 @@ describe('Selection strategies test suite', () => { average: true, median: false, }, - elu: { - aggregate: false, - average: false, - median: false, - }, }) await pool.destroy() pool = new DynamicThreadPool( @@ -1544,6 +1543,11 @@ describe('Selection strategies test suite', () => { expect( pool.workerChoiceStrategiesContext.getTaskStatisticsRequirements() ).toStrictEqual({ + elu: { + aggregate: false, + average: false, + median: false, + }, runTime: { aggregate: true, average: true, @@ -1554,11 +1558,6 @@ describe('Selection strategies test suite', () => { average: true, median: false, }, - elu: { - aggregate: false, - average: false, - median: false, - }, }) // We need to clean up the resources after our test await pool.destroy() @@ -1579,29 +1578,29 @@ describe('Selection strategies test suite', () => { await Promise.all(promises) for (const workerNode of pool.workerNodes) { expect(workerNode.usage).toStrictEqual({ + elu: { + active: { + history: expect.any(CircularBuffer), + }, + idle: { + history: expect.any(CircularBuffer), + }, + }, + runTime: expect.objectContaining({ + history: expect.any(CircularBuffer), + }), tasks: { executed: expect.any(Number), executing: 0, - queued: 0, + failed: 0, maxQueued: 0, + queued: 0, sequentiallyStolen: 0, stolen: 0, - failed: 0, }, - runTime: expect.objectContaining({ - history: expect.any(CircularBuffer), - }), waitTime: expect.objectContaining({ history: expect.any(CircularBuffer), }), - elu: { - idle: { - history: expect.any(CircularBuffer), - }, - active: { - history: expect.any(CircularBuffer), - }, - }, }) expect(workerNode.usage.tasks.executed).toBeGreaterThanOrEqual(0) expect(workerNode.usage.tasks.executed).toBeLessThanOrEqual( @@ -1663,29 +1662,29 @@ describe('Selection strategies test suite', () => { await Promise.all(promises) for (const workerNode of pool.workerNodes) { expect(workerNode.usage).toStrictEqual({ + elu: { + active: { + history: expect.any(CircularBuffer), + }, + idle: { + history: expect.any(CircularBuffer), + }, + }, + runTime: expect.objectContaining({ + history: expect.any(CircularBuffer), + }), tasks: { executed: expect.any(Number), executing: 0, - queued: 0, + failed: 0, maxQueued: 0, + queued: 0, sequentiallyStolen: 0, stolen: 0, - failed: 0, }, - runTime: expect.objectContaining({ - history: expect.any(CircularBuffer), - }), waitTime: expect.objectContaining({ history: expect.any(CircularBuffer), }), - elu: { - idle: { - history: expect.any(CircularBuffer), - }, - active: { - history: expect.any(CircularBuffer), - }, - }, }) expect(workerNode.usage.tasks.executed).toBeGreaterThanOrEqual(0) expect(workerNode.usage.tasks.executed).toBeLessThanOrEqual( @@ -1752,29 +1751,29 @@ describe('Selection strategies test suite', () => { await Promise.all(promises) for (const workerNode of pool.workerNodes) { expect(workerNode.usage).toStrictEqual({ + elu: { + active: { + history: expect.any(CircularBuffer), + }, + idle: { + history: expect.any(CircularBuffer), + }, + }, + runTime: expect.objectContaining({ + history: expect.any(CircularBuffer), + }), tasks: { executed: expect.any(Number), executing: 0, - queued: 0, + failed: 0, maxQueued: 0, + queued: 0, sequentiallyStolen: 0, stolen: 0, - failed: 0, }, - runTime: expect.objectContaining({ - history: expect.any(CircularBuffer), - }), waitTime: expect.objectContaining({ history: expect.any(CircularBuffer), }), - elu: { - idle: { - history: expect.any(CircularBuffer), - }, - active: { - history: expect.any(CircularBuffer), - }, - }, }) expect(workerNode.usage.tasks.executed).toBeGreaterThanOrEqual(0) expect(workerNode.usage.tasks.executed).toBeLessThanOrEqual( @@ -1897,8 +1896,8 @@ describe('Selection strategies test suite', () => { { workerChoiceStrategy } ) expect(pool.workerChoiceStrategiesContext.getPolicy()).toStrictEqual({ - dynamicWorkerUsage: false, dynamicWorkerReady: true, + dynamicWorkerUsage: false, }) await pool.destroy() pool = new DynamicThreadPool( @@ -1908,8 +1907,8 @@ describe('Selection strategies test suite', () => { { workerChoiceStrategy } ) expect(pool.workerChoiceStrategiesContext.getPolicy()).toStrictEqual({ - dynamicWorkerUsage: false, dynamicWorkerReady: true, + dynamicWorkerUsage: false, }) // We need to clean up the resources after our test await pool.destroy() @@ -1926,6 +1925,11 @@ describe('Selection strategies test suite', () => { expect( pool.workerChoiceStrategiesContext.getTaskStatisticsRequirements() ).toStrictEqual({ + elu: { + aggregate: false, + average: false, + median: false, + }, runTime: { aggregate: true, average: true, @@ -1936,11 +1940,6 @@ describe('Selection strategies test suite', () => { average: true, median: false, }, - elu: { - aggregate: false, - average: false, - median: false, - }, }) await pool.destroy() pool = new DynamicThreadPool( @@ -1952,6 +1951,11 @@ describe('Selection strategies test suite', () => { expect( pool.workerChoiceStrategiesContext.getTaskStatisticsRequirements() ).toStrictEqual({ + elu: { + aggregate: false, + average: false, + median: false, + }, runTime: { aggregate: true, average: true, @@ -1962,11 +1966,6 @@ describe('Selection strategies test suite', () => { average: true, median: false, }, - elu: { - aggregate: false, - average: false, - median: false, - }, }) // We need to clean up the resources after our test await pool.destroy() @@ -1990,29 +1989,29 @@ describe('Selection strategies test suite', () => { await Promise.all(promises) for (const workerNode of pool.workerNodes) { expect(workerNode.usage).toStrictEqual({ + elu: { + active: { + history: expect.any(CircularBuffer), + }, + idle: { + history: expect.any(CircularBuffer), + }, + }, + runTime: expect.objectContaining({ + history: expect.any(CircularBuffer), + }), tasks: { executed: expect.any(Number), executing: 0, - queued: 0, + failed: 0, maxQueued: 0, + queued: 0, sequentiallyStolen: 0, stolen: 0, - failed: 0, }, - runTime: expect.objectContaining({ - history: expect.any(CircularBuffer), - }), waitTime: expect.objectContaining({ history: expect.any(CircularBuffer), }), - elu: { - idle: { - history: expect.any(CircularBuffer), - }, - active: { - history: expect.any(CircularBuffer), - }, - }, }) expect(workerNode.usage.tasks.executed).toBeGreaterThanOrEqual(0) expect(workerNode.usage.tasks.executed).toBeLessThanOrEqual( @@ -2094,29 +2093,29 @@ describe('Selection strategies test suite', () => { await Promise.all(promises) for (const workerNode of pool.workerNodes) { expect(workerNode.usage).toStrictEqual({ + elu: { + active: { + history: expect.any(CircularBuffer), + }, + idle: { + history: expect.any(CircularBuffer), + }, + }, + runTime: expect.objectContaining({ + history: expect.any(CircularBuffer), + }), tasks: { executed: expect.any(Number), executing: 0, - queued: 0, + failed: 0, maxQueued: 0, + queued: 0, sequentiallyStolen: 0, stolen: 0, - failed: 0, }, - runTime: expect.objectContaining({ - history: expect.any(CircularBuffer), - }), waitTime: expect.objectContaining({ history: expect.any(CircularBuffer), }), - elu: { - idle: { - history: expect.any(CircularBuffer), - }, - active: { - history: expect.any(CircularBuffer), - }, - }, }) expect(workerNode.usage.tasks.executed).toBeGreaterThanOrEqual(0) expect(workerNode.usage.tasks.executed).toBeLessThanOrEqual( diff --git a/tests/pools/selection-strategies/weighted-round-robin-worker-choice-strategy.test.mjs b/tests/pools/selection-strategies/weighted-round-robin-worker-choice-strategy.test.mjs index fae2aecb..18a67bd7 100644 --- a/tests/pools/selection-strategies/weighted-round-robin-worker-choice-strategy.test.mjs +++ b/tests/pools/selection-strategies/weighted-round-robin-worker-choice-strategy.test.mjs @@ -1,6 +1,5 @@ -import { randomInt } from 'node:crypto' - import { expect } from 'expect' +import { randomInt } from 'node:crypto' import { FixedThreadPool } from '../../../lib/index.cjs' import { InterleavedWeightedRoundRobinWorkerChoiceStrategy } from '../../../lib/pools/selection-strategies/interleaved-weighted-round-robin-worker-choice-strategy.cjs' diff --git a/tests/pools/selection-strategies/worker-choice-strategies-context.test.mjs b/tests/pools/selection-strategies/worker-choice-strategies-context.test.mjs index 427103a4..c653d9f5 100644 --- a/tests/pools/selection-strategies/worker-choice-strategies-context.test.mjs +++ b/tests/pools/selection-strategies/worker-choice-strategies-context.test.mjs @@ -18,7 +18,7 @@ import { WorkerChoiceStrategiesContext } from '../../../lib/pools/selection-stra describe('Worker choice strategies context test suite', () => { const min = 1 const max = 3 - let fixedPool, dynamicPool + let dynamicPool, fixedPool before('Create pools', () => { fixedPool = new FixedThreadPool( diff --git a/tests/pools/thread/dynamic.test.mjs b/tests/pools/thread/dynamic.test.mjs index ab2d7f9c..1c626d61 100644 --- a/tests/pools/thread/dynamic.test.mjs +++ b/tests/pools/thread/dynamic.test.mjs @@ -99,8 +99,8 @@ describe('Dynamic thread pool test suite', () => { './tests/worker-files/thread/longRunningWorkerHardBehavior.mjs', { errorHandler: e => console.error(e), - onlineHandler: () => console.info('long executing worker is online'), exitHandler: () => console.info('long executing worker exited'), + onlineHandler: () => console.info('long executing worker is online'), } ) expect(longRunningPool.workerNodes.length).toBe(min) @@ -127,8 +127,8 @@ describe('Dynamic thread pool test suite', () => { './tests/worker-files/thread/longRunningWorkerSoftBehavior.mjs', { errorHandler: e => console.error(e), - onlineHandler: () => console.info('long executing worker is online'), exitHandler: () => console.info('long executing worker exited'), + onlineHandler: () => console.info('long executing worker is online'), } ) expect(longRunningPool.workerNodes.length).toBe(min) diff --git a/tests/pools/thread/fixed.test.mjs b/tests/pools/thread/fixed.test.mjs index 15698ae7..78ef51ff 100644 --- a/tests/pools/thread/fixed.test.mjs +++ b/tests/pools/thread/fixed.test.mjs @@ -8,7 +8,7 @@ import { waitPoolEvents, waitWorkerEvents } from '../../test-utils.cjs' describe('Fixed thread pool test suite', () => { const numberOfThreads = 6 const tasksConcurrency = 2 - let pool, queuePool, emptyPool, echoPool, errorPool, asyncErrorPool, asyncPool + let asyncErrorPool, asyncPool, echoPool, emptyPool, errorPool, pool, queuePool before('Create pools', () => { pool = new FixedThreadPool( @@ -23,10 +23,10 @@ describe('Fixed thread pool test suite', () => { './tests/worker-files/thread/testWorker.mjs', { enableTasksQueue: true, + errorHandler: e => console.error(e), tasksQueueOptions: { concurrency: tasksConcurrency, }, - errorHandler: e => console.error(e), } ) emptyPool = new FixedThreadPool( @@ -240,9 +240,9 @@ describe('Fixed thread pool test suite', () => { expect(typeof inError.message === 'string').toBe(true) expect(inError.message).toBe('Error Message from ThreadWorker') expect(taskError).toStrictEqual({ - name: DEFAULT_TASK_NAME, - message: new Error('Error Message from ThreadWorker'), data, + message: new Error('Error Message from ThreadWorker'), + name: DEFAULT_TASK_NAME, }) expect( errorPool.workerNodes.some( @@ -273,9 +273,9 @@ describe('Fixed thread pool test suite', () => { expect(typeof inError.message === 'string').toBe(true) expect(inError.message).toBe('Error Message from ThreadWorker:async') expect(taskError).toStrictEqual({ - name: DEFAULT_TASK_NAME, - message: new Error('Error Message from ThreadWorker:async'), data, + message: new Error('Error Message from ThreadWorker:async'), + name: DEFAULT_TASK_NAME, }) expect( asyncErrorPool.workerNodes.some( diff --git a/tests/pools/utils.test.mjs b/tests/pools/utils.test.mjs index b2265b03..36862a2a 100644 --- a/tests/pools/utils.test.mjs +++ b/tests/pools/utils.test.mjs @@ -1,8 +1,7 @@ +import { expect } from 'expect' import cluster, { Worker as ClusterWorker } from 'node:cluster' import { Worker as ThreadWorker } from 'node:worker_threads' -import { expect } from 'expect' - import { CircularBuffer } from '../../lib/circular-buffer.cjs' import { WorkerTypes } from '../../lib/index.cjs' import { @@ -29,10 +28,10 @@ describe('Pool utils test suite', () => { expect(getDefaultTasksQueueOptions(poolMaxSize)).toStrictEqual({ concurrency: 1, size: Math.pow(poolMaxSize, 2), - taskStealing: true, + tasksFinishedTimeout: 2000, tasksStealingOnBackPressure: true, tasksStealingRatio: 0.6, - tasksFinishedTimeout: 2000, + taskStealing: true, }) }) @@ -67,9 +66,9 @@ describe('Pool utils test suite', () => { ) expect(measurementStatistics).toMatchObject({ aggregate: 0.031, + average: 0.0010000000474974513, maximum: 0.02, minimum: 0.001, - average: 0.0010000000474974513, }) updateMeasurementStatistics( measurementStatistics, @@ -78,9 +77,9 @@ describe('Pool utils test suite', () => { ) expect(measurementStatistics).toMatchObject({ aggregate: 0.034, + average: 0.0020000000367872417, maximum: 0.02, minimum: 0.001, - average: 0.0020000000367872417, }) updateMeasurementStatistics( measurementStatistics, @@ -90,8 +89,8 @@ describe('Pool utils test suite', () => { expect(measurementStatistics).toMatchObject({ aggregate: 0.04, maximum: 0.02, - minimum: 0.001, median: 0.003000000026077032, + minimum: 0.001, }) updateMeasurementStatistics( measurementStatistics, @@ -100,9 +99,9 @@ describe('Pool utils test suite', () => { ) expect(measurementStatistics).toMatchObject({ aggregate: 0.05, + average: 0.004999999975552782, maximum: 0.02, minimum: 0.001, - average: 0.004999999975552782, }) }) diff --git a/tests/pools/worker-node.test.mjs b/tests/pools/worker-node.test.mjs index 69f1e15a..8698680f 100644 --- a/tests/pools/worker-node.test.mjs +++ b/tests/pools/worker-node.test.mjs @@ -1,8 +1,7 @@ +import { expect } from 'expect' import { Worker as ClusterWorker } from 'node:cluster' import { MessageChannel, Worker as ThreadWorker } from 'node:worker_threads' -import { expect } from 'expect' - import { CircularBuffer } from '../../lib/circular-buffer.cjs' import { WorkerTypes } from '../../lib/index.cjs' import { MeasurementHistorySize } from '../../lib/pools/worker.cjs' @@ -11,7 +10,7 @@ import { PriorityQueue } from '../../lib/queues/priority-queue.cjs' import { DEFAULT_TASK_NAME } from '../../lib/utils.cjs' describe('Worker node test suite', () => { - let threadWorkerNode, clusterWorkerNode + let clusterWorkerNode, threadWorkerNode before('Create worker nodes', () => { threadWorkerNode = new WorkerNode( @@ -236,40 +235,40 @@ describe('Worker node test suite', () => { expect(threadWorkerNode).toBeInstanceOf(WorkerNode) expect(threadWorkerNode.worker).toBeInstanceOf(ThreadWorker) expect(threadWorkerNode.info).toStrictEqual({ - id: threadWorkerNode.worker.threadId, - type: WorkerTypes.thread, + backPressure: false, + backPressureStealing: false, + continuousStealing: false, dynamic: false, + id: threadWorkerNode.worker.threadId, ready: false, stealing: false, stolen: false, - continuousStealing: false, - backPressureStealing: false, - backPressure: false, + type: WorkerTypes.thread, }) expect(threadWorkerNode.usage).toStrictEqual({ + elu: { + active: { + history: expect.any(CircularBuffer), + }, + idle: { + history: expect.any(CircularBuffer), + }, + }, + runTime: { + history: expect.any(CircularBuffer), + }, tasks: { executed: 0, executing: 0, - queued: 0, + failed: 0, maxQueued: 0, + queued: 0, sequentiallyStolen: 0, stolen: 0, - failed: 0, - }, - runTime: { - history: expect.any(CircularBuffer), }, waitTime: { history: expect.any(CircularBuffer), }, - elu: { - idle: { - history: expect.any(CircularBuffer), - }, - active: { - history: expect.any(CircularBuffer), - }, - }, }) expect(threadWorkerNode.usage.runTime.history.items.length).toBe( MeasurementHistorySize @@ -298,40 +297,40 @@ describe('Worker node test suite', () => { expect(clusterWorkerNode).toBeInstanceOf(WorkerNode) expect(clusterWorkerNode.worker).toBeInstanceOf(ClusterWorker) expect(clusterWorkerNode.info).toStrictEqual({ - id: clusterWorkerNode.worker.id, - type: WorkerTypes.cluster, + backPressure: false, + backPressureStealing: false, + continuousStealing: false, dynamic: false, + id: clusterWorkerNode.worker.id, ready: false, stealing: false, stolen: false, - continuousStealing: false, - backPressureStealing: false, - backPressure: false, + type: WorkerTypes.cluster, }) expect(clusterWorkerNode.usage).toStrictEqual({ + elu: { + active: { + history: expect.any(CircularBuffer), + }, + idle: { + history: expect.any(CircularBuffer), + }, + }, + runTime: { + history: expect.any(CircularBuffer), + }, tasks: { executed: 0, executing: 0, - queued: 0, + failed: 0, maxQueued: 0, + queued: 0, sequentiallyStolen: 0, stolen: 0, - failed: 0, - }, - runTime: { - history: expect.any(CircularBuffer), }, waitTime: { history: expect.any(CircularBuffer), }, - elu: { - idle: { - history: expect.any(CircularBuffer), - }, - active: { - history: expect.any(CircularBuffer), - }, - }, }) expect(clusterWorkerNode.usage.runTime.history.items.length).toBe( MeasurementHistorySize @@ -385,76 +384,76 @@ describe('Worker node test suite', () => { expect( threadWorkerNode.getTaskFunctionWorkerUsage(DEFAULT_TASK_NAME) ).toStrictEqual({ + elu: { + active: { + history: expect.any(CircularBuffer), + }, + idle: { + history: expect.any(CircularBuffer), + }, + }, + runTime: { + history: expect.any(CircularBuffer), + }, tasks: { executed: 0, executing: 0, + failed: 0, queued: 0, sequentiallyStolen: 0, stolen: 0, - failed: 0, - }, - runTime: { - history: expect.any(CircularBuffer), }, waitTime: { history: expect.any(CircularBuffer), }, + }) + expect(threadWorkerNode.getTaskFunctionWorkerUsage('fn1')).toStrictEqual({ elu: { - idle: { + active: { history: expect.any(CircularBuffer), }, - active: { + idle: { history: expect.any(CircularBuffer), }, }, - }) - expect(threadWorkerNode.getTaskFunctionWorkerUsage('fn1')).toStrictEqual({ + runTime: { + history: expect.any(CircularBuffer), + }, tasks: { executed: 0, executing: 0, + failed: 0, queued: 0, sequentiallyStolen: 0, stolen: 0, - failed: 0, - }, - runTime: { - history: expect.any(CircularBuffer), }, waitTime: { history: expect.any(CircularBuffer), }, + }) + expect(threadWorkerNode.getTaskFunctionWorkerUsage('fn2')).toStrictEqual({ elu: { - idle: { + active: { history: expect.any(CircularBuffer), }, - active: { + idle: { history: expect.any(CircularBuffer), }, }, - }) - expect(threadWorkerNode.getTaskFunctionWorkerUsage('fn2')).toStrictEqual({ + runTime: { + history: expect.any(CircularBuffer), + }, tasks: { executed: 0, executing: 0, + failed: 0, queued: 0, sequentiallyStolen: 0, stolen: 0, - failed: 0, - }, - runTime: { - history: expect.any(CircularBuffer), }, waitTime: { history: expect.any(CircularBuffer), }, - elu: { - idle: { - history: expect.any(CircularBuffer), - }, - active: { - history: expect.any(CircularBuffer), - }, - }, }) expect(threadWorkerNode.taskFunctionsUsage.size).toBe(2) }) diff --git a/tests/test-types.cjs b/tests/test-types.cjs index 707bb160..1607b958 100644 --- a/tests/test-types.cjs +++ b/tests/test-types.cjs @@ -1,7 +1,7 @@ const TaskFunctions = { - jsonIntegerSerialization: 'jsonIntegerSerialization', - fibonacci: 'fibonacci', factorial: 'factorial', + fibonacci: 'fibonacci', + jsonIntegerSerialization: 'jsonIntegerSerialization', } module.exports = { TaskFunctions } diff --git a/tests/test-utils.cjs b/tests/test-utils.cjs index 5006df6e..10fa7fa0 100644 --- a/tests/test-utils.cjs +++ b/tests/test-utils.cjs @@ -98,12 +98,12 @@ const factorial = n => { const executeTaskFunction = data => { switch (data.function) { - case TaskFunctions.jsonIntegerSerialization: - return jsonIntegerSerialization(data.n || 100) - case TaskFunctions.fibonacci: - return fibonacci(data.n || 100) case TaskFunctions.factorial: return factorial(data.n || 100) + case TaskFunctions.fibonacci: + return fibonacci(data.n || 100) + case TaskFunctions.jsonIntegerSerialization: + return jsonIntegerSerialization(data.n || 100) default: throw new Error('Unknown worker function') } diff --git a/tests/utils.test.mjs b/tests/utils.test.mjs index 25b4e5ae..4f84d818 100644 --- a/tests/utils.test.mjs +++ b/tests/utils.test.mjs @@ -1,8 +1,7 @@ +import { expect } from 'expect' import { randomInt } from 'node:crypto' import os from 'node:os' -import { expect } from 'expect' - import { KillBehaviors } from '../lib/index.cjs' import { availableParallelism, @@ -182,12 +181,12 @@ describe('Utils test suite', () => { expect(isAsyncFunction(async function () {})).toBe(true) expect(isAsyncFunction(async function named () {})).toBe(true) class TestClass { - testSync () {} - async testAsync () {} - testArrowSync = () => {} testArrowAsync = async () => {} - static testStaticSync () {} + testArrowSync = () => {} static async testStaticAsync () {} + static testStaticSync () {} + async testAsync () {} + testSync () {} } const testClass = new TestClass() expect(isAsyncFunction(testClass.testSync)).toBe(false) diff --git a/tests/worker-files/cluster/testMultipleTaskFunctionsWorker.cjs b/tests/worker-files/cluster/testMultipleTaskFunctionsWorker.cjs index 0f8286a5..6f1ebadc 100644 --- a/tests/worker-files/cluster/testMultipleTaskFunctionsWorker.cjs +++ b/tests/worker-files/cluster/testMultipleTaskFunctionsWorker.cjs @@ -1,16 +1,16 @@ 'use strict' const { ClusterWorker, KillBehaviors } = require('../../../lib/index.cjs') const { - jsonIntegerSerialization, factorial, fibonacci, + jsonIntegerSerialization, } = require('../../test-utils.cjs') module.exports = new ClusterWorker( { - jsonIntegerSerialization: data => jsonIntegerSerialization(data.n), factorial: data => factorial(data.n), fibonacci: data => fibonacci(data.n), + jsonIntegerSerialization: data => jsonIntegerSerialization(data.n), }, { killBehavior: KillBehaviors.HARD, diff --git a/tests/worker-files/cluster/testTaskFunctionObjectsWorker.cjs b/tests/worker-files/cluster/testTaskFunctionObjectsWorker.cjs index 9f4f4b4f..265ea136 100644 --- a/tests/worker-files/cluster/testTaskFunctionObjectsWorker.cjs +++ b/tests/worker-files/cluster/testTaskFunctionObjectsWorker.cjs @@ -1,5 +1,5 @@ 'use strict' -const { KillBehaviors, ClusterWorker } = require('../../../lib/index.cjs') +const { ClusterWorker, KillBehaviors } = require('../../../lib/index.cjs') const { factorial, fibonacci, @@ -8,11 +8,11 @@ const { module.exports = new ClusterWorker( { + factorial: { taskFunction: data => factorial(data.n) }, + fibonacci: { priority: -5, taskFunction: data => fibonacci(data.n) }, jsonIntegerSerialization: { taskFunction: data => jsonIntegerSerialization(data.n), }, - factorial: { taskFunction: data => factorial(data.n) }, - fibonacci: { taskFunction: data => fibonacci(data.n), priority: -5 }, }, { killBehavior: KillBehaviors.HARD, diff --git a/tests/worker-files/cluster/testWorker.cjs b/tests/worker-files/cluster/testWorker.cjs index 38459844..5af94942 100644 --- a/tests/worker-files/cluster/testWorker.cjs +++ b/tests/worker-files/cluster/testWorker.cjs @@ -1,7 +1,7 @@ 'use strict' const { ClusterWorker, KillBehaviors } = require('../../../lib/index.cjs') -const { executeTaskFunction } = require('../../test-utils.cjs') const { TaskFunctions } = require('../../test-types.cjs') +const { executeTaskFunction } = require('../../test-utils.cjs') /** * diff --git a/tests/worker-files/thread/testMultipleTaskFunctionsWorker.mjs b/tests/worker-files/thread/testMultipleTaskFunctionsWorker.mjs index 7928ab02..e9409728 100644 --- a/tests/worker-files/thread/testMultipleTaskFunctionsWorker.mjs +++ b/tests/worker-files/thread/testMultipleTaskFunctionsWorker.mjs @@ -7,9 +7,9 @@ import { export default new ThreadWorker( { - jsonIntegerSerialization: data => jsonIntegerSerialization(data.n), factorial: data => factorial(data.n), fibonacci: data => fibonacci(data.n), + jsonIntegerSerialization: data => jsonIntegerSerialization(data.n), }, { killBehavior: KillBehaviors.HARD, diff --git a/tests/worker-files/thread/testTaskFunctionObjectsWorker.mjs b/tests/worker-files/thread/testTaskFunctionObjectsWorker.mjs index 35cd6daf..566f436b 100644 --- a/tests/worker-files/thread/testTaskFunctionObjectsWorker.mjs +++ b/tests/worker-files/thread/testTaskFunctionObjectsWorker.mjs @@ -7,11 +7,11 @@ import { export default new ThreadWorker( { + factorial: { taskFunction: data => factorial(data.n) }, + fibonacci: { priority: -5, taskFunction: data => fibonacci(data.n) }, jsonIntegerSerialization: { taskFunction: data => jsonIntegerSerialization(data.n), }, - factorial: { taskFunction: data => factorial(data.n) }, - fibonacci: { taskFunction: data => fibonacci(data.n), priority: -5 }, }, { killBehavior: KillBehaviors.HARD, diff --git a/tests/worker/abstract-worker.test.mjs b/tests/worker/abstract-worker.test.mjs index ed4fcbdb..669f9b3f 100644 --- a/tests/worker/abstract-worker.test.mjs +++ b/tests/worker/abstract-worker.test.mjs @@ -25,8 +25,8 @@ describe('Abstract worker test suite', () => { const worker = new ThreadWorker(() => {}) expect(worker.opts).toStrictEqual({ killBehavior: KillBehaviors.SOFT, - maxInactiveTime: 60000, killHandler: EMPTY_FUNCTION, + maxInactiveTime: 60000, }) }) @@ -70,13 +70,13 @@ describe('Abstract worker test suite', () => { } const worker = new ClusterWorker(() => {}, { killBehavior: KillBehaviors.HARD, - maxInactiveTime: 6000, killHandler, + maxInactiveTime: 6000, }) expect(worker.opts).toStrictEqual({ killBehavior: KillBehaviors.HARD, - maxInactiveTime: 6000, killHandler, + maxInactiveTime: 6000, }) }) @@ -173,18 +173,18 @@ describe('Abstract worker test suite', () => { ) ) expect( - () => new ThreadWorker({ fn1: { taskFunction: fn1, priority: '' } }) + () => new ThreadWorker({ fn1: { priority: '', taskFunction: fn1 } }) ).toThrow(new TypeError("Invalid property 'priority': ''")) expect( - () => new ThreadWorker({ fn1: { taskFunction: fn1, priority: -21 } }) + () => new ThreadWorker({ fn1: { priority: -21, taskFunction: fn1 } }) ).toThrow(new RangeError("Property 'priority' must be between -20 and 19")) expect( - () => new ThreadWorker({ fn1: { taskFunction: fn1, priority: 20 } }) + () => new ThreadWorker({ fn1: { priority: 20, taskFunction: fn1 } }) ).toThrow(new RangeError("Property 'priority' must be between -20 and 19")) expect( () => new ThreadWorker({ - fn1: { taskFunction: fn1, strategy: 'invalidStrategy' }, + fn1: { strategy: 'invalidStrategy', taskFunction: fn1 }, }) ).toThrow(new Error("Invalid worker choice strategy 'invalidStrategy'")) }) @@ -214,17 +214,17 @@ describe('Abstract worker test suite', () => { it('Verify that taskFunctions parameter with multiple task functions object is taken', () => { const fn1Obj = { + priority: 5, taskFunction: () => { return 1 }, - priority: 5, } const fn2Obj = { + priority: 6, + strategy: WorkerChoiceStrategies.LESS_BUSY, taskFunction: () => { return 2 }, - priority: 6, - strategy: WorkerChoiceStrategies.LESS_BUSY, } const worker = new ThreadWorker({ fn1: fn1Obj, @@ -264,12 +264,12 @@ describe('Abstract worker test suite', () => { } const worker = new ClusterWorker({ fn1, fn2 }) expect(worker.hasTaskFunction(0)).toStrictEqual({ - status: false, error: new TypeError('name parameter is not a string'), + status: false, }) expect(worker.hasTaskFunction('')).toStrictEqual({ - status: false, error: new TypeError('name parameter is an empty string'), + status: false, }) expect(worker.hasTaskFunction(DEFAULT_TASK_NAME)).toStrictEqual({ status: true, @@ -291,57 +291,57 @@ describe('Abstract worker test suite', () => { } const worker = new ThreadWorker(fn1) expect(worker.addTaskFunction(0, fn1)).toStrictEqual({ - status: false, error: new TypeError('name parameter is not a string'), + status: false, }) expect(worker.addTaskFunction('', fn1)).toStrictEqual({ - status: false, error: new TypeError('name parameter is an empty string'), + status: false, }) expect(worker.addTaskFunction('fn2', 0)).toStrictEqual({ - status: false, error: new TypeError( "taskFunction object 'taskFunction' property 'undefined' is not a function" ), + status: false, }) expect(worker.addTaskFunction('fn3', '')).toStrictEqual({ - status: false, error: new TypeError( "taskFunction object 'taskFunction' property 'undefined' is not a function" ), + status: false, }) expect(worker.addTaskFunction('fn2', { taskFunction: 0 })).toStrictEqual({ - status: false, error: new TypeError( "taskFunction object 'taskFunction' property '0' is not a function" ), + status: false, }) expect(worker.addTaskFunction('fn3', { taskFunction: '' })).toStrictEqual({ - status: false, error: new TypeError( "taskFunction object 'taskFunction' property '' is not a function" ), + status: false, }) expect( - worker.addTaskFunction('fn2', { taskFunction: () => {}, priority: -21 }) + worker.addTaskFunction('fn2', { priority: -21, taskFunction: () => {} }) ).toStrictEqual({ - status: false, error: new RangeError("Property 'priority' must be between -20 and 19"), + status: false, }) expect( - worker.addTaskFunction('fn3', { taskFunction: () => {}, priority: 20 }) + worker.addTaskFunction('fn3', { priority: 20, taskFunction: () => {} }) ).toStrictEqual({ - status: false, error: new RangeError("Property 'priority' must be between -20 and 19"), + status: false, }) expect( worker.addTaskFunction('fn2', { - taskFunction: () => {}, strategy: 'invalidStrategy', + taskFunction: () => {}, }) ).toStrictEqual({ - status: false, error: new Error("Invalid worker choice strategy 'invalidStrategy'"), + status: false, }) expect(worker.taskFunctions.get(DEFAULT_TASK_NAME)).toStrictEqual({ taskFunction: expect.any(Function), @@ -354,10 +354,10 @@ describe('Abstract worker test suite', () => { worker.taskFunctions.get('fn1') ) expect(worker.addTaskFunction(DEFAULT_TASK_NAME, fn2)).toStrictEqual({ - status: false, error: new Error( 'Cannot add a task function with the default reserved name' ), + status: false, }) worker.addTaskFunction('fn2', fn2) expect(worker.taskFunctions.get(DEFAULT_TASK_NAME)).toStrictEqual({ @@ -413,12 +413,12 @@ describe('Abstract worker test suite', () => { } const worker = new ThreadWorker({ fn1, fn2 }) expect(worker.setDefaultTaskFunction(0, fn1)).toStrictEqual({ - status: false, error: new TypeError('name parameter is not a string'), + status: false, }) expect(worker.setDefaultTaskFunction('', fn1)).toStrictEqual({ - status: false, error: new TypeError('name parameter is an empty string'), + status: false, }) expect(worker.taskFunctions.get(DEFAULT_TASK_NAME)).toStrictEqual({ taskFunction: expect.any(Function), @@ -434,16 +434,16 @@ describe('Abstract worker test suite', () => { worker.taskFunctions.get('fn1') ) expect(worker.setDefaultTaskFunction(DEFAULT_TASK_NAME)).toStrictEqual({ - status: false, error: new Error( 'Cannot set the default task function reserved name as the default task function' ), + status: false, }) expect(worker.setDefaultTaskFunction('fn3')).toStrictEqual({ - status: false, error: new Error( 'Cannot set the default task function to a non-existing task function' ), + status: false, }) worker.setDefaultTaskFunction('fn1') expect(worker.taskFunctions.get(DEFAULT_TASK_NAME)).toStrictEqual( diff --git a/tests/worker/cluster-worker.test.mjs b/tests/worker/cluster-worker.test.mjs index 2ad79113..fcb6564e 100644 --- a/tests/worker/cluster-worker.test.mjs +++ b/tests/worker/cluster-worker.test.mjs @@ -45,12 +45,12 @@ describe('Cluster worker test suite', () => { send: stub().returns(), }) expect(worker.removeTaskFunction(0, fn1)).toStrictEqual({ - status: false, error: new TypeError('name parameter is not a string'), + status: false, }) expect(worker.removeTaskFunction('', fn1)).toStrictEqual({ - status: false, error: new TypeError('name parameter is an empty string'), + status: false, }) expect(worker.taskFunctions.get(DEFAULT_TASK_NAME)).toStrictEqual({ taskFunction: expect.any(Function), @@ -66,16 +66,16 @@ describe('Cluster worker test suite', () => { worker.taskFunctions.get('fn1') ) expect(worker.removeTaskFunction(DEFAULT_TASK_NAME)).toStrictEqual({ - status: false, error: new Error( 'Cannot remove the task function with the default reserved name' ), + status: false, }) expect(worker.removeTaskFunction('fn1')).toStrictEqual({ - status: false, error: new Error( 'Cannot remove the task function used as the default task function' ), + status: false, }) worker.removeTaskFunction('fn2') expect(worker.taskFunctions.get(DEFAULT_TASK_NAME)).toStrictEqual({ diff --git a/tests/worker/thread-worker.test.mjs b/tests/worker/thread-worker.test.mjs index f78451f7..ed9f16ac 100644 --- a/tests/worker/thread-worker.test.mjs +++ b/tests/worker/thread-worker.test.mjs @@ -23,9 +23,9 @@ describe('Thread worker test suite', () => { }) worker.isMain = false worker.port = { + close: stub().returns(), postMessage: stub().returns(), unref: stub().returns(), - close: stub().returns(), } worker.handleKillMessage() expect(worker.port.postMessage.calledOnce).toBe(true) @@ -46,12 +46,12 @@ describe('Thread worker test suite', () => { postMessage: stub().returns(), } expect(worker.removeTaskFunction(0, fn1)).toStrictEqual({ - status: false, error: new TypeError('name parameter is not a string'), + status: false, }) expect(worker.removeTaskFunction('', fn1)).toStrictEqual({ - status: false, error: new TypeError('name parameter is an empty string'), + status: false, }) expect(worker.taskFunctions.get(DEFAULT_TASK_NAME)).toStrictEqual({ taskFunction: expect.any(Function), @@ -67,16 +67,16 @@ describe('Thread worker test suite', () => { worker.taskFunctions.get('fn1') ) expect(worker.removeTaskFunction(DEFAULT_TASK_NAME)).toStrictEqual({ - status: false, error: new Error( 'Cannot remove the task function with the default reserved name' ), + status: false, }) expect(worker.removeTaskFunction('fn1')).toStrictEqual({ - status: false, error: new Error( 'Cannot remove the task function used as the default task function' ), + status: false, }) worker.removeTaskFunction('fn2') expect(worker.taskFunctions.get(DEFAULT_TASK_NAME)).toStrictEqual({ diff --git a/typedoc.mjs b/typedoc.mjs index d139d4e8..328fb0c8 100644 --- a/typedoc.mjs +++ b/typedoc.mjs @@ -24,8 +24,8 @@ try { ) } rmSync(join(dirname(fileURLToPath(import.meta.url)), 'tmp'), { - recursive: true, force: true, + recursive: true, }) } catch (e) { console.error(e)