DEFAULT_TASK_NAME,
DEFAULT_WORKER_CHOICE_STRATEGY_OPTIONS,
EMPTY_FUNCTION,
+ average,
isKillBehavior,
isPlainObject,
+ max,
median,
+ min,
round,
updateMeasurementStatistics
} from '../utils'
* Whether the pool is starting or not.
*/
private readonly starting: boolean
+ /**
+ * Whether the pool is started or not.
+ */
+ private started: boolean
/**
* The start timestamp of the pool.
*/
this.starting = true
this.startPool()
this.starting = false
+ this.started = true
this.startTimestamp = performance.now()
}
)
}
if (
- workerChoiceStrategyOptions.choiceRetries != null &&
- !Number.isSafeInteger(workerChoiceStrategyOptions.choiceRetries)
+ workerChoiceStrategyOptions.retries != null &&
+ !Number.isSafeInteger(workerChoiceStrategyOptions.retries)
) {
throw new TypeError(
- 'Invalid worker choice strategy options: choice retries must be an integer'
+ 'Invalid worker choice strategy options: retries must be an integer'
)
}
if (
- workerChoiceStrategyOptions.choiceRetries != null &&
- workerChoiceStrategyOptions.choiceRetries <= 0
+ workerChoiceStrategyOptions.retries != null &&
+ workerChoiceStrategyOptions.retries < 0
) {
throw new RangeError(
- `Invalid worker choice strategy options: choice retries '${workerChoiceStrategyOptions.choiceRetries}' must be greater than zero`
+ `Invalid worker choice strategy options: retries '${workerChoiceStrategyOptions.retries}' must be greater or equal than zero`
)
}
if (
}
if (
tasksQueueOptions?.concurrency != null &&
- !Number.isSafeInteger(tasksQueueOptions.concurrency)
+ !Number.isSafeInteger(tasksQueueOptions?.concurrency)
) {
throw new TypeError(
- 'Invalid worker tasks concurrency: must be an integer'
+ 'Invalid worker node tasks concurrency: must be an integer'
)
}
if (
tasksQueueOptions?.concurrency != null &&
- tasksQueueOptions.concurrency <= 0
+ tasksQueueOptions?.concurrency <= 0
) {
throw new RangeError(
- `Invalid worker tasks concurrency: ${tasksQueueOptions.concurrency} is a negative integer or zero`
+ `Invalid worker node tasks concurrency: ${tasksQueueOptions?.concurrency} is a negative integer or zero`
+ )
+ }
+ if (tasksQueueOptions?.queueMaxSize != null) {
+ throw new Error(
+ 'Invalid tasks queue options: queueMaxSize is deprecated, please use size instead'
+ )
+ }
+ if (
+ tasksQueueOptions?.size != null &&
+ !Number.isSafeInteger(tasksQueueOptions?.size)
+ ) {
+ throw new TypeError(
+ 'Invalid worker node tasks queue size: must be an integer'
+ )
+ }
+ if (tasksQueueOptions?.size != null && tasksQueueOptions?.size <= 0) {
+ throw new RangeError(
+ `Invalid worker node tasks queue size: ${tasksQueueOptions?.size} is a negative integer or zero`
)
}
}
...(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,
.runTime.aggregate && {
runTime: {
minimum: round(
- Math.min(
+ min(
...this.workerNodes.map(
(workerNode) => workerNode.usage.runTime?.minimum ?? Infinity
)
)
),
maximum: round(
- Math.max(
+ max(
...this.workerNodes.map(
(workerNode) => workerNode.usage.runTime?.maximum ?? -Infinity
)
)
),
- average: round(
- this.workerNodes.reduce(
- (accumulator, workerNode) =>
- accumulator + (workerNode.usage.runTime?.aggregate ?? 0),
- 0
- ) /
- this.workerNodes.reduce(
- (accumulator, workerNode) =>
- accumulator + (workerNode.usage.tasks?.executed ?? 0),
- 0
+ ...(this.workerChoiceStrategyContext.getTaskStatisticsRequirements()
+ .runTime.average && {
+ average: round(
+ average(
+ this.workerNodes.reduce<number[]>(
+ (accumulator, workerNode) =>
+ accumulator.concat(workerNode.usage.runTime.history),
+ []
+ )
)
- ),
+ )
+ }),
...(this.workerChoiceStrategyContext.getTaskStatisticsRequirements()
.runTime.median && {
median: round(
median(
- this.workerNodes.map(
- (workerNode) => workerNode.usage.runTime?.median ?? 0
+ this.workerNodes.reduce<number[]>(
+ (accumulator, workerNode) =>
+ accumulator.concat(workerNode.usage.runTime.history),
+ []
)
)
)
.waitTime.aggregate && {
waitTime: {
minimum: round(
- Math.min(
+ min(
...this.workerNodes.map(
(workerNode) => workerNode.usage.waitTime?.minimum ?? Infinity
)
)
),
maximum: round(
- Math.max(
+ max(
...this.workerNodes.map(
(workerNode) => workerNode.usage.waitTime?.maximum ?? -Infinity
)
)
),
- average: round(
- this.workerNodes.reduce(
- (accumulator, workerNode) =>
- accumulator + (workerNode.usage.waitTime?.aggregate ?? 0),
- 0
- ) /
- this.workerNodes.reduce(
- (accumulator, workerNode) =>
- accumulator + (workerNode.usage.tasks?.executed ?? 0),
- 0
+ ...(this.workerChoiceStrategyContext.getTaskStatisticsRequirements()
+ .waitTime.average && {
+ average: round(
+ average(
+ this.workerNodes.reduce<number[]>(
+ (accumulator, workerNode) =>
+ accumulator.concat(workerNode.usage.waitTime.history),
+ []
+ )
)
- ),
+ )
+ }),
...(this.workerChoiceStrategyContext.getTaskStatisticsRequirements()
.waitTime.median && {
median: round(
median(
- this.workerNodes.map(
- (workerNode) => workerNode.usage.waitTime?.median ?? 0
+ this.workerNodes.reduce<number[]>(
+ (accumulator, workerNode) =>
+ accumulator.concat(workerNode.usage.waitTime.history),
+ []
)
)
)
this.checkValidTasksQueueOptions(tasksQueueOptions)
this.opts.tasksQueueOptions =
this.buildTasksQueueOptions(tasksQueueOptions)
+ this.setTasksQueueSize(this.opts.tasksQueueOptions.size as number)
} else if (this.opts.tasksQueueOptions != null) {
delete this.opts.tasksQueueOptions
}
}
+ private setTasksQueueSize (size: number): void {
+ for (const workerNode of this.workerNodes) {
+ workerNode.tasksQueueBackPressureSize = size
+ }
+ }
+
private buildTasksQueueOptions (
tasksQueueOptions: TasksQueueOptions
): TasksQueueOptions {
return {
- concurrency: tasksQueueOptions?.concurrency ?? 1
+ ...{
+ size: Math.pow(this.maxSize, 2),
+ concurrency: 1
+ },
+ ...tasksQueueOptions
}
}
return []
}
+ private shallExecuteTask (workerNodeKey: number): boolean {
+ return (
+ this.tasksQueueSize(workerNodeKey) === 0 &&
+ this.workerNodes[workerNodeKey].usage.tasks.executing <
+ (this.opts.tasksQueueOptions?.concurrency as number)
+ )
+ }
+
/** @inheritDoc */
public async execute (
data?: Data,
transferList?: TransferListItem[]
): Promise<Response> {
return await new Promise<Response>((resolve, reject) => {
+ if (!this.started) {
+ reject(new Error('Cannot execute a task on destroyed pool'))
+ return
+ }
if (name != null && typeof name !== 'string') {
reject(new TypeError('name argument must be a string'))
+ return
}
if (
name != null &&
name.trim().length === 0
) {
reject(new TypeError('name argument must not be an empty string'))
+ return
}
if (transferList != null && !Array.isArray(transferList)) {
reject(new TypeError('transferList argument must be an array'))
+ return
}
const timestamp = performance.now()
const workerNodeKey = this.chooseWorkerNode()
- const workerInfo = this.getWorkerInfo(workerNodeKey) as WorkerInfo
- if (
- name != null &&
- Array.isArray(workerInfo.taskFunctions) &&
- !workerInfo.taskFunctions.includes(name)
- ) {
- reject(
- new Error(`Task function '${name}' is not registered in the pool`)
- )
- }
+ const workerInfo = this.getWorkerInfo(workerNodeKey)
const task: Task<Data> = {
name: name ?? DEFAULT_TASK_NAME,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
if (
this.opts.enableTasksQueue === false ||
(this.opts.enableTasksQueue === true &&
- this.workerNodes[workerNodeKey].usage.tasks.executing <
- (this.opts.tasksQueueOptions?.concurrency as number))
+ this.shallExecuteTask(workerNodeKey))
) {
this.executeTask(workerNodeKey, task)
} else {
})
)
this.emitter?.emit(PoolEvents.destroy, this.info)
+ this.started = false
}
protected async sendKillMessageToWorker (
* @virtual
*/
protected setupHook (): void {
- // Intentionally empty
+ /* Intentionally empty */
}
/**
const taskFunctionWorkerUsage = this.workerNodes[
workerNodeKey
].getTaskFunctionWorkerUsage(
- message.taskPerformance?.name ?? DEFAULT_TASK_NAME
+ message.taskPerformance?.name as string
) as WorkerUsage
this.updateTaskStatisticsWorkerUsage(taskFunctionWorkerUsage, message)
this.updateRunTimeWorkerUsage(taskFunctionWorkerUsage, message)
workerTaskStatistics.executing > 0
) {
--workerTaskStatistics.executing
- } else if (
- workerTaskStatistics.executing != null &&
- workerTaskStatistics.executing < 0
- ) {
- throw new Error(
- 'Worker usage statistic for tasks executing cannot be negative'
- )
}
if (message.taskError == null) {
++workerTaskStatistics.executed
workerUsage: WorkerUsage,
message: MessageValue<Response>
): void {
+ if (message.taskError != null) {
+ return
+ }
updateMeasurementStatistics(
workerUsage.runTime,
this.workerChoiceStrategyContext.getTaskStatisticsRequirements().runTime,
- message.taskPerformance?.runTime ?? 0,
- workerUsage.tasks.executed
+ message.taskPerformance?.runTime ?? 0
)
}
updateMeasurementStatistics(
workerUsage.waitTime,
this.workerChoiceStrategyContext.getTaskStatisticsRequirements().waitTime,
- taskWaitTime,
- workerUsage.tasks.executed
+ taskWaitTime
)
}
workerUsage: WorkerUsage,
message: MessageValue<Response>
): void {
+ if (message.taskError != null) {
+ return
+ }
const eluTaskStatisticsRequirements: MeasurementStatisticsRequirements =
this.workerChoiceStrategyContext.getTaskStatisticsRequirements().elu
updateMeasurementStatistics(
workerUsage.elu.active,
eluTaskStatisticsRequirements,
- message.taskPerformance?.elu?.active ?? 0,
- workerUsage.tasks.executed
+ message.taskPerformance?.elu?.active ?? 0
)
updateMeasurementStatistics(
workerUsage.elu.idle,
eluTaskStatisticsRequirements,
- message.taskPerformance?.elu?.idle ?? 0,
- workerUsage.tasks.executed
+ message.taskPerformance?.elu?.idle ?? 0
)
if (eluTaskStatisticsRequirements.aggregate) {
if (message.taskPerformance?.elu != null) {
worker.on('error', this.opts.errorHandler ?? EMPTY_FUNCTION)
worker.on('error', (error) => {
const workerNodeKey = this.getWorkerNodeKeyByWorker(worker)
- const workerInfo = this.getWorkerInfo(workerNodeKey) as WorkerInfo
+ const workerInfo = this.getWorkerInfo(workerNodeKey)
workerInfo.ready = false
this.workerNodes[workerNodeKey].closeChannel()
this.emitter?.emit(PoolEvents.error, error)
- if (this.opts.restartWorkerOnError === true && !this.starting) {
+ if (
+ this.opts.restartWorkerOnError === true &&
+ !this.starting &&
+ this.started
+ ) {
if (workerInfo.dynamic) {
this.createAndSetupDynamicWorkerNode()
} else {
})
}
})
- const workerInfo = this.getWorkerInfo(workerNodeKey) as WorkerInfo
+ const workerInfo = this.getWorkerInfo(workerNodeKey)
this.sendToWorker(workerNodeKey, {
checkActive: true,
workerId: workerInfo.id as number
this.sendStartupMessageToWorker(workerNodeKey)
// Send the statistics message to worker.
this.sendStatisticsMessageToWorker(workerNodeKey)
+ if (this.opts.enableTasksQueue === true) {
+ this.workerNodes[workerNodeKey].onEmptyQueue =
+ this.taskStealingOnEmptyQueue.bind(this)
+ this.workerNodes[workerNodeKey].onBackPressure =
+ this.tasksStealingOnBackPressure.bind(this)
+ }
}
/**
elu: this.workerChoiceStrategyContext.getTaskStatisticsRequirements()
.elu.aggregate
},
- workerId: (this.getWorkerInfo(workerNodeKey) as WorkerInfo).id as number
+ workerId: this.getWorkerInfo(workerNodeKey).id as number
})
}
private redistributeQueuedTasks (workerNodeKey: number): void {
while (this.tasksQueueSize(workerNodeKey) > 0) {
- let targetWorkerNodeKey: number = workerNodeKey
- let minQueuedTasks = Infinity
- let executeTask = false
- for (const [workerNodeId, workerNode] of this.workerNodes.entries()) {
- const workerInfo = this.getWorkerInfo(workerNodeId) as WorkerInfo
- if (
- workerNodeId !== workerNodeKey &&
- workerInfo.ready &&
- workerNode.usage.tasks.queued === 0
- ) {
- if (
- this.workerNodes[workerNodeId].usage.tasks.executing <
- (this.opts.tasksQueueOptions?.concurrency as number)
- ) {
- executeTask = true
- }
- targetWorkerNodeKey = workerNodeId
- break
- }
- if (
- workerNodeId !== workerNodeKey &&
- workerInfo.ready &&
- workerNode.usage.tasks.queued < minQueuedTasks
- ) {
- minQueuedTasks = workerNode.usage.tasks.queued
- targetWorkerNodeKey = workerNodeId
- }
+ const destinationWorkerNodeKey = this.workerNodes.reduce(
+ (minWorkerNodeKey, workerNode, workerNodeKey, workerNodes) => {
+ return workerNode.info.ready &&
+ workerNode.usage.tasks.queued <
+ workerNodes[minWorkerNodeKey].usage.tasks.queued
+ ? workerNodeKey
+ : minWorkerNodeKey
+ },
+ 0
+ )
+ const destinationWorkerNode = this.workerNodes[destinationWorkerNodeKey]
+ const task = {
+ ...(this.dequeueTask(workerNodeKey) as Task<Data>),
+ workerId: destinationWorkerNode.info.id as number
}
- if (executeTask) {
- this.executeTask(
- targetWorkerNodeKey,
- this.dequeueTask(workerNodeKey) as Task<Data>
- )
+ if (this.shallExecuteTask(destinationWorkerNodeKey)) {
+ this.executeTask(destinationWorkerNodeKey, task)
} else {
- this.enqueueTask(
- targetWorkerNodeKey,
- this.dequeueTask(workerNodeKey) as Task<Data>
+ this.enqueueTask(destinationWorkerNodeKey, task)
+ }
+ }
+ }
+
+ private updateTaskStolenStatisticsWorkerUsage (
+ workerNodeKey: number,
+ taskName: string
+ ): void {
+ const workerNode = this.workerNodes[workerNodeKey]
+ if (workerNode?.usage != null) {
+ ++workerNode.usage.tasks.stolen
+ }
+ if (
+ this.shallUpdateTaskFunctionWorkerUsage(workerNodeKey) &&
+ workerNode.getTaskFunctionWorkerUsage(taskName) != null
+ ) {
+ const taskFunctionWorkerUsage = workerNode.getTaskFunctionWorkerUsage(
+ taskName
+ ) as WorkerUsage
+ ++taskFunctionWorkerUsage.tasks.stolen
+ }
+ }
+
+ private taskStealingOnEmptyQueue (workerId: number): void {
+ const destinationWorkerNodeKey = this.getWorkerNodeKeyByWorkerId(workerId)
+ const destinationWorkerNode = this.workerNodes[destinationWorkerNodeKey]
+ const workerNodes = this.workerNodes
+ .slice()
+ .sort(
+ (workerNodeA, workerNodeB) =>
+ workerNodeB.usage.tasks.queued - workerNodeA.usage.tasks.queued
+ )
+ const sourceWorkerNode = workerNodes.find(
+ (workerNode) =>
+ workerNode.info.ready &&
+ workerNode.info.id !== workerId &&
+ workerNode.usage.tasks.queued > 0
+ )
+ if (sourceWorkerNode != null) {
+ const task = {
+ ...(sourceWorkerNode.popTask() as Task<Data>),
+ workerId: destinationWorkerNode.info.id as number
+ }
+ if (this.shallExecuteTask(destinationWorkerNodeKey)) {
+ this.executeTask(destinationWorkerNodeKey, task)
+ } else {
+ this.enqueueTask(destinationWorkerNodeKey, task)
+ }
+ this.updateTaskStolenStatisticsWorkerUsage(
+ destinationWorkerNodeKey,
+ task.name as string
+ )
+ }
+ }
+
+ private tasksStealingOnBackPressure (workerId: number): void {
+ const sizeOffset = 1
+ if ((this.opts.tasksQueueOptions?.size as number) <= sizeOffset) {
+ return
+ }
+ 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.ready &&
+ workerNode.info.id !== workerId &&
+ workerNode.usage.tasks.queued <
+ (this.opts.tasksQueueOptions?.size as number) - sizeOffset
+ ) {
+ const task = {
+ ...(sourceWorkerNode.popTask() as Task<Data>),
+ workerId: workerNode.info.id as number
+ }
+ if (this.shallExecuteTask(workerNodeKey)) {
+ this.executeTask(workerNodeKey, task)
+ } else {
+ this.enqueueTask(workerNodeKey, task)
+ }
+ this.updateTaskStolenStatisticsWorkerUsage(
+ workerNodeKey,
+ task.name as string
)
}
}
this.handleTaskExecutionResponse(message)
} else if (message.taskFunctions != null) {
// Task functions message received from worker
- (
- this.getWorkerInfo(
- this.getWorkerNodeKeyByWorkerId(message.workerId)
- ) as WorkerInfo
+ this.getWorkerInfo(
+ this.getWorkerNodeKeyByWorkerId(message.workerId)
).taskFunctions = message.taskFunctions
}
}
}
const workerInfo = this.getWorkerInfo(
this.getWorkerNodeKeyByWorkerId(message.workerId)
- ) as WorkerInfo
+ )
workerInfo.ready = message.ready as boolean
workerInfo.taskFunctions = message.taskFunctions
if (this.emitter != null && this.ready) {
}
const workerNodeKey = promiseResponse.workerNodeKey
this.afterTaskExecutionHook(workerNodeKey, message)
+ this.workerChoiceStrategyContext.update(workerNodeKey)
this.promiseResponseMap.delete(taskId as string)
if (
this.opts.enableTasksQueue === true &&
this.dequeueTask(workerNodeKey) as Task<Data>
)
}
- this.workerChoiceStrategyContext.update(workerNodeKey)
}
}
* @param workerNodeKey - The worker node key.
* @returns The worker information.
*/
- protected getWorkerInfo (workerNodeKey: number): WorkerInfo | undefined {
- return this.workerNodes[workerNodeKey]?.info
+ protected getWorkerInfo (workerNodeKey: number): WorkerInfo {
+ return this.workerNodes[workerNodeKey].info
}
/**
private addWorkerNode (worker: Worker): number {
const workerNode = new WorkerNode<Worker, Data>(
worker,
- this.worker,
- this.maxSize
+ this.opts.tasksQueueOptions?.size ?? Math.pow(this.maxSize, 2)
)
// Flag the worker node as ready at pool startup.
if (this.starting) {
this.workerNodes.push(workerNode)
const workerNodeKey = this.getWorkerNodeKeyByWorker(worker)
if (workerNodeKey === -1) {
- throw new Error('Worker node added not found')
+ throw new Error('Worker added not found in worker nodes')
}
return workerNodeKey
}