@nasriya/atomix
Version:
Composable helper functions for building reliable systems
235 lines (234 loc) • 9.67 kB
JavaScript
"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;