average,
isKillBehavior,
isPlainObject,
+ max,
median,
+ min,
round,
updateMeasurementStatistics
} from '../utils'
public readonly emitter?: PoolEmitter
/**
- * The task execution response promise map.
- *
+ * The task execution response promise map:
* - `key`: The message id of each submitted task.
* - `value`: An object that contains the worker, the execution response promise resolve and reject callbacks.
*
*/
protected readonly max?: number
- /**
- * Whether the pool is starting or not.
- */
- private readonly starting: boolean
/**
* Whether the pool is started or not.
*/
private started: boolean
+ /**
+ * Whether the pool is starting or not.
+ */
+ private starting: boolean
/**
* The start timestamp of the pool.
*/
this.setupHook()
- this.starting = true
- this.startPool()
+ this.started = false
this.starting = false
- this.started = true
+ if (this.opts.startWorkers === true) {
+ this.start()
+ }
this.startTimestamp = performance.now()
}
private checkPoolOptions (opts: PoolOptions<Worker>): void {
if (isPlainObject(opts)) {
+ this.opts.startWorkers = opts.startWorkers ?? true
+ this.checkValidWorkerChoiceStrategy(
+ opts.workerChoiceStrategy as WorkerChoiceStrategy
+ )
this.opts.workerChoiceStrategy =
opts.workerChoiceStrategy ?? WorkerChoiceStrategies.ROUND_ROBIN
- this.checkValidWorkerChoiceStrategy(this.opts.workerChoiceStrategy)
+ this.checkValidWorkerChoiceStrategyOptions(
+ opts.workerChoiceStrategyOptions as WorkerChoiceStrategyOptions
+ )
this.opts.workerChoiceStrategyOptions = {
...DEFAULT_WORKER_CHOICE_STRATEGY_OPTIONS,
...opts.workerChoiceStrategyOptions
}
- this.checkValidWorkerChoiceStrategyOptions(
- this.opts.workerChoiceStrategyOptions
- )
this.opts.restartWorkerOnError = opts.restartWorkerOnError ?? true
this.opts.enableEvents = opts.enableEvents ?? true
this.opts.enableTasksQueue = opts.enableTasksQueue ?? false
private checkValidWorkerChoiceStrategy (
workerChoiceStrategy: WorkerChoiceStrategy
): void {
- if (!Object.values(WorkerChoiceStrategies).includes(workerChoiceStrategy)) {
+ if (
+ workerChoiceStrategy != null &&
+ !Object.values(WorkerChoiceStrategies).includes(workerChoiceStrategy)
+ ) {
throw new Error(
`Invalid worker choice strategy '${workerChoiceStrategy}'`
)
private checkValidWorkerChoiceStrategyOptions (
workerChoiceStrategyOptions: WorkerChoiceStrategyOptions
): void {
- if (!isPlainObject(workerChoiceStrategyOptions)) {
+ if (
+ workerChoiceStrategyOptions != null &&
+ !isPlainObject(workerChoiceStrategyOptions)
+ ) {
throw new TypeError(
'Invalid worker choice strategy options: must be a plain object'
)
}
if (
- workerChoiceStrategyOptions.retries != null &&
+ workerChoiceStrategyOptions?.retries != null &&
!Number.isSafeInteger(workerChoiceStrategyOptions.retries)
) {
throw new TypeError(
)
}
if (
- workerChoiceStrategyOptions.retries != null &&
+ workerChoiceStrategyOptions?.retries != null &&
workerChoiceStrategyOptions.retries < 0
) {
throw new RangeError(
)
}
if (
- workerChoiceStrategyOptions.weights != null &&
+ workerChoiceStrategyOptions?.weights != null &&
Object.keys(workerChoiceStrategyOptions.weights).length !== this.maxSize
) {
throw new Error(
)
}
if (
- workerChoiceStrategyOptions.measurement != null &&
+ workerChoiceStrategyOptions?.measurement != null &&
!Object.values(Measurements).includes(
workerChoiceStrategyOptions.measurement
)
`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)
}
}
- private startPool (): void {
- while (
- this.workerNodes.reduce(
- (accumulator, workerNode) =>
- !workerNode.info.dynamic ? accumulator + 1 : accumulator,
- 0
- ) < this.numberOfWorkers
- ) {
- this.createAndSetupWorkerNode()
- }
- }
-
/** @inheritDoc */
public get info (): PoolInfo {
return {
version,
type: this.type,
worker: this.worker,
+ started: this.started,
ready: this.ready,
strategy: this.opts.workerChoiceStrategy as WorkerChoiceStrategy,
minSize: this.minSize,
.runTime.aggregate && {
runTime: {
minimum: round(
- Math.min(
+ min(
...this.workerNodes.map(
- (workerNode) => workerNode.usage.runTime?.minimum ?? Infinity
+ workerNode => workerNode.usage.runTime?.minimum ?? Infinity
)
)
),
maximum: round(
- Math.max(
+ max(
...this.workerNodes.map(
- (workerNode) => workerNode.usage.runTime?.maximum ?? -Infinity
+ workerNode => workerNode.usage.runTime?.maximum ?? -Infinity
)
)
),
.waitTime.aggregate && {
waitTime: {
minimum: round(
- Math.min(
+ min(
...this.workerNodes.map(
- (workerNode) => workerNode.usage.waitTime?.minimum ?? Infinity
+ workerNode => workerNode.usage.waitTime?.minimum ?? Infinity
)
)
),
maximum: round(
- Math.max(
+ max(
...this.workerNodes.map(
- (workerNode) => workerNode.usage.waitTime?.maximum ?? -Infinity
+ workerNode => workerNode.usage.waitTime?.maximum ?? -Infinity
)
)
),
*/
private getWorkerNodeKeyByWorker (worker: Worker): number {
return this.workerNodes.findIndex(
- (workerNode) => workerNode.worker === worker
+ workerNode => workerNode.worker === worker
)
}
*/
private getWorkerNodeKeyByWorkerId (workerId: number): number {
return this.workerNodes.findIndex(
- (workerNode) => workerNode.info.id === workerId
+ workerNode => workerNode.info.id === workerId
)
}
tasksQueueOptions?: TasksQueueOptions
): void {
if (this.opts.enableTasksQueue === true && !enable) {
+ this.unsetTaskStealing()
+ this.unsetTasksStealingOnBackPressure()
this.flushTasksQueues()
}
this.opts.enableTasksQueue = enable
this.checkValidTasksQueueOptions(tasksQueueOptions)
this.opts.tasksQueueOptions =
this.buildTasksQueueOptions(tasksQueueOptions)
- this.setTasksQueueMaxSize(this.opts.tasksQueueOptions.size as number)
+ this.setTasksQueueSize(this.opts.tasksQueueOptions.size as number)
+ if (this.opts.tasksQueueOptions.taskStealing === true) {
+ this.setTaskStealing()
+ } else {
+ this.unsetTaskStealing()
+ }
+ if (this.opts.tasksQueueOptions.tasksStealingOnBackPressure === true) {
+ this.setTasksStealingOnBackPressure()
+ } else {
+ this.unsetTasksStealingOnBackPressure()
+ }
} 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 {
...{
size: Math.pow(this.maxSize, 2),
- concurrency: 1
+ concurrency: 1,
+ taskStealing: true,
+ tasksStealingOnBackPressure: true
},
...tasksQueueOptions
}
}
+ private setTasksQueueSize (size: number): void {
+ for (const workerNode of this.workerNodes) {
+ workerNode.tasksQueueBackPressureSize = size
+ }
+ }
+
+ private setTaskStealing (): void {
+ for (const [workerNodeKey] of this.workerNodes.entries()) {
+ this.workerNodes[workerNodeKey].onEmptyQueue =
+ this.taskStealingOnEmptyQueue.bind(this)
+ }
+ }
+
+ private unsetTaskStealing (): void {
+ for (const [workerNodeKey] of this.workerNodes.entries()) {
+ delete this.workerNodes[workerNodeKey].onEmptyQueue
+ }
+ }
+
+ private setTasksStealingOnBackPressure (): void {
+ for (const [workerNodeKey] of this.workerNodes.entries()) {
+ this.workerNodes[workerNodeKey].onBackPressure =
+ this.tasksStealingOnBackPressure.bind(this)
+ }
+ }
+
+ private unsetTasksStealingOnBackPressure (): void {
+ for (const [workerNodeKey] of this.workerNodes.entries()) {
+ delete this.workerNodes[workerNodeKey].onBackPressure
+ }
+ }
+
/**
* Whether the pool is full or not.
*
if (this.opts.enableTasksQueue === true) {
return (
this.workerNodes.findIndex(
- (workerNode) =>
+ workerNode =>
workerNode.info.ready &&
workerNode.usage.tasks.executing <
(this.opts.tasksQueueOptions?.concurrency as number)
) === -1
)
- } else {
- return (
- this.workerNodes.findIndex(
- (workerNode) =>
- workerNode.info.ready && workerNode.usage.tasks.executing === 0
- ) === -1
- )
}
+ return (
+ this.workerNodes.findIndex(
+ workerNode =>
+ workerNode.info.ready && workerNode.usage.tasks.executing === 0
+ ) === -1
+ )
}
/** @inheritDoc */
): Promise<Response> {
return await new Promise<Response>((resolve, reject) => {
if (!this.started) {
- reject(new Error('Cannot execute a task on destroyed pool'))
+ reject(new Error('Cannot execute a task on not started pool'))
return
}
if (name != null && typeof name !== 'string') {
}
const timestamp = performance.now()
const workerNodeKey = this.chooseWorkerNode()
- const workerInfo = this.getWorkerInfo(workerNodeKey) as WorkerInfo
const task: Task<Data> = {
name: name ?? DEFAULT_TASK_NAME,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
data: data ?? ({} as Data),
transferList,
timestamp,
- workerId: workerInfo.id as number,
+ workerId: this.getWorkerInfo(workerNodeKey).id as number,
taskId: randomUUID()
}
this.promiseResponseMap.set(task.taskId as string, {
})
}
+ /** @inheritdoc */
+ public start (): void {
+ this.starting = true
+ while (
+ this.workerNodes.reduce(
+ (accumulator, workerNode) =>
+ !workerNode.info.dynamic ? accumulator + 1 : accumulator,
+ 0
+ ) < this.numberOfWorkers
+ ) {
+ this.createAndSetupWorkerNode()
+ }
+ this.starting = false
+ this.started = true
+ }
+
/** @inheritDoc */
public async destroy (): Promise<void> {
await Promise.all(
workerId: number
): Promise<void> {
await new Promise<void>((resolve, reject) => {
- this.registerWorkerMessageListener(workerNodeKey, (message) => {
+ this.registerWorkerMessageListener(workerNodeKey, message => {
if (message.kill === 'success') {
resolve()
} else if (message.kill === 'failure') {
* @virtual
*/
protected setupHook (): void {
- /** Intentionally empty */
+ /* Intentionally empty */
}
/**
worker.on('online', this.opts.onlineHandler ?? EMPTY_FUNCTION)
worker.on('message', this.opts.messageHandler ?? EMPTY_FUNCTION)
worker.on('error', this.opts.errorHandler ?? EMPTY_FUNCTION)
- worker.on('error', (error) => {
+ 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.started &&
!this.starting &&
- this.started
+ this.opts.restartWorkerOnError === true
) {
if (workerInfo.dynamic) {
this.createAndSetupDynamicWorkerNode()
this.createAndSetupWorkerNode()
}
}
- if (this.opts.enableTasksQueue === true) {
+ if (this.started && this.opts.enableTasksQueue === true) {
this.redistributeQueuedTasks(workerNodeKey)
}
})
*/
protected createAndSetupDynamicWorkerNode (): number {
const workerNodeKey = this.createAndSetupWorkerNode()
- this.registerWorkerMessageListener(workerNodeKey, (message) => {
+ this.registerWorkerMessageListener(workerNodeKey, message => {
const localWorkerNodeKey = this.getWorkerNodeKeyByWorkerId(
message.workerId
)
workerUsage.tasks.executing === 0 &&
this.tasksQueueSize(localWorkerNodeKey) === 0)))
) {
- this.destroyWorkerNode(localWorkerNodeKey).catch((error) => {
+ this.destroyWorkerNode(localWorkerNodeKey).catch(error => {
this.emitter?.emit(PoolEvents.error, error)
})
}
})
- const workerInfo = this.getWorkerInfo(workerNodeKey) as WorkerInfo
+ const workerInfo = this.getWorkerInfo(workerNodeKey)
this.sendToWorker(workerNodeKey, {
checkActive: true,
workerId: workerInfo.id as number
// 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)
+ if (this.opts.tasksQueueOptions?.taskStealing === true) {
+ this.workerNodes[workerNodeKey].onEmptyQueue =
+ this.taskStealingOnEmptyQueue.bind(this)
+ }
+ if (this.opts.tasksQueueOptions?.tasksStealingOnBackPressure === true) {
+ 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 destinationWorkerNodeKey!: number
- let minQueuedTasks = Infinity
- 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
- }
- }
+ 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 (destinationWorkerNodeKey != null) {
- const destinationWorkerNode = this.workerNodes[destinationWorkerNodeKey]
- const task = {
- ...(this.dequeueTask(workerNodeKey) as Task<Data>),
- workerId: destinationWorkerNode.info.id as number
- }
- if (this.shallExecuteTask(destinationWorkerNodeKey)) {
- this.executeTask(destinationWorkerNodeKey, task)
- } else {
- this.enqueueTask(destinationWorkerNodeKey, task)
- }
+ if (this.shallExecuteTask(destinationWorkerNodeKey)) {
+ this.executeTask(destinationWorkerNodeKey, task)
+ } else {
+ this.enqueueTask(destinationWorkerNodeKey, task)
}
}
}
private updateTaskStolenStatisticsWorkerUsage (
workerNodeKey: number,
- workerNode: IWorkerNode<Worker, Data>,
taskName: string
): void {
+ const workerNode = this.workerNodes[workerNodeKey]
if (workerNode?.usage != null) {
++workerNode.usage.tasks.stolen
}
(workerNodeA, workerNodeB) =>
workerNodeB.usage.tasks.queued - workerNodeA.usage.tasks.queued
)
- for (const sourceWorkerNode of workerNodes) {
- if (sourceWorkerNode.usage.tasks.queued === 0) {
- break
+ 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 (
- 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 (this.shallExecuteTask(destinationWorkerNodeKey)) {
- this.executeTask(destinationWorkerNodeKey, task)
- } else {
- this.enqueueTask(destinationWorkerNodeKey, task)
- }
- this.updateTaskStolenStatisticsWorkerUsage(
- destinationWorkerNodeKey,
- destinationWorkerNode,
- task.name as string
- )
- break
+ 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 {
- if ((this.opts.tasksQueueOptions?.size as number) <= 1) {
+ const sizeOffset = 1
+ if ((this.opts.tasksQueueOptions?.size as number) <= sizeOffset) {
return
}
const sourceWorkerNode =
workerNode.info.ready &&
workerNode.info.id !== workerId &&
workerNode.usage.tasks.queued <
- (this.opts.tasksQueueOptions?.size as number) - 1
+ (this.opts.tasksQueueOptions?.size as number) - sizeOffset
) {
const task = {
...(sourceWorkerNode.popTask() as Task<Data>),
}
this.updateTaskStolenStatisticsWorkerUsage(
workerNodeKey,
- workerNode,
task.name as string
)
}
* @returns The listener function to execute when a message is received from a worker.
*/
protected workerListener (): (message: MessageValue<Response>) => void {
- return (message) => {
+ return message => {
this.checkMessageWorkerId(message)
if (message.ready != null && message.taskFunctions != null) {
// Worker ready response received from worker
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) {
- this.emitter.emit(PoolEvents.ready, this.info)
+ if (this.ready) {
+ this.emitter?.emit(PoolEvents.ready, this.info)
}
}
}
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
}
/**
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
}
return (
this.opts.enableTasksQueue === true &&
this.workerNodes.findIndex(
- (workerNode) => !workerNode.hasBackPressure()
+ workerNode => !workerNode.hasBackPressure()
) === -1
)
}