feat: randomize startup delays
authorJérôme Benoit <jerome.benoit@sap.com>
Sun, 10 Sep 2023 15:41:26 +0000 (17:41 +0200)
committerJérôme Benoit <jerome.benoit@sap.com>
Sun, 10 Sep 2023 15:41:26 +0000 (17:41 +0200)
Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
src/utils/Utils.ts
src/worker/WorkerDynamicPool.ts
src/worker/WorkerSet.ts
src/worker/WorkerStaticPool.ts
src/worker/WorkerUtils.ts

index 14a6aa69ef3bffef0ea4ee60a9b392f876277122..808ef380d170b2a5a11acfeaa96e7d23b49e1040 100644 (file)
@@ -1,4 +1,4 @@
-import { randomBytes, randomInt, randomUUID } from 'node:crypto';
+import { randomBytes, randomInt, randomUUID, webcrypto } from 'node:crypto';
 import { inspect } from 'node:util';
 
 import {
@@ -354,10 +354,10 @@ export const promiseWithTimeout = async <T>(
 /**
  * Generates a cryptographically secure random number in the [0,1[ range
  *
- * @returns
+ * @returns A number in the [0,1[ range
  */
 export const secureRandom = (): number => {
-  return randomBytes(4).readUInt32LE() / 0x100000000;
+  return webcrypto.getRandomValues(new Uint32Array(1))[0] / 0x100000000;
 };
 
 export const JSONStringifyWithMapSupport = (
index 5824d8adfa21fc44e3c59d22d9c3fd94461e496e..7506a5f1a7a335623a377a477d63a8b4d1cbb10d 100644 (file)
@@ -2,7 +2,7 @@ import { DynamicThreadPool, type PoolEmitter, type PoolInfo } from 'poolifier';
 
 import { WorkerAbstract } from './WorkerAbstract';
 import type { WorkerData, WorkerOptions } from './WorkerTypes';
-import { sleep } from './WorkerUtils';
+import { randomizeDelay, sleep } from './WorkerUtils';
 
 export class WorkerDynamicPool extends WorkerAbstract<WorkerData> {
   private readonly pool: DynamicThreadPool<WorkerData>;
@@ -54,6 +54,6 @@ export class WorkerDynamicPool extends WorkerAbstract<WorkerData> {
     await this.pool.execute(elementData);
     // Start element sequentially to optimize memory at startup
     this.workerOptions.elementStartDelay! > 0 &&
-      (await sleep(this.workerOptions.elementStartDelay!));
+      (await sleep(randomizeDelay(this.workerOptions.elementStartDelay!)));
   }
 }
index 7ddc96c73c27febb8a179a9faa4da6ee01c0615a..82e2e8369083c54b572e5e99c2a99abd25b0d877 100644 (file)
@@ -14,7 +14,7 @@ import {
   type WorkerSetElement,
   WorkerSetEvents,
 } from './WorkerTypes';
-import { sleep } from './WorkerUtils';
+import { randomizeDelay, sleep } from './WorkerUtils';
 
 export class WorkerSet extends WorkerAbstract<WorkerData> {
   public readonly emitter!: EventEmitter;
@@ -76,7 +76,8 @@ export class WorkerSet extends WorkerAbstract<WorkerData> {
   public async start(): Promise<void> {
     this.addWorkerSetElement();
     // Add worker set element sequentially to optimize memory at startup
-    this.workerOptions.workerStartDelay! > 0 && (await sleep(this.workerOptions.workerStartDelay!));
+    this.workerOptions.workerStartDelay! > 0 &&
+      (await sleep(randomizeDelay(this.workerOptions.workerStartDelay!)));
     this.started = true;
   }
 
@@ -111,7 +112,7 @@ export class WorkerSet extends WorkerAbstract<WorkerData> {
     ++workerSetElement.numberOfWorkerElements;
     // Add element sequentially to optimize memory at startup
     if (this.workerOptions.elementStartDelay! > 0) {
-      await sleep(this.workerOptions.elementStartDelay!);
+      await sleep(randomizeDelay(this.workerOptions.elementStartDelay!));
     }
   }
 
@@ -170,7 +171,7 @@ export class WorkerSet extends WorkerAbstract<WorkerData> {
       chosenWorkerSetElement = this.addWorkerSetElement();
       // Add worker set element sequentially to optimize memory at startup
       this.workerOptions.workerStartDelay! > 0 &&
-        (await sleep(this.workerOptions.workerStartDelay!));
+        (await sleep(randomizeDelay(this.workerOptions.workerStartDelay!)));
     }
     return chosenWorkerSetElement;
   }
index acc61b8fad0fadac0fa66a7f7433a9e143db4658..fb70bc43fe1f5b7e8e33fcbf04668993cd2c0ff4 100644 (file)
@@ -2,7 +2,7 @@ import { FixedThreadPool, type PoolEmitter, type PoolInfo } from 'poolifier';
 
 import { WorkerAbstract } from './WorkerAbstract';
 import type { WorkerData, WorkerOptions } from './WorkerTypes';
-import { sleep } from './WorkerUtils';
+import { randomizeDelay, sleep } from './WorkerUtils';
 
 export class WorkerStaticPool extends WorkerAbstract<WorkerData> {
   private readonly pool: FixedThreadPool<WorkerData>;
@@ -53,6 +53,6 @@ export class WorkerStaticPool extends WorkerAbstract<WorkerData> {
     await this.pool.execute(elementData);
     // Start element sequentially to optimize memory at startup
     this.workerOptions.elementStartDelay! > 0 &&
-      (await sleep(this.workerOptions.elementStartDelay!));
+      (await sleep(randomizeDelay(this.workerOptions.elementStartDelay!)));
   }
 }
index e930a47f826a33674c08cd7f1f99ff31f15facbc..1874767370a1a5d438993fd6ec2dba1b9f1318d1 100644 (file)
@@ -1,3 +1,5 @@
+import { webcrypto } from 'node:crypto';
+
 import chalk from 'chalk';
 
 export const sleep = async (milliSeconds: number): Promise<NodeJS.Timeout> => {
@@ -17,3 +19,20 @@ export const defaultExitHandler = (code: number): void => {
 export const defaultErrorHandler = (error: Error): void => {
   console.error(chalk.red('Worker errored: '), error);
 };
+
+export const randomizeDelay = (delay: number): number => {
+  const random = secureRandom();
+  const sign = random < 0.5 ? -1 : 1;
+  const randomSum = delay * 0.2 * random; // 0-20% of the delay
+  return delay + sign * randomSum;
+};
+
+/**
+ * Generates a cryptographically secure random number in the [0,1[ range
+ *
+ * @returns A number in the [0,1[ range
+ * @internal
+ */
+const secureRandom = (): number => {
+  return webcrypto.getRandomValues(new Uint32Array(1))[0] / 0x100000000;
+};