import type {
MessageValue,
PromiseResponseWrapper,
- Task
+ Task,
+ Writable
} from '../utility-types'
import {
DEFAULT_TASK_NAME,
* 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()
}
protected checkDynamicPoolSize (min: number, max: number): void {
if (this.type === PoolTypes.dynamic) {
if (max == null) {
- throw new Error(
+ throw new TypeError(
'Cannot instantiate a dynamic pool without specifying the maximum pool size'
)
} else if (!Number.isSafeInteger(max)) {
}
private checkValidTasksQueueOptions (
- tasksQueueOptions: TasksQueueOptions
+ tasksQueueOptions: Writable<TasksQueueOptions>
): void {
if (tasksQueueOptions != null && !isPlainObject(tasksQueueOptions)) {
throw new TypeError('Invalid tasks queue options: must be a plain object')
!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
+ ) {
+ throw new RangeError(
+ `Invalid worker node tasks concurrency: ${tasksQueueOptions.concurrency} is a negative integer or zero`
+ )
+ }
+ if (
+ tasksQueueOptions?.queueMaxSize != null &&
+ tasksQueueOptions?.size != null
) {
throw new Error(
- `Invalid worker tasks concurrency: ${tasksQueueOptions.concurrency} is a negative integer or zero`
+ 'Invalid tasks queue options: cannot specify both queueMaxSize and size'
+ )
+ }
+ if (tasksQueueOptions?.queueMaxSize != null) {
+ tasksQueueOptions.size = tasksQueueOptions.queueMaxSize
+ }
+ if (
+ tasksQueueOptions?.size != null &&
+ !Number.isSafeInteger(tasksQueueOptions.size)
+ ) {
+ throw new TypeError(
+ 'Invalid worker node tasks queue max size: must be an integer'
+ )
+ }
+ if (tasksQueueOptions?.size != null && tasksQueueOptions.size <= 0) {
+ throw new RangeError(
+ `Invalid worker node tasks queue max size: ${tasksQueueOptions.size} is a negative integer or zero`
)
}
}
this.checkValidTasksQueueOptions(tasksQueueOptions)
this.opts.tasksQueueOptions =
this.buildTasksQueueOptions(tasksQueueOptions)
+ this.setTasksQueueMaxSize(this.opts.tasksQueueOptions.size as number)
} else if (this.opts.tasksQueueOptions != null) {
delete this.opts.tasksQueueOptions
}
}
+ private setTasksQueueMaxSize (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
}
}
transferList?: TransferListItem[]
): Promise<Response> {
return await new Promise<Response>((resolve, reject) => {
+ if (!this.started) {
+ reject(new Error('Cannot execute a task on destroyed pool'))
+ }
if (name != null && typeof name !== 'string') {
reject(new TypeError('name argument must be a string'))
}
})
)
this.emitter?.emit(PoolEvents.destroy, this.info)
+ this.started = false
}
protected async sendKillMessageToWorker (
const taskFunctionWorkerUsage = this.workerNodes[
workerNodeKey
].getTaskFunctionWorkerUsage(task.name as string) as WorkerUsage
- if (taskFunctionWorkerUsage != null) {
- ++taskFunctionWorkerUsage.tasks.executing
- this.updateWaitTimeWorkerUsage(taskFunctionWorkerUsage, task)
- }
+ ++taskFunctionWorkerUsage.tasks.executing
+ this.updateWaitTimeWorkerUsage(taskFunctionWorkerUsage, task)
}
}
if (
this.shallUpdateTaskFunctionWorkerUsage(workerNodeKey) &&
this.workerNodes[workerNodeKey].getTaskFunctionWorkerUsage(
- message.taskPerformance?.name ?? DEFAULT_TASK_NAME
+ message.taskPerformance?.name as string
) != null
) {
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
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 {
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)
+ }
}
/**
private redistributeQueuedTasks (workerNodeKey: number): void {
while (this.tasksQueueSize(workerNodeKey) > 0) {
- let targetWorkerNodeKey: number = workerNodeKey
+ let destinationWorkerNodeKey: number = workerNodeKey
let minQueuedTasks = Infinity
let executeTask = false
for (const [workerNodeId, workerNode] of this.workerNodes.entries()) {
- const workerInfo = this.getWorkerInfo(workerNodeId) as WorkerInfo
if (
+ workerNode.info.ready &&
+ workerNodeId !== workerNodeKey &&
+ workerNode.usage.tasks.executing <
+ (this.opts.tasksQueueOptions?.concurrency as number)
+ ) {
+ executeTask = true
+ }
+ if (
+ workerNode.info.ready &&
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
+ destinationWorkerNodeKey = workerNodeId
break
}
if (
+ workerNode.info.ready &&
workerNodeId !== workerNodeKey &&
- workerInfo.ready &&
workerNode.usage.tasks.queued < minQueuedTasks
) {
minQueuedTasks = workerNode.usage.tasks.queued
- targetWorkerNodeKey = workerNodeId
+ destinationWorkerNodeKey = workerNodeId
}
}
+ const task = {
+ ...(this.dequeueTask(workerNodeKey) as Task<Data>),
+ workerId: (this.getWorkerInfo(destinationWorkerNodeKey) as WorkerInfo)
+ .id as number
+ }
if (executeTask) {
- this.executeTask(
- targetWorkerNodeKey,
- this.dequeueTask(workerNodeKey) as Task<Data>
- )
+ this.executeTask(destinationWorkerNodeKey, task)
} else {
- this.enqueueTask(
- targetWorkerNodeKey,
- this.dequeueTask(workerNodeKey) as Task<Data>
- )
+ this.enqueueTask(destinationWorkerNodeKey, task)
+ }
+ }
+ }
+
+ 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
+ )
+ for (const sourceWorkerNode of workerNodes) {
+ if (
+ sourceWorkerNode.info.ready &&
+ sourceWorkerNode.info.id !== workerId &&
+ sourceWorkerNode.usage.tasks.queued > 0
+ ) {
+ const task = {
+ ...(sourceWorkerNode.popTask() as Task<Data>),
+ workerId: destinationWorkerNode.info.id as number
+ }
+ if (
+ destinationWorkerNode.usage.tasks.executing <
+ (this.opts.tasksQueueOptions?.concurrency as number)
+ ) {
+ this.executeTask(destinationWorkerNodeKey, task)
+ } else {
+ this.enqueueTask(destinationWorkerNodeKey, task)
+ }
+ break
+ }
+ }
+ }
+
+ private tasksStealingOnBackPressure (workerId: number): void {
+ 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 (
+ workerNode.info.ready &&
+ workerNode.info.id !== workerId &&
+ sourceWorkerNode.usage.tasks.queued > 0 &&
+ !workerNode.hasBackPressure()
+ ) {
+ const task = {
+ ...(sourceWorkerNode.popTask() as Task<Data>),
+ workerId: workerNode.info.id as number
+ }
+ if (
+ workerNode.usage.tasks.executing <
+ (this.opts.tasksQueueOptions?.concurrency as number)
+ ) {
+ this.executeTask(workerNodeKey, task)
+ } else {
+ this.enqueueTask(workerNodeKey, task)
+ }
}
}
}
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) {