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'],
}
const TaskFunctions = {
- jsonIntegerSerialization: 'jsonIntegerSerialization',
- fibonacci: 'fibonacci',
factorial: 'factorial',
+ fibonacci: 'fibonacci',
+ jsonIntegerSerialization: 'jsonIntegerSerialization',
readWriteFiles: 'readWriteFiles',
}
rmSync,
writeFileSync,
} = require('node:fs')
+
const { TaskFunctions } = require('./benchmarks-types.cjs')
const jsonIntegerSerialization = n => {
import { strictEqual } from 'node:assert'
-
import { bench, clear, group, run } from 'tatami-ng'
import {
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),
'./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
}
return {
[name]: {
latency: {
- value: stats?.avg,
lower_value: stats?.min,
upper_value: stats?.max,
+ value: stats?.avg,
},
throughput: {
value: stats?.iter,
switch (
parseArgs({
+ allowPositionals: true,
args: process.argv,
options: {
type: {
- type: 'string',
short: 't',
+ type: 'string',
},
},
strict: true,
- allowPositionals: true,
}).values.type
) {
case 'tatami-ng':
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 || {}
import { randomInt } from 'node:crypto'
-
import { bench, group, run } from 'tatami-ng'
/**
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'
'jsdoc/check-tag-names': [
'warn',
{
- typed: true,
definedTags: ['defaultValue', 'experimental', 'typeParam'],
+ typed: true,
},
],
},
},
+
...plugins['typescript-eslint'].config(
{
extends: [
}
),
{
- plugins: {
- 'simple-import-sort': simpleImportSort,
- },
rules: {
'@cspell/spellchecker': [
'warn',
},
},
],
- 'simple-import-sort/imports': 'error',
- 'simple-import-sort/exports': 'error',
},
},
+ perfectionist.configs['recommended-natural'],
...neostandard({
- ts: true,
globals: {
...globals.mocha,
},
+ ts: true,
}),
{
files: [
{
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
{
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',
},
},
])
'use strict'
const {
+ availableParallelism,
DynamicThreadPool,
PoolEvents,
- availableParallelism,
} = require('poolifier')
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
'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
'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'),
}
)
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/'
import { dirname, extname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
-
import { availableParallelism, DynamicThreadPool } from 'poolifier'
import type { WorkerData, WorkerResponse } from './types.js'
workerFile,
{
enableTasksQueue: true,
- tasksQueueOptions: {
- concurrency: 8,
- },
errorHandler: (e: Error) => {
console.error('Thread worker error:', e)
},
+ tasksQueueOptions: {
+ concurrency: 8,
+ },
}
)
-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 {
class HttpClientWorker extends ThreadWorker<WorkerData, WorkerResponse> {
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.
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(),
}
},
})
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,
})
import { dirname, extname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
-
import { availableParallelism, FixedClusterPool } from 'poolifier'
import type { WorkerData, WorkerResponse } from './types.js'
workerFile,
{
enableEvents: false,
+ errorHandler: (e: Error) => {
+ console.error('Cluster worker error:', e)
+ },
onlineHandler: () => {
pool
.execute({ port: 8080 })
console.error('Express failed to start in cluster worker:', error)
})
},
- errorHandler: (e: Error) => {
- console.error('Cluster worker error:', e)
- },
}
)
}
export interface WorkerResponse {
- status: boolean
port?: number
+ status: boolean
}
import type { WorkerData, WorkerResponse } from './types.js'
class ExpressWorker extends ClusterWorker<WorkerData, WorkerResponse> {
- 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 {
}
}
+ private static server: Server
+
private static readonly startExpress = (
workerData?: WorkerData
): WorkerResponse => {
)
})
return {
- status: true,
port: listenerPort ?? port,
+ status: true,
}
}
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,
})
ClusterWorkerData,
ClusterWorkerResponse
> {
- private static server: Server
private static requestHandlerPool: DynamicThreadPool<
ThreadWorkerData<DataPayload>,
ThreadWorkerResponse<DataPayload>
>
+ 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!
)
})
return {
- status: true,
port: listenerPort ?? port,
+ status: true,
}
}
import { dirname, extname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
-
import { availableParallelism, FixedClusterPool } from 'poolifier'
import type { ClusterWorkerData, ClusterWorkerResponse } from './types.js'
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) {
console.error('Express failed to start in cluster worker:', error)
})
},
- errorHandler: (e: Error) => {
- console.error('Cluster worker error:', e)
- },
}
)
Data extends ThreadWorkerData<DataPayload>,
Response extends ThreadWorkerResponse<DataPayload>
> extends ThreadWorker<Data, Response> {
- private static readonly factorial = (n: number | bigint): bigint => {
+ private static readonly factorial = (n: bigint | number): bigint => {
if (n === 0 || n === 1) {
return 1n
} else {
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 {
-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'
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'
WorkerResponse<BodyPayload>
>(1, availableParallelism(), workerFile, {
enableTasksQueue: true,
- tasksQueueOptions: {
- concurrency: 8,
- },
errorHandler: (e: Error) => {
console.error('Thread worker error:', e)
},
+ tasksQueueOptions: {
+ concurrency: 8,
+ },
})
Data extends WorkerData<BodyPayload>,
Response extends WorkerResponse<BodyPayload>
> extends ThreadWorker<Data, Response> {
- 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 {
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,
})
import { dirname, extname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
-
import { availableParallelism, FixedClusterPool } from 'poolifier'
import type { WorkerData, WorkerResponse } from './types.js'
workerFile,
{
enableEvents: false,
+ errorHandler: (e: Error) => {
+ console.error('Cluster worker error:', e)
+ },
onlineHandler: () => {
pool
.execute({ port: 8080 })
console.error('Fastify failed to start in cluster worker:', error)
})
},
- errorHandler: (e: Error) => {
- console.error('Cluster worker error:', e)
- },
}
)
}
export interface WorkerResponse {
- status: boolean
port?: number
+ status: boolean
}
import type { WorkerData, WorkerResponse } from './types.js'
class FastifyWorker extends ClusterWorker<WorkerData, WorkerResponse> {
- 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 {
}
}
+ private static fastify: FastifyInstance
+
private static readonly startFastify = async (
workerData?: WorkerData
): Promise<WorkerResponse> => {
await FastifyWorker.fastify.listen({ port })
return {
- status: true,
port: (FastifyWorker.fastify.server.address() as AddressInfo).port,
+ status: true,
}
}
-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<ThreadWorkerData, ThreadWorkerResponse>
execute: (
data?: ThreadWorkerData,
name?: string,
transferList?: readonly TransferListItem[]
) => Promise<ThreadWorkerResponse>
+ pool: DynamicThreadPool<ThreadWorkerData, ThreadWorkerResponse>
}
}
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,
})
+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'
) => {
options = {
...{
- minWorkers: 1,
maxWorkers: availableParallelism(),
+ minWorkers: 1,
},
...options,
}
- const { workerFile, minWorkers, maxWorkers, ...poolOptions } = options
+ const { maxWorkers, minWorkers, workerFile, ...poolOptions } = options
const pool = new DynamicThreadPool<ThreadWorkerData, ThreadWorkerResponse>(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
minWorkers!,
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
await FastifyWorker.fastify.listen({ port })
return {
- status: true,
port: (FastifyWorker.fastify.server.address() as AddressInfo).port,
+ status: true,
}
}
import { dirname, extname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
-
import { availableParallelism, FixedClusterPool } from 'poolifier'
import type { ClusterWorkerData, ClusterWorkerResponse } from './types.js'
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) {
console.error('Fastify failed to start in cluster worker:', error)
})
},
- errorHandler: (e: Error) => {
- console.error('Cluster worker error:', e)
- },
}
)
Data extends ThreadWorkerData<DataPayload>,
Response extends ThreadWorkerResponse<DataPayload>
> extends ThreadWorker<Data, Response> {
- private static readonly factorial = (n: number | bigint): bigint => {
+ private static readonly factorial = (n: bigint | number): bigint => {
if (n === 0 || n === 1) {
return 1n
} else {
}
export interface ClusterWorkerResponse {
- status: boolean
port?: number
+ status: boolean
}
export interface DataPayload {
}
export interface FastifyPoolifierOptions extends ThreadPoolOptions {
- workerFile: string
- minWorkers?: number
maxWorkers?: number
+ minWorkers?: number
+ workerFile: string
}
-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<WorkerData, WorkerResponse>
execute: (
data?: WorkerData,
name?: string,
transferList?: readonly TransferListItem[]
) => Promise<WorkerResponse>
+ pool: DynamicThreadPool<WorkerData, WorkerResponse>
}
}
+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'
) => {
options = {
...{
- minWorkers: 1,
maxWorkers: availableParallelism(),
+ minWorkers: 1,
},
...options,
}
- const { workerFile, minWorkers, maxWorkers, ...poolOptions } = options
+ const { maxWorkers, minWorkers, workerFile, ...poolOptions } = options
const pool = new DynamicThreadPool<WorkerData, WorkerResponse>(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
minWorkers!,
+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'
/**
)
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 => {
}
export interface FastifyPoolifierOptions extends ThreadPoolOptions {
- workerFile: string
- minWorkers?: number
maxWorkers?: number
+ minWorkers?: number
+ workerFile: string
}
Data extends WorkerData<BodyPayload>,
Response extends WorkerResponse<BodyPayload>
> extends ThreadWorker<Data, Response> {
- 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 {
import { dirname, extname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
-
import {
availableParallelism,
DynamicThreadPool,
availableParallelism(),
workerFile,
{
- onlineHandler: () => {
- console.info('Worker is online')
- },
errorHandler: (e: Error) => {
console.error(e)
},
+ onlineHandler: () => {
+ console.info('Worker is online')
+ },
}
)
availableParallelism(),
workerFile,
{
- onlineHandler: () => {
- console.info('Worker is online')
- },
errorHandler: (e: Error) => {
console.error(e)
},
+ onlineHandler: () => {
+ console.info('Worker is online')
+ },
}
)
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" <foo@domain.tld>',
- to,
+ html: '<b>Hello world?</b>',
subject: 'Hello',
text: 'Hello world?',
- html: '<b>Hello world?</b>',
+ to,
+ },
+ smtpTransport: {
+ auth: {
+ pass: 'REPLACE-WITH-YOUR-GENERATED-PASSWORD',
+ user: 'REPLACE-WITH-YOUR-ALIAS@DOMAIN.TLD',
+ },
+ host: 'smtp.domain.tld',
+ port: 465,
+ secure: true,
},
})
)
+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'
SMTPTransport.SentMessageInfo
>(0, availableParallelism(), workerFile, {
enableTasksQueue: true,
- tasksQueueOptions: {
- concurrency: 8,
- },
errorHandler: (e: Error) => {
console.error('Thread worker error:', e)
},
+ tasksQueueOptions: {
+ concurrency: 8,
+ },
})
import type SMTPTransport from 'nodemailer/lib/smtp-transport/index.js'
export interface WorkerData {
- smtpTransport: SMTPTransport.Options
mail: Mail.Options
+ smtpTransport: SMTPTransport.Options
}
-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'
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' }))
}
})
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,
})
import { dirname, extname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
-
import { availableParallelism, FixedClusterPool } from 'poolifier'
import type { WorkerData, WorkerResponse } from './types.js'
workerFile,
{
enableEvents: false,
+ errorHandler: (e: Error) => {
+ console.error('Cluster worker error', e)
+ },
onlineHandler: () => {
pool
.execute({ port: 8080 })
)
})
},
- errorHandler: (e: Error) => {
- console.error('Cluster worker error', e)
- },
}
)
}
export interface MessagePayload<T = unknown> {
- type: MessageType
data: T
+ type: MessageType
}
export interface DataPayload {
}
export interface WorkerResponse {
- status: boolean
port?: number
+ status: boolean
}
} from './types.js'
class WebSocketServerWorker extends ClusterWorker<WorkerData, WorkerResponse> {
- 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 {
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<DataPayload>
switch (type) {
case MessageType.echo:
ws.send(
JSON.stringify({
- type: MessageType.echo,
data,
+ type: MessageType.echo,
})
)
break
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)
)
})
})
return {
- status: true,
port: WebSocketServerWorker.wss.options.port,
+ status: true,
}
}
+ private static wss: WebSocketServer
+
public constructor () {
super(WebSocketServerWorker.startWebSocketServer, {
killHandler: () => {
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' }))
}
})
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,
})
import { dirname, extname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
-
import { availableParallelism, FixedClusterPool } from 'poolifier'
import type { ClusterWorkerData, ClusterWorkerResponse } from './types.js'
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) {
)
})
},
- errorHandler: (e: Error) => {
- console.error('Cluster worker error', e)
- },
}
)
Data extends ThreadWorkerData<DataPayload>,
Response extends ThreadWorkerResponse<DataPayload>
> extends ThreadWorker<Data, Response> {
- private static readonly factorial = (n: number | bigint): bigint => {
+ private static readonly factorial = (n: bigint | number): bigint => {
if (n === 0 || n === 1) {
return 1n
} else {
}
export interface MessagePayload<T = unknown> {
- type: MessageType
data: T
+ type: MessageType
}
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<T = unknown> {
ClusterWorkerData,
ClusterWorkerResponse
> {
- private static wss: WebSocketServer
private static requestHandlerPool: DynamicThreadPool<
ThreadWorkerData<DataPayload>,
ThreadWorkerResponse<DataPayload>
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!
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<DataPayload>
switch (type) {
.then(response => {
ws.send(
JSON.stringify({
- type: MessageType.echo,
data: response.data,
+ type: MessageType.echo,
})
)
return undefined
ws.send(
JSON.stringify(
{
- type: MessageType.factorial,
data: response.data,
+ type: MessageType.factorial,
},
(_, v: unknown) =>
typeof v === 'bigint' ? v.toString() : v
})
})
return {
- status: true,
port: WebSocketServerWorker.wss.options.port,
+ status: true,
}
}
+ private static wss: WebSocketServer
+
public constructor () {
super(WebSocketServerWorker.startWebSocketServer, {
killHandler: async () => {
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' }))
}
})
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<DataPayload>
switch (type) {
.then(response => {
ws.send(
JSON.stringify({
- type: MessageType.echo,
data: response.data,
+ type: MessageType.echo,
})
)
return undefined
ws.send(
JSON.stringify(
{
- type: MessageType.factorial,
data: response.data,
+ type: MessageType.factorial,
},
(_, v: unknown) => (typeof v === 'bigint' ? v.toString() : v)
)
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'
WorkerResponse<DataPayload>
>(1, availableParallelism(), workerFile, {
enableTasksQueue: true,
- tasksQueueOptions: {
- concurrency: 8,
- },
errorHandler: (e: Error) => {
console.error('Thread worker error:', e)
},
+ tasksQueueOptions: {
+ concurrency: 8,
+ },
})
}
export interface MessagePayload<T = unknown> {
- type: MessageType
data: T
+ type: MessageType
}
export interface DataPayload {
Data extends WorkerData<DataPayload>,
Response extends WorkerResponse<DataPayload>
> extends ThreadWorker<Data, Response> {
- private static readonly factorial = (n: number | bigint): bigint => {
+ private static readonly factorial = (n: bigint | number): bigint => {
if (n === 0 || n === 1) {
return 1n
} else {
}
export interface MyResponse {
- message: string
data?: MyData
+ message: string
}
class MyThreadWorker extends ThreadWorker<MyData, MyResponse> {
private async process (data?: MyData): Promise<MyResponse> {
return await new Promise(resolve => {
setTimeout(() => {
- resolve({ message: 'Hello from Worker :)', data })
+ resolve({ data, message: 'Hello from Worker :)' })
}, 1000)
})
}
"@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",
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
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
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
'@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==}
'@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==}
'@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==}
'@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==}
'@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==}
'@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==}
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}
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}
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==}
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==}
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==}
'@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
'@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
'@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
'@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
'@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
'@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
'@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-docker@1.1.7': {}
- '@cspell/dict-dotnet@5.0.2': {}
+ '@cspell/dict-dotnet@5.0.3': {}
'@cspell/dict-elixir@4.0.3': {}
'@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-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:
'@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': {}
'@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
'@types/conventional-commits-parser@5.0.0':
dependencies:
- '@types/node': 22.4.1
+ '@types/node': 22.5.0
'@types/eslint@9.6.0':
dependencies:
'@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:
'@types/minimatch@5.1.2': {}
- '@types/node@22.4.1':
+ '@types/node@22.5.0':
dependencies:
undici-types: 6.19.8
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
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)
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
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
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
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):
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: {}
-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'
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',
}
format: 'esm',
...(isDevelopmentBuild
? {
+ chunkFileNames: '[name]-[hash].mjs',
dir: './lib',
entryFileNames: '[name].mjs',
- chunkFileNames: '[name]-[hash].mjs',
preserveModules: true,
preserveModulesRoot: './src',
}
}),
},
],
- external: [/^node:*/],
plugins: [
typescript({
- tsconfig: './tsconfig.build.json',
compilerOptions: {
sourceMap: sourcemap,
},
+ tsconfig: './tsconfig.build.json',
}),
del({
targets: ['./lib/*'],
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,
},
])
* @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
/**
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.
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.
}
/**
- * 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))
}
}
+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,
round,
sleep,
} from '../utils.js'
-import type {
- TaskFunction,
- TaskFunctionObject,
-} from '../worker/task-functions.js'
import { KillBehaviors } from '../worker/worker-options.js'
import {
type IPool,
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'
/**
Data = unknown,
Response = unknown
> implements IPool<Worker, Data, Response> {
- /** @inheritDoc */
- public readonly workerNodes: IWorkerNode<Worker, Data>[] = []
-
- /** @inheritDoc */
- public emitter?: EventEmitterAsyncResource
-
/**
* The task execution response promise map:
* - `key`: The message id of each submitted task.
>
/**
- * 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<Data, Response>
- >
+ protected readonly workerMessageListener = (
+ message: MessageValue<Response>
+ ): 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<WorkerChoiceStrategy> => {
+ 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<Worker>,
- 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<string, TaskFunctionObject<Data, Response>>()
+ 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<Data>
+ ): 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<Worker>): 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<Worker, Data>,
+ destinationWorkerNodeKey: number
+ ): Task<Data> | 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<Data, Response>
+ >
+
+ private readonly workerNodeStealTask = (
+ workerNodeKey: number
+ ): Task<Data> | 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<Worker, Data>[] = []
+
+ /**
+ * 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<Worker>,
+ 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<number[]>(
- (accumulator, workerNode) =>
- accumulator.concat(
- workerNode.usage.runTime.history.toArray()
- ),
- []
- )
- )
- ),
- }),
- ...(this.workerChoiceStrategiesContext.getTaskStatisticsRequirements()
- .runTime.median && {
- median: round(
- median(
- this.workerNodes.reduce<number[]>(
- (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<number[]>(
- (accumulator, workerNode) =>
- accumulator.concat(
- workerNode.usage.waitTime.history.toArray()
- ),
- []
- )
- )
- ),
- }),
- ...(this.workerChoiceStrategiesContext.getTaskStatisticsRequirements()
- .waitTime.median && {
- median: round(
- median(
- this.workerNodes.reduce<number[]>(
- (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<number[]>(
- (accumulator, workerNode) =>
- accumulator.concat(
- workerNode.usage.elu.idle.history.toArray()
- ),
- []
- )
- )
- ),
- }),
- ...(this.workerChoiceStrategiesContext.getTaskStatisticsRequirements()
- .elu.median && {
- median: round(
- median(
- this.workerNodes.reduce<number[]>(
- (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<number[]>(
- (accumulator, workerNode) =>
- accumulator.concat(
- workerNode.usage.elu.active.history.toArray()
- ),
- []
- )
- )
- ),
- }),
- ...(this.workerChoiceStrategiesContext.getTaskStatisticsRequirements()
- .elu.median && {
- median: round(
- median(
- this.workerNodes.reduce<number[]>(
- (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<Data | Response>): 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<Data>
- ): Promise<boolean> {
- return await new Promise<boolean>((resolve, reject) => {
- const taskFunctionOperationListener = (
- message: MessageValue<Response>
- ): 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<Data>
- ): Promise<boolean> {
- return await new Promise<boolean>((resolve, reject) => {
- const responsesReceived = new Array<MessageValue<Response>>()
- const taskFunctionOperationsListener = (
- message: MessageValue<Response>
- ): 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<Data, Response> | TaskFunctionObject<Data, Response>
- ): Promise<boolean> {
- 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<Data, Response>
- }
- 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<boolean> {
- 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<string, TaskFunctionObject<Data, Response>>()
- /**
- * 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<WorkerChoiceStrategy> => {
- 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<boolean> {
- 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<Response> {
- return await new Promise<Response>((resolve, reject) => {
- const timestamp = performance.now()
- const workerNodeKey = this.chooseWorkerNode(name)
- const task: Task<Data> = {
- 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<Response> {
- 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<Data>,
- name?: string,
- transferList?: readonly TransferListItem[]
- ): Promise<Response[]> {
- 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<Response>
+ ): 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<Data>
+ ): 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<void> {
- 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<void> {
- await new Promise<void>((resolve, reject) => {
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- if (this.workerNodes[workerNodeKey] == null) {
- resolve()
- return
- }
- const killMessageListener = (message: MessageValue<Response>): 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<Message>) => void
+ ): void
+
/**
* Terminates the worker node given its worker node key.
* @param workerNodeKey - The worker node key.
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
+ )
}
/**
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<Data>
+ listener: (message: MessageValue<Message>) => 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<Message>) => 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<Data>,
+ 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<Worker, Data>): 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<Data | Response>): 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<Worker>): 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<Response>
+ 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.
)
}
- /**
- * 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<Data>,
- transferList?: readonly TransferListItem[]
- ): void
+ /**
+ * Creates a worker node.
+ * @returns The created worker node.
+ */
+ private createWorkerNode (): IWorkerNode<Worker, Data> {
+ const workerNode = new WorkerNode<Worker, Data>(
+ 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<Data> | undefined {
+ return this.workerNodes[workerNodeKey].dequeueTask()
+ }
+
+ private enqueueTask (workerNodeKey: number, task: Task<Data>): 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<Data>): 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<Data>): void {
+ if (this.shallExecuteTask(workerNodeKey)) {
+ this.executeTask(workerNodeKey, task)
+ } else {
+ this.enqueueTask(workerNodeKey, task)
+ }
+ }
+
+ private handleTaskExecutionResponse (message: MessageValue<Response>): 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<Response>): 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.
}
}
- /**
- * 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<Response> {
+ return await new Promise<Response>((resolve, reject) => {
+ const timestamp = performance.now()
+ const workerNodeKey = this.chooseWorkerNode(name)
+ const task: Task<Data> = {
+ 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<Message>) => 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<Message>) => 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<Message>) => 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<Worker, Data>): 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<void> {
+ await new Promise<void>((resolve, reject) => {
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (this.workerNodes[workerNodeKey] == null) {
+ resolve()
+ return
+ }
+ const killMessageListener = (message: MessageValue<Response>): 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.
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<Data>
+ ): Promise<boolean> {
+ return await new Promise<boolean>((resolve, reject) => {
+ const taskFunctionOperationListener = (
+ message: MessageValue<Response>
+ ): 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<Data>): void {
- if (this.shallExecuteTask(workerNodeKey)) {
- this.executeTask(workerNodeKey, task)
- } else {
- this.enqueueTask(workerNodeKey, task)
+ private async sendTaskFunctionOperationToWorkers (
+ message: MessageValue<Data>
+ ): Promise<boolean> {
+ return await new Promise<boolean>((resolve, reject) => {
+ const responsesReceived = new Array<MessageValue<Response>>()
+ const taskFunctionOperationsListener = (
+ message: MessageValue<Response>
+ ): 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
+ )
}
}
}
}
- 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<Worker, Data>,
- destinationWorkerNodeKey: number
- ): Task<Data> | 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<Data, Response> | TaskFunctionObject<Data, Response>
+ ): Promise<boolean> {
+ 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<Data, Response>
+ }
+ 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<Data>
- ): void => {
- const { workerNodeKey } = eventDetail
- if (workerNodeKey == null) {
- throw new Error(
- "WorkerNode event detail 'workerNodeKey' property must be defined"
- )
+ /** @inheritDoc */
+ public async destroy (): Promise<void> {
+ 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<Response> {
+ 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<Data> | undefined => {
- const workerNodes = this.workerNodes
- .slice()
- .sort(
- (workerNodeA, workerNodeB) =>
- workerNodeB.usage.tasks.queued - workerNodeA.usage.tasks.queued
+ return []
+ }
+
+ /** @inheritDoc */
+ public async mapExecute (
+ data: Iterable<Data>,
+ name?: string,
+ transferList?: readonly TransferListItem[]
+ ): Promise<Response[]> {
+ 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<boolean> {
+ 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<boolean> {
+ 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<Response>
- ): 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<Response>): 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<Response>): 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<number[]>(
+ (accumulator, workerNode) =>
+ accumulator.concat(
+ workerNode.usage.runTime.history.toArray()
+ ),
+ []
+ )
+ )
+ ),
+ }),
+ ...(this.workerChoiceStrategiesContext.getTaskStatisticsRequirements()
+ .runTime.median && {
+ median: round(
+ median(
+ this.workerNodes.reduce<number[]>(
+ (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<number[]>(
+ (accumulator, workerNode) =>
+ accumulator.concat(
+ workerNode.usage.waitTime.history.toArray()
+ ),
+ []
+ )
+ )
+ ),
+ }),
+ ...(this.workerChoiceStrategiesContext.getTaskStatisticsRequirements()
+ .waitTime.median && {
+ median: round(
+ median(
+ this.workerNodes.reduce<number[]>(
+ (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<number[]>(
+ (accumulator, workerNode) =>
+ accumulator.concat(
+ workerNode.usage.elu.active.history.toArray()
+ ),
+ []
+ )
+ )
+ ),
+ }),
+ ...(this.workerChoiceStrategiesContext.getTaskStatisticsRequirements()
+ .elu.median && {
+ median: round(
+ median(
+ this.workerNodes.reduce<number[]>(
+ (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<number[]>(
+ (accumulator, workerNode) =>
+ accumulator.concat(
+ workerNode.usage.elu.idle.history.toArray()
+ ),
+ []
+ )
+ )
+ ),
+ }),
+ ...(this.workerChoiceStrategiesContext.getTaskStatisticsRequirements()
+ .elu.median && {
+ median: round(
+ median(
+ this.workerNodes.reduce<number[]>(
+ (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<Worker, Data> {
- const workerNode = new WorkerNode<Worker, Data>(
- 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<Worker, Data>): 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<Worker, Data>): 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<Data>): void {
- this.beforeTaskExecutionHook(workerNodeKey, task)
- this.sendToWorker(workerNodeKey, task, task.transferList)
- this.checkAndEmitTaskExecutionEvents()
- }
-
- private enqueueTask (workerNodeKey: number, task: Task<Data>): number {
- const tasksQueueSize = this.workerNodes[workerNodeKey].enqueueTask(task)
- this.checkAndEmitTaskQueuingEvents()
- return tasksQueueSize
- }
-
- private dequeueTask (workerNodeKey: number): Task<Data> | 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
}
)
}
- /** @inheritDoc */
- protected shallCreateDynamicWorker (): boolean {
- return (!this.full && this.internalBusy()) || this.empty
- }
-
/** @inheritDoc */
protected checkAndEmitDynamicWorkerCreationEvents (): void {
if (this.full) {
}
/** @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
+ }
}
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'
}
/** @inheritDoc */
- protected setupHook (): void {
- cluster.setupPrimary({ ...this.opts.settings, exec: this.filePath })
+ protected checkAndEmitDynamicWorkerCreationEvents (): void {
+ /* noop */
+ }
+
+ /** @inheritDoc */
+ protected deregisterWorkerMessageListener<Message extends Data | Response>(
+ workerNodeKey: number,
+ listener: (message: MessageValue<Message>) => void
+ ): void {
+ this.workerNodes[workerNodeKey].worker.off('message', listener)
}
/** @inheritDoc */
}
/** @inheritDoc */
- protected sendToWorker (
+ protected registerOnceWorkerMessageListener<Message extends Data | Response>(
workerNodeKey: number,
- message: MessageValue<Data>
+ listener: (message: MessageValue<Message>) => void
): void {
- this.workerNodes[workerNodeKey]?.worker.send({
- ...message,
- workerId: this.getWorkerInfo(workerNodeKey)?.id,
- } satisfies MessageValue<Data>)
- }
-
- /** @inheritDoc */
- protected sendStartupMessageToWorker (workerNodeKey: number): void {
- this.sendToWorker(workerNodeKey, {
- ready: false,
- })
+ this.workerNodes[workerNodeKey].worker.once('message', listener)
}
/** @inheritDoc */
}
/** @inheritDoc */
- protected registerOnceWorkerMessageListener<Message extends Data | Response>(
- workerNodeKey: number,
- listener: (message: MessageValue<Message>) => void
- ): void {
- this.workerNodes[workerNodeKey].worker.once('message', listener)
+ protected sendStartupMessageToWorker (workerNodeKey: number): void {
+ this.sendToWorker(workerNodeKey, {
+ ready: false,
+ })
}
/** @inheritDoc */
- protected deregisterWorkerMessageListener<Message extends Data | Response>(
+ protected sendToWorker (
workerNodeKey: number,
- listener: (message: MessageValue<Message>) => void
+ message: MessageValue<Data>
): void {
- this.workerNodes[workerNodeKey].worker.off('message', listener)
+ this.workerNodes[workerNodeKey]?.worker.send({
+ ...message,
+ workerId: this.getWorkerInfo(workerNodeKey)?.id,
+ } satisfies MessageValue<Data>)
+ }
+
+ /** @inheritDoc */
+ protected setupHook (): void {
+ cluster.setupPrimary({ ...this.opts.settings, exec: this.filePath })
}
/** @inheritDoc */
}
/** @inheritDoc */
- protected checkAndEmitDynamicWorkerCreationEvents (): void {
- /* noop */
+ protected get busy (): boolean {
+ return this.internalBusy()
}
/** @inheritDoc */
protected get worker (): WorkerType {
return WorkerTypes.cluster
}
-
- /** @inheritDoc */
- protected get busy (): boolean {
- return this.internalBusy()
- }
}
* 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)
/**
* 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)
/**
* 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
*/
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
}
/**
*/
export interface PoolOptions<Worker extends IWorker> {
/**
- * A function that will listen for online event on each worker.
- * @defaultValue `() => {}`
+ * Pool events integrated with async resource emission.
+ * @defaultValue true
*/
- onlineHandler?: OnlineHandler<Worker>
+ enableEvents?: boolean
/**
- * A function that will listen for message event on each worker.
- * @defaultValue `() => {}`
+ * Pool worker node tasks queue.
+ * @defaultValue false
*/
- messageHandler?: MessageHandler<Worker>
+ enableTasksQueue?: boolean
+ /**
+ * Key/value pairs to add to worker process environment.
+ * @see https://nodejs.org/api/cluster.html#cluster_cluster_fork_env
+ */
+ env?: Record<string, unknown>
/**
* A function that will listen for error event on each worker.
* @defaultValue `() => {}`
*/
exitHandler?: ExitHandler<Worker>
/**
- * 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<Worker>
/**
- * The worker choice strategy options.
+ * A function that will listen for online event on each worker.
+ * @defaultValue `() => {}`
*/
- workerChoiceStrategyOptions?: WorkerChoiceStrategyOptions
+ onlineHandler?: OnlineHandler<Worker>
/**
* 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<string, unknown>
+ 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
}
/**
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<Data, Response> | TaskFunctionObject<Data, Response>
+ ) => Promise<boolean>
/**
- * Pool worker nodes.
- * @internal
+ * Terminates all workers in this pool.
*/
- readonly workerNodes: IWorkerNode<Worker, Data>[]
+ readonly destroy: () => Promise<void>
/**
* Pool event emitter integrated with async resource.
* The async tracking tooling identifier is `poolifier:<PoolType>-<WorkerType>-pool`.
* - `'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.
name?: string,
transferList?: readonly TransferListItem[]
) => Promise<Response>
+ /**
+ * 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.
name?: string,
transferList?: readonly TransferListItem[]
) => Promise<Response[]>
- /**
- * Starts the minimum number of workers in this pool.
- */
- readonly start: () => void
- /**
- * Terminates all workers in this pool.
- */
- readonly destroy: () => Promise<void>
- /**
- * 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<Data, Response> | TaskFunctionObject<Data, Response>
- ) => Promise<boolean>
/**
* Removes a task function from this pool.
* @param name - The name of the task function.
* @returns `true` if the task function was removed, `false` otherwise.
*/
readonly removeTaskFunction: (name: string) => Promise<boolean>
- /**
- * 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<boolean>
+ /**
+ * 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.
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<Worker, Data>[]
}
import type { IPool } from '../pool.js'
-import { DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS } from '../utils.js'
import type { IWorker } from '../worker.js'
import type {
IWorkerChoiceStrategy,
TaskStatisticsRequirements,
WorkerChoiceStrategyOptions,
} from './selection-strategies-types.js'
+
+import { DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS } from '../utils.js'
import {
buildWorkerChoiceStrategyOptions,
toggleMedianMeasurementStatisticsRequirements,
/** @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,
}
/**
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<Worker, Data, Response>(
- 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.
*/
}
}
+ /**
+ * 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.
}
/**
- * 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
}
/**
? 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<Worker, Data, Response>(
+ this.pool,
+ opts
+ )
+ this.setTaskStatisticsRequirements(this.opts)
+ }
+
+ /** @inheritDoc */
+ public abstract update (workerNodeKey: number): boolean
}
import type { IPool } from '../pool.js'
import type { IWorker } from '../worker.js'
+
import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy.js'
import {
type IWorkerChoiceStrategy,
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,
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 {
)
}
- /**
- * 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
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
+ }
}
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.
>
extends AbstractWorkerChoiceStrategy<Worker, Data, Response>
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.
*/
* 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 (
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 */
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) {
}
/** @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
}
}
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.
implements IWorkerChoiceStrategy {
/** @inheritDoc */
public readonly taskStatisticsRequirements: TaskStatisticsRequirements = {
+ elu: DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS,
runTime: {
aggregate: true,
average: false,
average: false,
median: false,
},
- elu: DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS,
}
/** @inheritDoc */
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 */
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
}
}
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.
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 */
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 */
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
}
}
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.
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 */
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
}
}
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.
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 */
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
}
}
* 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.
*/
*/
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)
/**
* 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)
/**
* 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.
*/
* @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.
* @internal
*/
export interface TaskStatisticsRequirements {
+ /**
+ * Tasks event loop utilization requirements.
+ */
+ readonly elu: MeasurementStatisticsRequirements
/**
* Tasks runtime requirements.
*/
* Tasks wait time requirements.
*/
readonly waitTime: MeasurementStatisticsRequirements
- /**
- * Tasks event loop utilization requirements.
- */
- readonly elu: MeasurementStatisticsRequirements
}
/**
* @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
}
/**
* @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.
* @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
}
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'
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
opts.weights = opts.weights ?? getDefaultWeights(pool.info.maxSize)
return {
...{
+ elu: { median: false },
runTime: { median: false },
waitTime: { median: false },
- elu: { median: false },
},
...opts,
}
([_, workerChoiceStrategy]) => workerChoiceStrategy.strategyPolicy
)
return {
- dynamicWorkerUsage: policies.some(p => p.dynamicWorkerUsage),
dynamicWorkerReady: policies.some(p => p.dynamicWorkerReady),
+ dynamicWorkerUsage: policies.some(p => p.dynamicWorkerUsage),
}
}
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),
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),
- },
}
}
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
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.
>
extends AbstractWorkerChoiceStrategy<Worker, Data, Response>
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,
average: true,
median: false,
},
- elu: DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS,
}
- /**
- * Worker node virtual execution time.
- */
- private workerNodeVirtualTaskExecutionTime = 0
-
/** @inheritDoc */
public constructor (
pool: IPool<Worker, Data, Response>,
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 */
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
}
}
WorkerChoiceStrategy,
WorkerChoiceStrategyOptions,
} from './selection-strategies-types.js'
+
import { WorkerChoiceStrategies } from './selection-strategies-types.js'
import {
buildWorkerChoiceStrategiesPolicy,
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.
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.
}
/**
- * 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<Worker, Data, Response>,
opts?: WorkerChoiceStrategyOptions
- ): void {
- if (workerChoiceStrategy !== this.defaultWorkerChoiceStrategy) {
- this.defaultWorkerChoiceStrategy = workerChoiceStrategy
- this.addWorkerChoiceStrategy(workerChoiceStrategy, this.pool, opts)
+ ): Map<WorkerChoiceStrategy, IWorkerChoiceStrategy> {
+ if (!this.workerChoiceStrategies.has(workerChoiceStrategy)) {
+ return this.workerChoiceStrategies.set(
+ workerChoiceStrategy,
+ getWorkerChoiceStrategy<Worker, Data, Response>(
+ 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
}
/**
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.
).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)
}
}
/**
- * 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<Worker, Data, Response>,
- opts?: WorkerChoiceStrategyOptions
- ): Map<WorkerChoiceStrategy, IWorkerChoiceStrategy> {
- if (!this.workerChoiceStrategies.has(workerChoiceStrategy)) {
- return this.workerChoiceStrategies.set(
- workerChoiceStrategy,
- getWorkerChoiceStrategy<Worker, Data, Response>(
- 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)
}
}
)
}
- /** @inheritDoc */
- protected shallCreateDynamicWorker (): boolean {
- return (!this.full && this.internalBusy()) || this.empty
- }
-
/** @inheritDoc */
protected checkAndEmitDynamicWorkerCreationEvents (): void {
if (this.full) {
}
/** @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
+ }
}
} 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'
}
/** @inheritDoc */
- protected isMain (): boolean {
- return isMainThread
+ protected checkAndEmitDynamicWorkerCreationEvents (): void {
+ /* noop */
}
/** @inheritDoc */
- protected sendToWorker (
+ protected deregisterWorkerMessageListener<Message extends Data | Response>(
workerNodeKey: number,
- message: MessageValue<Data>,
- transferList?: readonly TransferListItem[]
+ listener: (message: MessageValue<Message>) => void
): void {
- this.workerNodes[workerNodeKey]?.messageChannel?.port1.postMessage(
- {
- ...message,
- workerId: this.getWorkerInfo(workerNodeKey)?.id,
- } satisfies MessageValue<Data>,
- 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<Data>,
- [port2]
- )
+ protected isMain (): boolean {
+ return isMainThread
}
/** @inheritDoc */
- protected registerWorkerMessageListener<Message extends Data | Response>(
+ protected registerOnceWorkerMessageListener<Message extends Data | Response>(
workerNodeKey: number,
listener: (message: MessageValue<Message>) => void
): void {
- this.workerNodes[workerNodeKey].messageChannel?.port1.on(
+ this.workerNodes[workerNodeKey].messageChannel?.port1.once(
'message',
listener
)
}
/** @inheritDoc */
- protected registerOnceWorkerMessageListener<Message extends Data | Response>(
+ protected registerWorkerMessageListener<Message extends Data | Response>(
workerNodeKey: number,
listener: (message: MessageValue<Message>) => void
): void {
- this.workerNodes[workerNodeKey].messageChannel?.port1.once(
+ this.workerNodes[workerNodeKey].messageChannel?.port1.on(
'message',
listener
)
}
/** @inheritDoc */
- protected deregisterWorkerMessageListener<Message extends Data | Response>(
+ 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<Data>,
+ [port2]
+ )
+ }
+
+ /** @inheritDoc */
+ protected sendToWorker (
workerNodeKey: number,
- listener: (message: MessageValue<Message>) => void
+ message: MessageValue<Data>,
+ 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<Data>,
+ transferList
)
}
}
/** @inheritDoc */
- protected checkAndEmitDynamicWorkerCreationEvents (): void {
- /* noop */
+ protected get busy (): boolean {
+ return this.internalBusy()
}
/** @inheritDoc */
protected get worker (): WorkerType {
return WorkerTypes.thread
}
-
- /** @inheritDoc */
- protected get busy (): boolean {
- return this.internalBusy()
- }
}
} 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,
poolMaxSize: number
): Required<TasksQueueOptions> => {
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,
}
}
}
export const checkValidWorkerChoiceStrategy = (
- workerChoiceStrategy: WorkerChoiceStrategy | undefined
+ workerChoiceStrategy: undefined | WorkerChoiceStrategy
): void => {
if (
workerChoiceStrategy != null &&
}
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')
Response = unknown
>(
workerChoiceStrategiesContext:
- | WorkerChoiceStrategiesContext<Worker, Data, Response>
- | undefined,
+ | undefined
+ | WorkerChoiceStrategiesContext<Worker, Data, Response>,
workerUsage: WorkerUsage,
task: Task<Data>
): void => {
Response = unknown
>(
workerChoiceStrategiesContext:
- | WorkerChoiceStrategiesContext<Worker, Data, Response>
- | undefined,
+ | undefined
+ | WorkerChoiceStrategiesContext<Worker, Data, Response>,
workerUsage: WorkerUsage,
message: MessageValue<Response>
): void => {
Response = unknown
>(
workerChoiceStrategiesContext:
- | WorkerChoiceStrategiesContext<Worker, Data, Response>
- | undefined,
+ | undefined
+ | WorkerChoiceStrategiesContext<Worker, Data, Response>,
workerUsage: WorkerUsage,
message: MessageValue<Response>
): void => {
opts: { env?: Record<string, unknown>; 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}'`)
* @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) {
return
}
switch (workerNodeEvent) {
- case 'idle':
case 'backPressure':
+ case 'idle':
case 'taskFinished':
workerNode.on(workerNodeEvent, () => {
++events
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,
export class WorkerNode<Worker extends IWorker, Data = unknown>
extends EventEmitter
implements IWorkerNode<Worker, Data> {
- /** @inheritdoc */
- public readonly worker: Worker
+ private setBackPressureFlag: boolean
+ private readonly taskFunctionsUsage: Map<string, WorkerUsage>
+ private readonly tasksQueue: PriorityQueue<Task<Data>>
/** @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<Task<Data>>
- private setBackPressureFlag: boolean
- private readonly taskFunctionsUsage: Map<string, WorkerUsage>
+ /** @inheritdoc */
+ public usage: WorkerUsage
+ /** @inheritdoc */
+ public readonly worker: Worker
/**
* Constructs a new worker node.
this.taskFunctionsUsage = new Map<string, WorkerUsage>()
}
+ 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<Data>): 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<Data> | undefined {
+ // Start from the last empty or partially filled bucket
+ return this.dequeueTask(this.tasksQueue.buckets + 1)
}
/** @inheritdoc */
}
/** @inheritdoc */
- public dequeueLastPrioritizedTask (): Task<Data> | 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<void> {
- const waitWorkerExit = new Promise<void>(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<Data>): 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<Worker>
- ): void {
- this.worker.on(event, handler)
- }
-
- /** @inheritdoc */
- public registerOnceWorkerEventHandler (
- event: string,
- handler: EventHandler<Worker>
- ): 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`
}
/** @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<Worker>
+ ): 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<Worker>
+ ): 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<void> {
+ const waitWorkerExit = new Promise<void>(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
}
}
* @typeParam Worker - Type of worker.
*/
export type EventHandler<Worker extends IWorker> =
- | OnlineHandler<Worker>
- | MessageHandler<Worker>
| ErrorHandler<Worker>
| ExitHandler<Worker>
+ | MessageHandler<Worker>
+ | OnlineHandler<Worker>
/**
* Measurement history size.
*/
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
}
/**
* @internal
*/
export interface EventLoopUtilizationMeasurementStatistics {
- readonly idle: MeasurementStatistics
readonly active: MeasurementStatistics
+ readonly idle: MeasurementStatistics
utilization?: number
}
*/
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.
*/
* 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)
/**
*/
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.
*/
* 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
}
/**
*/
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
}
/**
* 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.
* @param handler - The event handler.
*/
readonly once: (event: string, handler: EventHandler<this>) => 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<number>
/**
- * 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
}
/**
* @internal
*/
export interface WorkerNodeOptions {
- workerOptions?: WorkerOptions
env?: Record<string, unknown>
tasksQueueBackPressureSize: number | undefined
tasksQueueBucketSize: number | undefined
tasksQueuePriority: boolean | undefined
+ workerOptions?: WorkerOptions
}
/**
export interface IWorkerNode<Worker extends IWorker, Data = unknown>
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<Data>) => number
+ readonly dequeueLastPrioritizedTask: () => Task<Data> | undefined
/**
* Dequeue task.
* @param bucket - The prioritized bucket to dequeue from. @defaultValue 0
*/
readonly dequeueTask: (bucket?: number) => Task<Data> | 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<Data> | undefined
+ readonly enqueueTask: (task: Task<Data>) => 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<void>
+ 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<Worker>
) => 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<Worker>
) => 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<void>
+ /**
+ * Worker usage statistics.
+ */
+ readonly usage: WorkerUsage
+ /**
+ * Worker.
+ */
+ readonly worker: Worker
}
/**
/** @inheritdoc */
public readonly capacity: number
/** @inheritdoc */
- public size!: number
- /** @inheritdoc */
public nodeArray: FixedQueueNode<T>[]
+ /** @inheritdoc */
+ public size!: number
/**
* Constructs a fixed queue.
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 */
}
/** @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 */
next: () => {
if (i >= this.size) {
return {
- value: undefined,
done: true,
+ value: undefined,
}
}
const value = this.nodeArray[index].data
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`)
- }
- }
}
-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.
-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.
* @internal
*/
export class PriorityQueue<T> {
- private head!: PriorityQueueNode<T>
- private tail!: PriorityQueueNode<T>
private readonly bucketSize: number
+ private head!: PriorityQueueNode<T>
private priorityEnabled: boolean
+ private tail!: PriorityQueueNode<T>
/** The priority queue maximum size. */
public maxSize!: number
this.clear()
}
- /**
- * The priority queue size.
- * @returns The priority queue size.
- */
- public get size (): number {
- let node: PriorityQueueNode<T> | 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<T>[]
+ ): PriorityQueueNode<T> {
+ let fixedQueue: IFixedQueue<T>
+ if (this.priorityEnabled) {
+ fixedQueue = new FixedPriorityQueue(this.bucketSize)
+ } else {
+ fixedQueue = new FixedQueue(this.bucketSize)
}
- this.priorityEnabled = enablePriority
- let head: PriorityQueueNode<T>
- let tail: PriorityQueueNode<T>
- let prev: PriorityQueueNode<T> | undefined
- let node: PriorityQueueNode<T> | 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
}
/**
}
/**
- * 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
}
/**
const value = node.get(index) as T
if (value == null) {
return {
- value: undefined,
done: true,
+ value: undefined,
}
}
++index
index = 0
}
return {
- value,
done: false,
+ value,
}
},
}
}
- private getPriorityQueueNode (
- nodeArray?: FixedQueueNode<T>[]
- ): PriorityQueueNode<T> {
- let fixedQueue: IFixedQueue<T>
- 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<T>
+ let tail: PriorityQueueNode<T>
+ let prev: PriorityQueueNode<T> | undefined
+ let node: PriorityQueueNode<T> | 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<T> | undefined = this.tail
+ let size = 0
+ while (node != null) {
+ size += node.size
+ node = node.next
+ }
+ return size
}
}
* @internal
*/
export interface IFixedQueue<T> {
+ /**
+ * 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<T>
/** The fixed queue capacity. */
readonly capacity: number
- /** The fixed queue size. */
- readonly size: number
- /** The fixed queue node array. */
- nodeArray: FixedQueueNode<T>[]
+ /**
+ * 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.
* @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<T>
+ /** The fixed queue node array. */
+ nodeArray: FixedQueueNode<T>[]
+ /** The fixed queue size. */
+ readonly size: number
}
/**
*/
export interface WorkerError<Data = unknown> {
/**
- * 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
}
/**
*/
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
}
/**
* @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
}
/**
* @internal
*/
export interface Task<Data = unknown> {
- /**
- * 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
*/
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[]
}
/**
export interface MessageValue<Data = unknown, ErrorData = unknown>
extends Task<Data> {
/**
- * 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<ErrorData>
+ 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.
*/
* 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<ErrorData>
/**
- * Message port.
+ * Worker id.
*/
- readonly port?: MessagePort
+ readonly workerId?: number
}
/**
*/
export interface PromiseResponseWrapper<Response = unknown> {
/**
- * Resolve callback to fulfill the promise.
+ * The asynchronous resource used to track the task execution.
*/
- readonly resolve: (value: Response | PromiseLike<Response>) => 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> | Response) => void
/**
- * The asynchronous resource used to track the task execution.
+ * The worker node key executing the task.
*/
- readonly asyncResource?: AsyncResource
+ readonly workerNodeKey: number
}
/**
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,
TaskPerformance,
WorkerStatistics,
} from '../utility-types.js'
-import {
- buildTaskFunctionProperties,
- DEFAULT_TASK_NAME,
- EMPTY_FUNCTION,
- isAsyncFunction,
- isPlainObject,
-} from '../utils.js'
import type {
TaskAsyncFunction,
TaskFunction,
TaskFunctions,
TaskSyncFunction,
} from './task-functions.js'
+
+import {
+ buildTaskFunctionProperties,
+ DEFAULT_TASK_NAME,
+ EMPTY_FUNCTION,
+ isAsyncFunction,
+ isPlainObject,
+} from '../utils.js'
import {
checkTaskFunctionName,
checkValidTaskFunctionObjectEntry,
* 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,
}
/**
* @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<string, TaskFunctionObject<Data, Response>>
+ 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<Data>): 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<Data, Response>, task)
+ } else {
+ this.runSync(fn as TaskSyncFunction<Data, Response>, 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<Data, Response>,
+ task: Task<Data>
+ ): 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<Data, Response>,
+ task: Task<Data>
+ ): 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<string, TaskFunctionObject<Data, Response>>
/**
* Constructs a new poolifier worker.
*/
public constructor (
protected readonly isMain: boolean | undefined,
- private readonly mainWorker: MainWorker | undefined | null,
+ private readonly mainWorker: MainWorker | null | undefined,
taskFunctions: TaskFunction<Data, Response> | TaskFunctions<Data, Response>,
protected opts: WorkerOptions = DEFAULT_WORKER_OPTIONS
) {
}
}
- 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<Data, Response>
- | TaskFunctions<Data, Response>
- | undefined
- ): void {
- if (taskFunctions == null) {
- throw new Error('taskFunctions parameter is mandatory')
- }
- this.taskFunctions = new Map<string, TaskFunctionObject<Data, Response>>()
- 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<Data, Response>(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<Data>): void {
+ this.stopCheckActive()
+ if (isAsyncFunction(this.opts.killHandler)) {
+ ;(this.opts.killHandler as () => Promise<void>)()
+ .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<Data, Response> | TaskFunctionObject<Data, Response>
- ): 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<Data, Response>
- }
- checkValidTaskFunctionObjectEntry<Data, Response>(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<Data>): void
- /**
- * Worker message listener.
- * @param message - The received message.
- */
- protected messageListener (message: MessageValue<Data>): 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<Data>
): void {
- const { taskFunctionOperation, taskFunctionProperties, taskFunction } =
+ const { taskFunction, taskFunctionOperation, taskFunctionProperties } =
message
if (taskFunctionProperties == null) {
throw new Error(
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,
...(!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<Data>): void {
- this.stopCheckActive()
- if (isAsyncFunction(this.opts.killHandler)) {
- ;(this.opts.killHandler as () => Promise<void>)()
- .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<Data>): 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<Data>): 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<Response, Data>
+ ): 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(),
+ }),
}
}
}
/**
- * 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<Data>): 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<Response, Data>
- ): void
-
- /**
- * Sends task functions properties to the main worker.
- */
- protected sendTaskFunctionsPropertiesToMainWorker (): void {
- this.sendToMainWorker({
- taskFunctionsProperties: this.listTaskFunctionsProperties(),
- })
+ private checkTaskFunctions (
+ taskFunctions:
+ | TaskFunction<Data, Response>
+ | TaskFunctions<Data, Response>
+ | undefined
+ ): void {
+ if (taskFunctions == null) {
+ throw new Error('taskFunctions parameter is mandatory')
+ }
+ this.taskFunctions = new Map<string, TaskFunctionObject<Data, Response>>()
+ 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<Data, Response>(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<Data>): 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<Data, Response>, task)
- } else {
- this.runSync(fn as TaskSyncFunction<Data, Response>, 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<Data, Response>,
- task: Task<Data>
- ): void => {
- const { name, taskId, data } = task
+ public addTaskFunction (
+ name: string,
+ fn: TaskFunction<Data, Response> | TaskFunctionObject<Data, Response>
+ ): 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<Data, Response>
+ }
+ checkValidTaskFunctionObjectEntry<Data, Response>(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<Data, Response>,
- task: Task<Data>
- ): 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 }
}
}
}
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`.
*
Data = unknown,
Response = unknown
> extends AbstractWorker<Worker, Data, Response> {
+ /** @inheritDoc */
+ protected readonly sendToMainWorker = (
+ message: MessageValue<Response>
+ ): void => {
+ this.getMainWorker().send({
+ ...message,
+ workerId: this.id,
+ } satisfies MessageValue<Response>)
+ }
+
/**
* Constructs a new poolifier cluster worker.
* @param taskFunctions - Task function(s) processed by the worker when the pool's `execution` function is invoked.
protected get id (): number {
return this.getMainWorker().id
}
-
- /** @inheritDoc */
- protected readonly sendToMainWorker = (
- message: MessageValue<Response>
- ): void => {
- this.getMainWorker().send({
- ...message,
- workerId: this.id,
- } satisfies MessageValue<Response>)
- }
}
* @typeParam Response - Type of execution response. This can only be structured-cloneable data.
*/
export type TaskFunction<Data = unknown, Response = unknown> =
- | TaskSyncFunction<Data, Response>
| TaskAsyncFunction<Data, Response>
+ | TaskSyncFunction<Data, Response>
/**
* Task function object.
* @typeParam Response - Type of execution response. This can only be structured-cloneable data.
*/
export interface TaskFunctionObject<Data = unknown, Response = unknown> {
- /**
- * Task function.
- */
- taskFunction: TaskFunction<Data, Response>
/**
* Task function priority. Lower values have higher priority.
*/
* Task function worker choice strategy.
*/
strategy?: WorkerChoiceStrategy
+ /**
+ * Task function.
+ */
+ taskFunction: TaskFunction<Data, Response>
}
/**
* Task function operation result.
*/
export interface TaskFunctionOperationResult {
- status: boolean
error?: Error
+ status: boolean
}
} 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`.
*
Data = unknown,
Response = unknown
> extends AbstractWorker<MessagePort, Data, Response> {
+ /** @inheritDoc */
+ protected readonly sendToMainWorker = (
+ message: MessageValue<Response>
+ ): void => {
+ this.port?.postMessage({
+ ...message,
+ workerId: this.id,
+ } satisfies MessageValue<Response>)
+ }
+
/**
* Message port used to communicate with the main worker.
*/
super(isMainThread, parentPort, taskFunctions, opts)
}
+ /**
+ * @inheritDoc
+ */
+ protected handleError (error: Error | string): string {
+ return error as string
+ }
+
+ /** @inheritDoc */
+ protected handleKillMessage (message: MessageValue<Data>): void {
+ super.handleKillMessage(message)
+ this.port?.unref()
+ this.port?.close()
+ }
+
/** @inheritDoc */
protected handleReadyMessage (message: MessageValue<Data>): void {
if (
}
}
- /** @inheritDoc */
- protected handleKillMessage (message: MessageValue<Data>): void {
- super.handleKillMessage(message)
- this.port?.unref()
- this.port?.close()
- }
-
/** @inheritDoc */
protected get id (): number {
return threadId
}
-
- /** @inheritDoc */
- protected readonly sendToMainWorker = (
- message: MessageValue<Response>
- ): void => {
- this.port?.postMessage({
- ...message,
- workerId: this.id,
- } satisfies MessageValue<Response>)
- }
-
- /**
- * @inheritDoc
- */
- protected handleError (error: Error | string): string {
- return error as string
- }
}
+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')
/**
* 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)
/**
/**
* Handler called when a worker is killed.
*/
-export type KillHandler = () => void | Promise<void>
+export type KillHandler = () => Promise<void> | void
/**
* Options for workers.
* @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.
*
* @defaultValue 60000
*/
maxInactiveTime?: number
- /**
- * The function to call when a worker is killed.
- * @defaultValue `() => {}`
- */
- killHandler?: KillHandler
}
+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'
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),
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 },
})
}
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),
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),
expect(
pool.workerChoiceStrategiesContext.getTaskStatisticsRequirements()
).toStrictEqual({
+ elu: {
+ aggregate: true,
+ average: false,
+ median: true,
+ },
runTime: {
aggregate: true,
average: false,
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),
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,
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)
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(
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(
}
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(
'./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(
'./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()
})
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()
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()
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()
}
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()
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(
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(
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()
})
// 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()
})
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()
})
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()
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++
},
).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'")
}
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(),
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
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(),
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(
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()
})
)
expect(dynamicThreadPool.listTaskFunctionsProperties()).toStrictEqual([
{ name: DEFAULT_TASK_NAME },
- { name: 'jsonIntegerSerialization' },
{ name: 'factorial' },
{ name: 'fibonacci' },
+ { name: 'jsonIntegerSerialization' },
])
await expect(
dynamicThreadPool.setDefaultTaskFunction('factorial')
expect(dynamicThreadPool.listTaskFunctionsProperties()).toStrictEqual([
{ name: DEFAULT_TASK_NAME },
{ name: 'factorial' },
- { name: 'jsonIntegerSerialization' },
{ name: 'fibonacci' },
+ { name: 'jsonIntegerSerialization' },
])
await expect(
dynamicThreadPool.setDefaultTaskFunction('fibonacci')
expect(dynamicThreadPool.listTaskFunctionsProperties()).toStrictEqual([
{ name: DEFAULT_TASK_NAME },
{ name: 'fibonacci' },
- { name: 'jsonIntegerSerialization' },
{ name: 'factorial' },
+ { name: 'jsonIntegerSerialization' },
])
await dynamicThreadPool.destroy()
})
)
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')
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)
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,
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)
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)
)
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')
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)
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,
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)
const workerNodeKey = 0
await expect(
pool.sendTaskFunctionOperationToWorker(workerNodeKey, {
+ taskFunction: (() => {}).toString(),
taskFunctionOperation: 'add',
taskFunctionProperties: { name: 'empty' },
- taskFunction: (() => {}).toString(),
})
).resolves.toBe(true)
expect(
)
await expect(
pool.sendTaskFunctionOperationToWorkers({
+ taskFunction: (() => {}).toString(),
taskFunctionOperation: 'add',
taskFunctionProperties: { name: 'empty' },
- taskFunction: (() => {}).toString(),
})
).resolves.toBe(true)
for (const workerNode of pool.workerNodes) {
'./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)
'./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)
-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'
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(
'./tests/worker-files/cluster/testWorker.cjs',
{
enableTasksQueue: true,
+ errorHandler: e => console.error(e),
tasksQueueOptions: {
concurrency: tasksConcurrency,
},
- errorHandler: e => console.error(e),
}
)
emptyPool = new FixedClusterPool(
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(
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(
})
expect(cluster.settings).toMatchObject({
args: ['--use', 'http'],
- silent: true,
exec: workerFilePath,
+ silent: true,
})
await pool.destroy()
})
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,
threadFixedPool.info.maxSize * 2
)
const workerChoiceStrategyOptions = {
+ elu: { median: true },
runTime: { median: true },
waitTime: { median: true },
- elu: { median: true },
weights: {
0: 100,
1: 100,
-import { randomInt } from 'node:crypto'
-
import { expect } from 'expect'
+import { randomInt } from 'node:crypto'
import { CircularBuffer } from '../../../lib/circular-buffer.cjs'
import {
{ workerChoiceStrategy }
)
expect(pool.workerChoiceStrategiesContext.getPolicy()).toStrictEqual({
- dynamicWorkerUsage: false,
dynamicWorkerReady: true,
+ dynamicWorkerUsage: false,
})
await pool.destroy()
pool = new DynamicThreadPool(
{ 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()
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,
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,
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(
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(
{ workerChoiceStrategy }
)
expect(pool.workerChoiceStrategiesContext.getPolicy()).toStrictEqual({
- dynamicWorkerUsage: false,
dynamicWorkerReady: true,
+ dynamicWorkerUsage: false,
})
await pool.destroy()
pool = new DynamicThreadPool(
{ 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()
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,
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,
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(
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(
{ workerChoiceStrategy }
)
expect(pool.workerChoiceStrategiesContext.getPolicy()).toStrictEqual({
- dynamicWorkerUsage: false,
dynamicWorkerReady: true,
+ dynamicWorkerUsage: false,
})
await pool.destroy()
pool = new DynamicThreadPool(
{ 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()
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,
},
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,
},
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(
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(
{ workerChoiceStrategy }
)
expect(pool.workerChoiceStrategiesContext.getPolicy()).toStrictEqual({
- dynamicWorkerUsage: false,
dynamicWorkerReady: true,
+ dynamicWorkerUsage: false,
})
await pool.destroy()
pool = new DynamicThreadPool(
{ 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()
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,
},
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,
},
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(
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(
{ workerChoiceStrategy }
)
expect(pool.workerChoiceStrategiesContext.getPolicy()).toStrictEqual({
- dynamicWorkerUsage: false,
dynamicWorkerReady: true,
+ dynamicWorkerUsage: false,
})
await pool.destroy()
pool = new DynamicThreadPool(
{ 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()
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,
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,
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(
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(
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(
{ workerChoiceStrategy }
)
expect(pool.workerChoiceStrategiesContext.getPolicy()).toStrictEqual({
- dynamicWorkerUsage: false,
dynamicWorkerReady: true,
+ dynamicWorkerUsage: false,
})
await pool.destroy()
pool = new DynamicThreadPool(
{ 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()
expect(
pool.workerChoiceStrategiesContext.getTaskStatisticsRequirements()
).toStrictEqual({
+ elu: {
+ aggregate: false,
+ average: false,
+ median: false,
+ },
runTime: {
aggregate: true,
average: true,
average: true,
median: false,
},
- elu: {
- aggregate: false,
- average: false,
- median: false,
- },
})
await pool.destroy()
pool = new DynamicThreadPool(
expect(
pool.workerChoiceStrategiesContext.getTaskStatisticsRequirements()
).toStrictEqual({
+ elu: {
+ aggregate: false,
+ average: false,
+ median: false,
+ },
runTime: {
aggregate: true,
average: true,
average: true,
median: false,
},
- elu: {
- aggregate: false,
- average: false,
- median: false,
- },
})
// We need to clean up the resources after our test
await pool.destroy()
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(
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(
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(
{ workerChoiceStrategy }
)
expect(pool.workerChoiceStrategiesContext.getPolicy()).toStrictEqual({
- dynamicWorkerUsage: false,
dynamicWorkerReady: true,
+ dynamicWorkerUsage: false,
})
await pool.destroy()
pool = new DynamicThreadPool(
{ 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()
expect(
pool.workerChoiceStrategiesContext.getTaskStatisticsRequirements()
).toStrictEqual({
+ elu: {
+ aggregate: false,
+ average: false,
+ median: false,
+ },
runTime: {
aggregate: true,
average: true,
average: true,
median: false,
},
- elu: {
- aggregate: false,
- average: false,
- median: false,
- },
})
await pool.destroy()
pool = new DynamicThreadPool(
expect(
pool.workerChoiceStrategiesContext.getTaskStatisticsRequirements()
).toStrictEqual({
+ elu: {
+ aggregate: false,
+ average: false,
+ median: false,
+ },
runTime: {
aggregate: true,
average: true,
average: true,
median: false,
},
- elu: {
- aggregate: false,
- average: false,
- median: false,
- },
})
// We need to clean up the resources after our test
await pool.destroy()
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(
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(
-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'
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(
'./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)
'./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)
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(
'./tests/worker-files/thread/testWorker.mjs',
{
enableTasksQueue: true,
+ errorHandler: e => console.error(e),
tasksQueueOptions: {
concurrency: tasksConcurrency,
},
- errorHandler: e => console.error(e),
}
)
emptyPool = new FixedThreadPool(
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(
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(
+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 {
expect(getDefaultTasksQueueOptions(poolMaxSize)).toStrictEqual({
concurrency: 1,
size: Math.pow(poolMaxSize, 2),
- taskStealing: true,
+ tasksFinishedTimeout: 2000,
tasksStealingOnBackPressure: true,
tasksStealingRatio: 0.6,
- tasksFinishedTimeout: 2000,
+ taskStealing: true,
})
})
)
expect(measurementStatistics).toMatchObject({
aggregate: 0.031,
+ average: 0.0010000000474974513,
maximum: 0.02,
minimum: 0.001,
- average: 0.0010000000474974513,
})
updateMeasurementStatistics(
measurementStatistics,
)
expect(measurementStatistics).toMatchObject({
aggregate: 0.034,
+ average: 0.0020000000367872417,
maximum: 0.02,
minimum: 0.001,
- average: 0.0020000000367872417,
})
updateMeasurementStatistics(
measurementStatistics,
expect(measurementStatistics).toMatchObject({
aggregate: 0.04,
maximum: 0.02,
- minimum: 0.001,
median: 0.003000000026077032,
+ minimum: 0.001,
})
updateMeasurementStatistics(
measurementStatistics,
)
expect(measurementStatistics).toMatchObject({
aggregate: 0.05,
+ average: 0.004999999975552782,
maximum: 0.02,
minimum: 0.001,
- average: 0.004999999975552782,
})
})
+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'
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(
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
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
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)
})
const TaskFunctions = {
- jsonIntegerSerialization: 'jsonIntegerSerialization',
- fibonacci: 'fibonacci',
factorial: 'factorial',
+ fibonacci: 'fibonacci',
+ jsonIntegerSerialization: 'jsonIntegerSerialization',
}
module.exports = { TaskFunctions }
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')
}
+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,
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)
'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,
'use strict'
-const { KillBehaviors, ClusterWorker } = require('../../../lib/index.cjs')
+const { ClusterWorker, KillBehaviors } = require('../../../lib/index.cjs')
const {
factorial,
fibonacci,
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,
'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')
/**
*
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,
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,
const worker = new ThreadWorker(() => {})
expect(worker.opts).toStrictEqual({
killBehavior: KillBehaviors.SOFT,
- maxInactiveTime: 60000,
killHandler: EMPTY_FUNCTION,
+ maxInactiveTime: 60000,
})
})
}
const worker = new ClusterWorker(() => {}, {
killBehavior: KillBehaviors.HARD,
- maxInactiveTime: 6000,
killHandler,
+ maxInactiveTime: 6000,
})
expect(worker.opts).toStrictEqual({
killBehavior: KillBehaviors.HARD,
- maxInactiveTime: 6000,
killHandler,
+ maxInactiveTime: 6000,
})
})
)
)
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'"))
})
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,
}
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,
}
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),
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({
}
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),
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(
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),
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({
})
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)
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),
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({
)
}
rmSync(join(dirname(fileURLToPath(import.meta.url)), 'tmp'), {
- recursive: true,
force: true,
+ recursive: true,
})
} catch (e) {
console.error(e)