UNPKG

@nasriya/atomix

Version:

Composable helper functions for building reliable systems

235 lines (234 loc) 9.67 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AdaptiveTaskQueue = void 0; const records_utils_1 = __importDefault(require("../../domains/data-types/record/records-utils")); const utils_1 = __importDefault(require("../../domains/utils/utils")); const valueIs_1 = __importDefault(require("../../valueIs")); const TasksQueue_1 = __importDefault(require("./TasksQueue")); /** * AdaptiveTaskQueue extends the base TasksQueue by dynamically adjusting * its concurrency limit based on the observed task addition rate (RPS). * * This class tracks the rate of tasks being added over a rolling time window, * then periodically recalculates and adjusts the concurrency limit to optimize * throughput and resource usage. * * It is especially useful in environments with variable workload, where * adapting concurrency based on real-time demand can improve performance and * efficiency without manual tuning. * * Key features: * - Tracks requests per second (RPS) using a sliding time window. * - Automatically increases or decreases concurrency limit based on RPS. * - Emits concurrency update events to allow external monitoring. * - Configurable time window for rate tracking and debounce delay for recalculations. * * @extends TasksQueue * * @param {AdaptiveTaskQueueOptions} [options] - Configuration options for adaptive behavior. * @param {number} [options.windowDurationMs=1000] - Duration of the rolling window in milliseconds for RPS calculation. * @param {number} [options.recalcDebounce=200] - Debounce delay in milliseconds before concurrency is recalculated. * @param {boolean} [options.autoRun] - Whether to automatically run the queue when tasks are added (inherited from TasksQueue). * @param {number} [options.concurrencyLimit] - Initial concurrency limit (inherited from TasksQueue). * * @example * ```ts * import { AdaptiveTaskQueue } from '@nasriya/atomix/tools'; * * const queue = new AdaptiveTaskQueue({ * windowDurationMs: 500, * recalcDebounce: 100, * autoRun: true, * }); * * queue.onConcurrencyUpdate(newLimit => { * console.log(`Concurrency updated to: ${newLimit}`); * }); * * queue.addTask({ * type: 'task', * action: async () => { * // Your async work here * } * }); * * await queue.untilComplete(); * ``` * * @since 1.0.23 */ class AdaptiveTaskQueue extends TasksQueue_1.default { #_addedSinceLast = 0; #_rateWindow = []; #_adjustRate; #_eventHandlers = { userHandlers: { onConcurrencyUpdate: null }, internalHandlers: { onConcurrencyUpdate: (rps) => { this.#_eventHandlers.userHandlers.onConcurrencyUpdate?.(rps); } }, }; #_configs = Object.seal({ /** RPS calculation window */ windowDurationMs: 1_000, /** Delay before recalculating concurrency */ recalcDebounce: 200, concurrencyLimit: 50 }); #_helpers = { trackRate: () => { const now = Date.now(); this.#_rateWindow.push(now); // Trim old values outside the window while (this.#_rateWindow.length && this.#_rateWindow[0] < now - this.#_configs.windowDurationMs) { this.#_rateWindow.shift(); } if (this.#_rateWindow.length > 100_000) { this.#_rateWindow.splice(0, this.#_rateWindow.length - 100_000); } this.#_addedSinceLast++; }, determineConcurrency: () => { const rps = this.rps; if (rps >= 10_000) { return 1000; } if (rps > 7_000) { return 800; } if (rps > 5_000) { return 500; } if (rps > 1_000) { return 200; } return 100; }, adjustConcurrency: () => { if (this.#_addedSinceLast === 0) { return; } const newLimit = this.#_helpers.determineConcurrency(); if (super.concurrencyLimit !== newLimit) { super.concurrencyLimit = newLimit; this.#_eventHandlers.internalHandlers.onConcurrencyUpdate(newLimit); } this.#_addedSinceLast = 0; } }; constructor(options) { super(options); if (options !== undefined) { if (records_utils_1.default.hasOwnProperty(options, 'windowDurationMs')) { const windowDurationMs = options.windowDurationMs; if (!valueIs_1.default.number(windowDurationMs)) { throw new TypeError(`Expected 'windowDurationMs' to be a number but received ${typeof windowDurationMs}`); } if (!valueIs_1.default.integer(windowDurationMs)) { throw new TypeError(`Expected 'windowDurationMs' to be an integer but received ${typeof windowDurationMs}`); } if (!valueIs_1.default.positiveNumber(windowDurationMs)) { throw new RangeError(`Expected 'windowDurationMs' to be a positive number but received ${typeof windowDurationMs}`); } this.#_configs.windowDurationMs = windowDurationMs; } if (records_utils_1.default.hasOwnProperty(options, 'recalcDebounce')) { const recalcDebounce = options.recalcDebounce; if (!valueIs_1.default.number(recalcDebounce)) { throw new TypeError(`Expected 'recalcDebounce' to be a number but received ${typeof recalcDebounce}`); } if (!valueIs_1.default.integer(recalcDebounce)) { throw new TypeError(`Expected 'recalcDebounce' to be an integer but received ${typeof recalcDebounce}`); } if (!valueIs_1.default.positiveNumber(recalcDebounce)) { throw new RangeError(`Expected 'recalcDebounce' to be a positive number but received ${typeof recalcDebounce}`); } this.#_configs.recalcDebounce = recalcDebounce; } } this.#_adjustRate = utils_1.default.throttle(this.#_helpers.adjustConcurrency, this.#_configs.recalcDebounce); super.concurrencyLimit = this.#_configs.concurrencyLimit; } /** * Adds a task to the adaptive task queue. This method overrides the base * implementation to include rate tracking and dynamic concurrency adjustment * before delegating to the parent method. * * @param task - The task to be added to the queue. * @param options - (Optional) Additional options for task handling, such as * whether to auto-run the queue for this task. * @returns The current instance of the task queue, allowing for method chaining. * @override * @since 1.0.23 */ addTask(task, options) { this.#_helpers.trackRate(); this.#_adjustRate(); return super.addTask(task, options); } /** * Adds multiple tasks to the adaptive task queue. This method overrides the * base implementation to include rate tracking and dynamic concurrency * adjustment before delegating to the parent method. * * @param tasks - An array of tasks to be added to the queue. * @param options - (Optional) Additional options for task handling, such as * whether to auto-run the queue for this task. * @returns The current instance of the task queue, allowing for method chaining. * @override * @since 1.0.23 */ bulkAddTasks(tasks, options) { this.#_helpers.trackRate(); this.#_adjustRate(); return super.bulkAddTasks(tasks, options); } /** * The current requests per second (RPS) rate for this queue. * * This is a rolling average over the past `WINDOW_DURATION` milliseconds. * @readonly * @since 1.0.23 */ get rps() { return this.#_rateWindow.length / (this.#_configs.windowDurationMs / 1000); } /** * The current concurrency limit of the adaptive task queue. * * This value is dynamically adjusted based on the current requests per second (RPS) * to ensure optimal performance while avoiding overload. * * @readonly * @since 1.0.23 */ get concurrencyLimit() { return super.concurrencyLimit; } /** * Registers a callback function to be executed when the concurrency level is updated. * * The provided callback will be called with the current requests per second (RPS) * as its argument whenever the concurrency level is adjusted. * * @param callback - A function that receives the current RPS as an argument. * @throws {TypeError} If the provided callback is not a function. * @throws {RangeError} If the callback function expects more than one parameter. * @since 1.0.23 */ onConcurrencyUpdate(callback) { if (typeof callback !== 'function') { throw new TypeError(`Expected 'callback' to be a function but received ${typeof callback}`); } if (callback.length > 1) { throw new RangeError(`Expected 'callback' to have a maximum of 1 parameter but received ${callback.length}`); } this.#_eventHandlers.userHandlers.onConcurrencyUpdate = callback; } } exports.AdaptiveTaskQueue = AdaptiveTaskQueue; exports.default = AdaptiveTaskQueue;