import { randomUUID } from 'node:crypto'
import { performance } from 'node:perf_hooks'
-import { type TransferListItem } from 'node:worker_threads'
+import type { TransferListItem } from 'node:worker_threads'
+import { EventEmitterAsyncResource } from 'node:events'
import type {
MessageValue,
PromiseResponseWrapper,
max,
median,
min,
- round,
- updateMeasurementStatistics
+ round
} from '../utils'
import { KillBehaviors } from '../worker/worker-options'
import type { TaskFunction } from '../worker/task-functions'
import {
type IPool,
- PoolEmitter,
PoolEvents,
type PoolInfo,
type PoolOptions,
IWorker,
IWorkerNode,
WorkerInfo,
+ WorkerNodeEventDetail,
WorkerType,
WorkerUsage
} from './worker'
import {
checkFilePath,
checkValidTasksQueueOptions,
- checkValidWorkerChoiceStrategy
+ checkValidWorkerChoiceStrategy,
+ updateMeasurementStatistics
} from './utils'
/**
public readonly workerNodes: Array<IWorkerNode<Worker, Data>> = []
/** @inheritDoc */
- public readonly emitter?: PoolEmitter
+ public emitter?: EventEmitterAsyncResource
+
+ /**
+ * Dynamic pool maximum size property placeholder.
+ */
+ protected readonly max?: number
/**
* The task execution response promise map:
Response
>
- /**
- * Dynamic pool maximum size property placeholder.
- */
- protected readonly max?: number
-
/**
* The task functions added at runtime map:
* - `key`: The task function name.
'Cannot start a pool from a worker with the same type as the pool'
)
}
- this.checkNumberOfWorkers(this.numberOfWorkers)
checkFilePath(this.filePath)
+ this.checkNumberOfWorkers(this.numberOfWorkers)
this.checkPoolOptions(this.opts)
this.chooseWorkerNode = this.chooseWorkerNode.bind(this)
this.enqueueTask = this.enqueueTask.bind(this)
if (this.opts.enableEvents === true) {
- this.emitter = new PoolEmitter()
+ this.initializeEventEmitter()
}
this.workerChoiceStrategyContext = new WorkerChoiceStrategyContext<
Worker,
}
}
+ private initializeEventEmitter (): void {
+ this.emitter = new EventEmitterAsyncResource({
+ name: `poolifier:${this.type}-${this.worker}-pool`
+ })
+ }
+
/** @inheritDoc */
public get info (): PoolInfo {
return {
* @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<Response>): void {
+ private checkMessageWorkerId (message: MessageValue<Data | Response>): void {
if (message.workerId == null) {
throw new Error('Worker message received without worker id')
} else if (
private setTaskStealing (): void {
for (const [workerNodeKey] of this.workerNodes.entries()) {
- this.workerNodes[workerNodeKey].onEmptyQueue =
- this.taskStealingOnEmptyQueue.bind(this)
+ this.workerNodes[workerNodeKey].addEventListener(
+ 'emptyqueue',
+ this.handleEmptyQueueEvent
+ )
}
}
private unsetTaskStealing (): void {
for (const [workerNodeKey] of this.workerNodes.entries()) {
- delete this.workerNodes[workerNodeKey].onEmptyQueue
+ this.workerNodes[workerNodeKey].removeEventListener(
+ 'emptyqueue',
+ this.handleEmptyQueueEvent
+ )
}
}
private setTasksStealingOnBackPressure (): void {
for (const [workerNodeKey] of this.workerNodes.entries()) {
- this.workerNodes[workerNodeKey].onBackPressure =
- this.tasksStealingOnBackPressure.bind(this)
+ this.workerNodes[workerNodeKey].addEventListener(
+ 'backpressure',
+ this.handleBackPressureEvent
+ )
}
}
private unsetTasksStealingOnBackPressure (): void {
for (const [workerNodeKey] of this.workerNodes.entries()) {
- delete this.workerNodes[workerNodeKey].onBackPressure
+ this.workerNodes[workerNodeKey].removeEventListener(
+ 'backpressure',
+ this.handleBackPressureEvent
+ )
}
}
message: MessageValue<Data>
): Promise<boolean> {
return await new Promise<boolean>((resolve, reject) => {
- const workerId = this.getWorkerInfo(workerNodeKey).id as number
- this.registerWorkerMessageListener(workerNodeKey, message => {
+ const taskFunctionOperationListener = (
+ message: MessageValue<Response>
+ ): void => {
+ this.checkMessageWorkerId(message)
+ const workerId = this.getWorkerInfo(workerNodeKey).id as number
if (
- message.workerId === workerId &&
- message.taskFunctionOperationStatus === true
+ message.taskFunctionOperationStatus != null &&
+ message.workerId === workerId
) {
- resolve(true)
- } else if (
- message.workerId === workerId &&
- message.taskFunctionOperationStatus === false
- ) {
- reject(
- new Error(
- `Task function operation '${
- message.taskFunctionOperation as string
- }' failed on worker ${message.workerId} with error: '${
- message.workerError?.message as string
- }'`
+ if (message.taskFunctionOperationStatus) {
+ resolve(true)
+ } else if (!message.taskFunctionOperationStatus) {
+ reject(
+ new Error(
+ `Task function operation '${
+ message.taskFunctionOperation as string
+ }' failed on worker ${message.workerId} with error: '${
+ message.workerError?.message as string
+ }'`
+ )
)
+ }
+ this.deregisterWorkerMessageListener(
+ this.getWorkerNodeKeyByWorkerId(message.workerId),
+ taskFunctionOperationListener
)
}
- })
+ }
+ this.registerWorkerMessageListener(
+ workerNodeKey,
+ taskFunctionOperationListener
+ )
this.sendToWorker(workerNodeKey, message)
})
}
message: MessageValue<Data>
): Promise<boolean> {
return await new Promise<boolean>((resolve, reject) => {
- const responsesReceived = new Array<MessageValue<Data | Response>>()
- for (const [workerNodeKey] of this.workerNodes.entries()) {
- this.registerWorkerMessageListener(workerNodeKey, message => {
- if (message.taskFunctionOperationStatus != null) {
- responsesReceived.push(message)
+ 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.length === this.workerNodes.length &&
responsesReceived.every(
message => message.taskFunctionOperationStatus === true
)
) {
resolve(true)
} else if (
- responsesReceived.length === this.workerNodes.length &&
responsesReceived.some(
message => message.taskFunctionOperationStatus === false
)
)
)
}
+ this.deregisterWorkerMessageListener(
+ this.getWorkerNodeKeyByWorkerId(message.workerId),
+ taskFunctionOperationsListener
+ )
}
- })
+ }
+ }
+ for (const [workerNodeKey] of this.workerNodes.entries()) {
+ this.registerWorkerMessageListener(
+ workerNodeKey,
+ taskFunctionOperationsListener
+ )
this.sendToWorker(workerNodeKey, message)
}
})
})
)
this.emitter?.emit(PoolEvents.destroy, this.info)
+ this.emitter?.emitDestroy()
this.started = false
}
workerNodeKey: number
): Promise<void> {
await new Promise<void>((resolve, reject) => {
- this.registerWorkerMessageListener(workerNodeKey, message => {
+ const killMessageListener = (message: MessageValue<Response>): void => {
+ this.checkMessageWorkerId(message)
if (message.kill === 'success') {
resolve()
} else if (message.kill === 'failure') {
reject(
new Error(
- `Worker ${
+ `Kill message handling failed on worker ${
message.workerId as number
- } kill message handling failed`
+ }`
)
)
}
- })
+ }
+ this.registerWorkerMessageListener(workerNodeKey, killMessageListener)
this.sendToWorker(workerNodeKey, { kill: true })
})
}
const workerNodeKey = this.getWorkerNodeKeyByWorker(worker)
const workerInfo = this.getWorkerInfo(workerNodeKey)
workerInfo.ready = false
- this.workerNodes[workerNodeKey].closeChannel()
this.emitter?.emit(PoolEvents.error, error)
+ this.workerNodes[workerNodeKey].closeChannel()
if (
this.started &&
!this.starting &&
protected createAndSetupDynamicWorkerNode (): number {
const workerNodeKey = this.createAndSetupWorkerNode()
this.registerWorkerMessageListener(workerNodeKey, message => {
+ this.checkMessageWorkerId(message)
const localWorkerNodeKey = this.getWorkerNodeKeyByWorkerId(
message.workerId
)
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.
*/
protected afterWorkerNodeSetup (workerNodeKey: number): void {
// Listen to worker messages.
- this.registerWorkerMessageListener(workerNodeKey, this.workerListener())
+ this.registerWorkerMessageListener(
+ workerNodeKey,
+ this.workerMessageListener.bind(this)
+ )
// 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].onEmptyQueue =
- this.taskStealingOnEmptyQueue.bind(this)
+ this.workerNodes[workerNodeKey].addEventListener(
+ 'emptyqueue',
+ this.handleEmptyQueueEvent
+ )
}
if (this.opts.tasksQueueOptions?.tasksStealingOnBackPressure === true) {
- this.workerNodes[workerNodeKey].onBackPressure =
- this.tasksStealingOnBackPressure.bind(this)
+ this.workerNodes[workerNodeKey].addEventListener(
+ 'backpressure',
+ this.handleBackPressureEvent
+ )
}
}
}
}
}
- private taskStealingOnEmptyQueue (workerId: number): void {
- const destinationWorkerNodeKey = this.getWorkerNodeKeyByWorkerId(workerId)
+ private readonly handleEmptyQueueEvent = (
+ event: CustomEvent<WorkerNodeEventDetail>
+ ): void => {
+ const destinationWorkerNodeKey = this.getWorkerNodeKeyByWorkerId(
+ event.detail.workerId
+ )
const workerNodes = this.workerNodes
.slice()
.sort(
const sourceWorkerNode = workerNodes.find(
workerNode =>
workerNode.info.ready &&
- workerNode.info.id !== workerId &&
+ workerNode.info.id !== event.detail.workerId &&
workerNode.usage.tasks.queued > 0
)
if (sourceWorkerNode != null) {
}
}
- private tasksStealingOnBackPressure (workerId: number): void {
+ private readonly handleBackPressureEvent = (
+ event: CustomEvent<WorkerNodeEventDetail>
+ ): void => {
const sizeOffset = 1
if ((this.opts.tasksQueueOptions?.size as number) <= sizeOffset) {
return
}
const sourceWorkerNode =
- this.workerNodes[this.getWorkerNodeKeyByWorkerId(workerId)]
+ this.workerNodes[this.getWorkerNodeKeyByWorkerId(event.detail.workerId)]
const workerNodes = this.workerNodes
.slice()
.sort(
if (
sourceWorkerNode.usage.tasks.queued > 0 &&
workerNode.info.ready &&
- workerNode.info.id !== workerId &&
+ workerNode.info.id !== event.detail.workerId &&
workerNode.usage.tasks.queued <
(this.opts.tasksQueueOptions?.size as number) - sizeOffset
) {
}
/**
- * This method is the listener registered for each worker message.
- *
- * @returns The listener function to execute when a message is received from a worker.
+ * This method is the message listener registered on each worker.
*/
- protected workerListener (): (message: MessageValue<Response>) => void {
- return message => {
- this.checkMessageWorkerId(message)
- if (message.ready != null && message.taskFunctionNames != null) {
- // Worker ready response received from worker
- this.handleWorkerReadyResponse(message)
- } else if (message.taskId != null) {
- // Task execution response received from worker
- this.handleTaskExecutionResponse(message)
- } else if (message.taskFunctionNames != null) {
- // Task function names message received from worker
- this.getWorkerInfo(
- this.getWorkerNodeKeyByWorkerId(message.workerId)
- ).taskFunctionNames = message.taskFunctionNames
- }
+ protected workerMessageListener (message: MessageValue<Response>): void {
+ this.checkMessageWorkerId(message)
+ if (message.ready != null && message.taskFunctionNames != null) {
+ // Worker ready response received from worker
+ this.handleWorkerReadyResponse(message)
+ } else if (message.taskId != null) {
+ // Task execution response received from worker
+ this.handleTaskExecutionResponse(message)
+ } else if (message.taskFunctionNames != null) {
+ // Task function names message received from worker
+ this.getWorkerInfo(
+ this.getWorkerNodeKeyByWorkerId(message.workerId)
+ ).taskFunctionNames = message.taskFunctionNames
}
}