import type {
MessageValue,
PromiseResponseWrapper,
- Task
+ Task,
+ Writable
} from '../utility-types'
import {
DEFAULT_TASK_NAME,
DEFAULT_WORKER_CHOICE_STRATEGY_OPTIONS,
EMPTY_FUNCTION,
+ average,
isKillBehavior,
isPlainObject,
median,
}
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')
}
if (
tasksQueueOptions?.queueMaxSize != null &&
- !Number.isSafeInteger(tasksQueueOptions.queueMaxSize)
+ tasksQueueOptions?.size != null
) {
- throw new TypeError(
- 'Invalid worker node tasks queue max size: must be an integer'
+ throw new Error(
+ 'Invalid tasks queue options: cannot specify both queueMaxSize and size'
)
}
+ if (tasksQueueOptions?.queueMaxSize != null) {
+ tasksQueueOptions.size = tasksQueueOptions.queueMaxSize
+ }
if (
- tasksQueueOptions?.queueMaxSize != null &&
- tasksQueueOptions.queueMaxSize <= 0
+ 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.queueMaxSize} is a negative integer or zero`
+ `Invalid worker node tasks queue max size: ${tasksQueueOptions.size} is a negative integer or zero`
)
}
}
)
)
),
- 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),
+ []
)
)
)
)
)
),
- 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.setTasksQueueMaxSize(
- this.opts.tasksQueueOptions.queueMaxSize as number
- )
+ this.setTasksQueueMaxSize(this.opts.tasksQueueOptions.size as number)
} else if (this.opts.tasksQueueOptions != null) {
delete this.opts.tasksQueueOptions
}
}
- private setTasksQueueMaxSize (queueMaxSize: number): void {
+ private setTasksQueueMaxSize (size: number): void {
for (const workerNode of this.workerNodes) {
- workerNode.tasksQueueBackPressureSize = queueMaxSize
+ workerNode.tasksQueueBackPressureSize = size
}
}
): TasksQueueOptions {
return {
...{
- queueMaxSize: Math.pow(this.maxSize, 2),
+ size: Math.pow(this.maxSize, 2),
concurrency: 1
},
...tasksQueueOptions
if (
this.opts.enableTasksQueue === false ||
(this.opts.enableTasksQueue === true &&
+ this.tasksQueueSize(workerNodeKey) === 0 &&
this.workerNodes[workerNodeKey].usage.tasks.executing <
(this.opts.tasksQueueOptions?.concurrency as number))
) {
* @virtual
*/
protected setupHook (): void {
- // Intentionally empty
+ /** Intentionally empty */
}
/**
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) {
// 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 {
- const workerNodes = this.workerNodes.filter(
- (_, workerNodeId) => workerNodeId !== workerNodeKey
- )
while (this.tasksQueueSize(workerNodeKey) > 0) {
- let targetWorkerNodeKey: number = workerNodeKey
+ let destinationWorkerNodeKey!: number
let minQueuedTasks = Infinity
- let executeTask = false
- for (const [workerNodeId, workerNode] of workerNodes.entries()) {
+ for (const [workerNodeId, workerNode] of this.workerNodes.entries()) {
+ if (workerNode.info.ready && workerNodeId !== workerNodeKey) {
+ if (workerNode.usage.tasks.queued === 0) {
+ destinationWorkerNodeKey = workerNodeId
+ break
+ }
+ if (workerNode.usage.tasks.queued < minQueuedTasks) {
+ minQueuedTasks = workerNode.usage.tasks.queued
+ destinationWorkerNodeKey = workerNodeId
+ }
+ }
+ }
+ if (destinationWorkerNodeKey != null) {
+ const destinationWorkerNode = this.workerNodes[destinationWorkerNodeKey]
+ const task = {
+ ...(this.dequeueTask(workerNodeKey) as Task<Data>),
+ workerId: destinationWorkerNode.info.id as number
+ }
if (
- this.workerNodes[workerNodeId].usage.tasks.executing <
- (this.opts.tasksQueueOptions?.concurrency as number)
+ this.tasksQueueSize(destinationWorkerNodeKey) === 0 &&
+ destinationWorkerNode.usage.tasks.executing <
+ (this.opts.tasksQueueOptions?.concurrency as number)
) {
- executeTask = true
+ this.executeTask(destinationWorkerNodeKey, task)
+ } else {
+ this.enqueueTask(destinationWorkerNodeKey, task)
}
- if (workerNode.info.ready && workerNode.usage.tasks.queued === 0) {
- targetWorkerNodeKey = workerNodeId
- break
+ }
+ }
+ }
+
+ 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.usage.tasks.queued === 0) {
+ break
+ }
+ 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 (
- workerNode.info.ready &&
- workerNode.usage.tasks.queued < minQueuedTasks
+ this.tasksQueueSize(destinationWorkerNodeKey) === 0 &&
+ destinationWorkerNode.usage.tasks.executing <
+ (this.opts.tasksQueueOptions?.concurrency as number)
) {
- minQueuedTasks = workerNode.usage.tasks.queued
- targetWorkerNodeKey = workerNodeId
+ this.executeTask(destinationWorkerNodeKey, task)
+ } else {
+ this.enqueueTask(destinationWorkerNodeKey, task)
}
- }
- if (executeTask) {
- this.executeTask(
- targetWorkerNodeKey,
- this.dequeueTask(workerNodeKey) as Task<Data>
- )
- } else {
- this.enqueueTask(
- targetWorkerNodeKey,
- this.dequeueTask(workerNodeKey) as Task<Data>
- )
+ break
}
}
}
const sourceWorkerNode =
this.workerNodes[this.getWorkerNodeKeyByWorkerId(workerId)]
const workerNodes = this.workerNodes
- .filter((workerNode) => workerNode.info.id !== workerId)
+ .slice()
.sort(
(workerNodeA, workerNodeB) =>
workerNodeA.usage.tasks.queued - workerNodeB.usage.tasks.queued
)
for (const [workerNodeKey, workerNode] of workerNodes.entries()) {
if (
- workerNode.info.ready &&
sourceWorkerNode.usage.tasks.queued > 0 &&
- !workerNode.hasBackPressure()
+ workerNode.info.ready &&
+ workerNode.info.id !== workerId &&
+ workerNode.usage.tasks.queued <
+ (this.opts.tasksQueueOptions?.size as number) - 1
) {
+ const task = {
+ ...(sourceWorkerNode.popTask() as Task<Data>),
+ workerId: workerNode.info.id as number
+ }
if (
+ this.tasksQueueSize(workerNodeKey) === 0 &&
workerNode.usage.tasks.executing <
- (this.opts.tasksQueueOptions?.concurrency as number)
+ (this.opts.tasksQueueOptions?.concurrency as number)
) {
- this.executeTask(
- workerNodeKey,
- sourceWorkerNode.popTask() as Task<Data>
- )
+ this.executeTask(workerNodeKey, task)
} else {
- this.enqueueTask(
- workerNodeKey,
- sourceWorkerNode.popTask() as Task<Data>
- )
+ this.enqueueTask(workerNodeKey, task)
}
}
}
const workerNode = new WorkerNode<Worker, Data>(
worker,
this.worker,
- this.opts.tasksQueueOptions?.queueMaxSize ?? Math.pow(this.maxSize, 2)
+ this.opts.tasksQueueOptions?.size ?? Math.pow(this.maxSize, 2)
)
// Flag the worker node as ready at pool startup.
if (this.starting) {