Merge dependabot/npm_and_yarn/examples/typescript/websocket-server-pool/ws-cluster...
[poolifier.git] / src / pools / abstract-pool.ts
index 428dd9128ca8e26e20d5e766f6cab57b6c04ca2f..c6712d198bc28a9d77c08ae817e5c96c3b9227c6 100644 (file)
@@ -5,7 +5,8 @@ import { type TransferListItem } from 'node:worker_threads'
 import type {
   MessageValue,
   PromiseResponseWrapper,
-  Task
+  Task,
+  Writable
 } from '../utility-types'
 import {
   DEFAULT_TASK_NAME,
@@ -93,6 +94,10 @@ export abstract class AbstractPool<
    * 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.
    */
@@ -141,6 +146,7 @@ export abstract class AbstractPool<
     this.starting = true
     this.startPool()
     this.starting = false
+    this.started = true
 
     this.startTimestamp = performance.now()
   }
@@ -179,7 +185,7 @@ export abstract class AbstractPool<
   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)) {
@@ -285,7 +291,7 @@ export abstract class AbstractPool<
   }
 
   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')
@@ -295,15 +301,39 @@ export abstract class AbstractPool<
       !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`
       )
     }
   }
@@ -620,16 +650,27 @@ export abstract class AbstractPool<
       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
     }
   }
 
@@ -694,6 +735,9 @@ export abstract class AbstractPool<
     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'))
       }
@@ -754,6 +798,7 @@ export abstract class AbstractPool<
       })
     )
     this.emitter?.emit(PoolEvents.destroy, this.info)
+    this.started = false
   }
 
   protected async sendKillMessageToWorker (
@@ -819,10 +864,8 @@ export abstract class AbstractPool<
       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)
     }
   }
 
@@ -846,13 +889,13 @@ export abstract class AbstractPool<
     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)
@@ -885,13 +928,6 @@ export abstract class AbstractPool<
       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
@@ -1023,7 +1059,11 @@ export abstract class AbstractPool<
       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 {
@@ -1115,6 +1155,12 @@ export abstract class AbstractPool<
     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)
+    }
   }
 
   /**
@@ -1144,44 +1190,108 @@ export abstract class AbstractPool<
 
   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)
+        }
       }
     }
   }
@@ -1294,7 +1404,7 @@ export abstract class AbstractPool<
     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) {