@jonaskahn/maestro
Version:
Job orchestration made simple for Node.js message workflows
449 lines (400 loc) • 15.1 kB
JavaScript
/**
* @license
* Copyleft (c) 2025 Jonas Kahn. All rights are not reserved.
*
* This source code is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*
* Abstract Backpressure Monitor for intelligent rate limiting and consumer lag monitoring
*
* Implements adaptive backoff strategies for message brokers to prevent system overload
* while maintaining optimal throughput across Kafka, RabbitMQ, BullMQ implementations.
* This class must be extended by specific broker implementations.
*/
const logger = require("../services/logger-service");
const TtlConfig = require("../config/ttl-config");
const DEFAULT_VALUES = {
MAX_LAG_THRESHOLD: 100,
ENABLED_RESOURCE_LAG: false,
RATE_LIMIT_THRESHOLD: 100,
EXPONENTIAL_FACTOR: 2,
};
const BackpressureMonitorConfig = TtlConfig.getBackpressureConfig();
const MONITORING_STATES = {
ENABLED: true,
DISABLED: false,
MONITORING_ACTIVE: "active",
MONITORING_PAUSED: "paused",
};
const BACKPRESSURE_LEVELS = {
NONE: "NONE",
LOW: "LOW",
MEDIUM: "MEDIUM",
HIGH: "HIGH",
CRITICAL: "CRITICAL",
};
const ENV_KEYS = {
MAX_LAG: ["MO_BACKPRESSURE_MAX_LAG"],
ENABLED_RESOURCE_LAG: ["MO_BACKPRESSURE_ENABLED_RESOURCE_LAG"],
};
/**
* Abstract Backpressure Monitor Service
*
* Implements adaptive backoff strategies for message brokers to prevent system overload
* while maintaining optimal throughput. Monitors consumer lag and system resources
* to recommend processing delays and pause conditions.
*/
class AbstractMonitorService {
/**
* Creates a backpressure monitor with configuration validation and initialization
*
* @param {Object} config - Monitor configuration object
* @param {number} [config.lagThreshold] - Maximum acceptable message lag
* @param {boolean} [config.enabledResourceLag] - Whether to monitor system resources
* @param {number} [config.checkInterval] - Interval for checking metrics in ms
* @param {number} [config.rateLimitThreshold] - Threshold for rate limiting
* @param {number} [config.cacheTTL] - TTL for cached metrics in ms
* @param {number} [config.initialDelay] - Initial delay for backoff in ms
* @param {number} [config.maxDelay] - Maximum delay for backoff in ms
* @param {number} [config.exponentialFactor] - Factor for exponential backoff
*/
constructor(config) {
if (this.constructor === AbstractMonitorService) {
throw new Error("AbstractMonitorService cannot be instantiated directly");
}
if (!config || typeof config !== "object") {
throw new Error("Backpressure monitor configuration must be an object");
}
this.config = {
lagThreshold:
config.lagThreshold || this.getEnvironmentValueOrDefault(ENV_KEYS.MAX_LAG, DEFAULT_VALUES.MAX_LAG_THRESHOLD),
enabledResourceLag:
config.enabledResourceLag ||
this.getEnvironmentValueOrDefault(ENV_KEYS.ENABLED_RESOURCE_LAG, DEFAULT_VALUES.ENABLED_RESOURCE_LAG),
checkInterval: config.checkInterval || BackpressureMonitorConfig.checkInterval,
rateLimitThreshold: config.rateLimitThreshold || DEFAULT_VALUES.RATE_LIMIT_THRESHOLD,
cacheTTL: config.cacheTTL || BackpressureMonitorConfig.cacheTTL,
initialDelay: config.initialDelay || TtlConfig.getAllTtlValues().BACKOFF_MIN_DELAY,
maxDelay: config.maxDelay || TtlConfig.getAllTtlValues().BACKOFF_MAX_DELAY,
exponentialFactor: config.exponentialFactor || DEFAULT_VALUES.EXPONENTIAL_FACTOR,
};
this._topic = config.topic;
this.isMonitoring = MONITORING_STATES.DISABLED;
this.monitoringInterval = null;
logger.logDebug(`Backpressure monitor configured for broker ${this.getBrokerType()} on topic [ ${this._topic} ]`);
logger.logDebug(
`Backpressure monitor configuration for broker ${this.getBrokerType()} on topic [ ${this._topic} ]`,
{
lagThreshold: this.config.lagThreshold,
checkInterval: this.config.lagMonitorInterval,
rateLimitThreshold: this.config.rateLimitThreshold,
}
);
}
// --- Public API ---
/**
* Returns the broker type identifier
* @returns {string} Broker type ('kafka', 'rabbitmq', 'bullmq')
* @throws {Error} When method is not implemented by subclass
*/
getBrokerType() {
throw new Error("getBrokerType method must be implemented by subclass");
}
/**
* Connects and starts the monitoring service
* @returns {Promise<void>}
*/
async connect() {
await this.startMonitoring();
}
/**
* Disconnects and stops the monitoring service
* @returns {Promise<void>}
*/
async disconnect() {
await this.stopMonitoring();
}
/**
* Starts monitoring for backpressure conditions
* @returns {Promise<void>}
* @throws {Error} When monitoring initialization fails
*/
startMonitoring() {
if (this.isMonitoring === MONITORING_STATES.ENABLED) {
logger.logWarning(`Backpressure monitor for ${this.getBrokerType()} is already active`);
return Promise.resolve();
}
try {
this.monitoringInterval = setInterval(async () => {
await this.performMonitoringCheck();
}, this.config.checkInterval);
this.isMonitoring = MONITORING_STATES.ENABLED;
logger.logDebug(`Backpressure monitoring started for ${this.getBrokerType()}`);
return Promise.resolve();
} catch (error) {
logger.logError(`Failed to start backpressure monitoring for ${this.getBrokerType()}`, error);
throw error;
}
}
/**
* Performs a single monitoring check cycle
* @returns {Promise<void>}
*/
async performMonitoringCheck() {
try {
const metrics = await this.collectCurrentMetrics();
this.lastMetrics = metrics;
this.lastMetricsTime = Date.now();
const backpressureLevel = this.calculateBackpressureLevel(metrics);
if (backpressureLevel !== BACKPRESSURE_LEVELS.NONE) {
logger.logWarning(
`Backpressure detected (${backpressureLevel}): lag=${metrics.totalLag}, ` +
`memory=${metrics.memoryUsage}%, cpu=${metrics.cpuUsage}%`
);
}
} catch (error) {
logger.logWarning("Error during backpressure monitoring check", error);
}
}
/**
* Collects current metrics from all sources
* @returns {Promise<Object>} Combined metrics
*/
async collectCurrentMetrics() {
const lagMetrics = await this.collectLagMetrics();
const resourceMetrics = this.collectResourceMetrics();
return {
...lagMetrics,
...resourceMetrics,
timestamp: Date.now(),
brokerType: this.getBrokerType(),
};
}
/**
* Collects lag metrics from message broker
* @returns {Promise<Object>} Lag metrics
*/
async collectLagMetrics() {
try {
return await this.getConsumerLag();
} catch (error) {
logger.logWarning("Failed to collect consumer lag metrics", error);
return {
totalLag: 0,
maxPartitionLag: 0,
avgLag: 0,
lagThreshold: this.config.lagThreshold,
};
}
}
/**
* Gets consumer lag metrics from the message broker
* @returns {Promise<Object>} Lag metrics object with totalLag, maxPartitionLag, avgLag, and lagThreshold
* @throws {Error} When method is not implemented by subclass
*/
async getConsumerLag() {
throw new Error("getConsumerLag method must be implemented by subclass");
}
/**
* Collects resource metrics from system
* @returns {Promise<Object>} Resource metrics
*/
collectResourceMetrics() {
try {
return this.getResourceMetrics();
} catch (error) {
logger.logWarning("Failed to collect resource metrics", error);
return {
memoryUsage: 0,
cpuUsage: 0,
networkLatency: 0,
};
}
}
/**
* Gets system resource metrics like CPU and memory usage
* @returns {Promise<Object>} Resource metrics with cpuUsage and memoryUsage percentages
* @throws {Error} When method is not implemented by subclass
*/
getResourceMetrics() {
throw new Error("getResourceMetrics method must be implemented by subclass");
}
/**
* Calculates the current backpressure level based on metrics
* @param {Object} metrics - Collected metrics object
* @param {number} metrics.totalLag - Total message lag across all partitions
* @param {number} metrics.cpuUsage - Current CPU usage percentage
* @param {number} metrics.memoryUsage - Current memory usage percentage
* @returns {string} Backpressure level from BACKPRESSURE_LEVELS enum
*/
calculateBackpressureLevel(metrics) {
const lagLevel = this.getLagBackpressureLevel(metrics);
const resourceLevel = this.config.enabledResourceLag
? this.getResourceBackpressureLevel(metrics)
: BACKPRESSURE_LEVELS.NONE;
return this.getHighestBackpressureLevel(lagLevel, resourceLevel);
}
/**
* Determines backpressure level based on consumer lag
* @param {Object} metrics - Metrics object with lag information
* @param {number} metrics.totalLag - Total consumer lag across all partitions
* @param {number} metrics.lagThreshold - Maximum acceptable lag threshold
* @returns {string} Lag-based backpressure level from BACKPRESSURE_LEVELS enum
*/
getLagBackpressureLevel(metrics) {
if (!metrics || !metrics.totalLag) {
return BACKPRESSURE_LEVELS.NONE;
}
const lagRatio = metrics.totalLag / this.config.lagThreshold;
if (lagRatio > 1.0) return BACKPRESSURE_LEVELS.CRITICAL;
if (lagRatio > 0.8) return BACKPRESSURE_LEVELS.HIGH;
if (lagRatio > 0.6) return BACKPRESSURE_LEVELS.MEDIUM;
if (lagRatio > 0.3) return BACKPRESSURE_LEVELS.LOW;
return BACKPRESSURE_LEVELS.NONE;
}
/**
* Determines backpressure level based on system resource usage
* @param {Object} metrics - Metrics object with resource information
* @param {number} metrics.cpuUsage - Current CPU usage percentage (0-100)
* @param {number} metrics.memoryUsage - Current memory usage percentage (0-100)
* @returns {string} Resource-based backpressure level from BACKPRESSURE_LEVELS enum
*/
getResourceBackpressureLevel(metrics) {
if (!this.config.enabledResourceLag || !metrics) {
return BACKPRESSURE_LEVELS.NONE;
}
const maxResourceUsage = Math.max(metrics.memoryUsage || 0, metrics.cpuUsage || 0);
if (maxResourceUsage > 90) return BACKPRESSURE_LEVELS.CRITICAL;
if (maxResourceUsage > 80) return BACKPRESSURE_LEVELS.HIGH;
if (maxResourceUsage > 70) return BACKPRESSURE_LEVELS.MEDIUM;
if (maxResourceUsage > 50) return BACKPRESSURE_LEVELS.LOW;
return BACKPRESSURE_LEVELS.NONE;
}
/**
* Determines the highest backpressure level between two levels
* @param {string} level1 - First backpressure level from BACKPRESSURE_LEVELS enum
* @param {string} level2 - Second backpressure level from BACKPRESSURE_LEVELS enum
* @returns {string} Highest backpressure level from BACKPRESSURE_LEVELS enum
*/
getHighestBackpressureLevel(level1, level2) {
const levelOrder = [
BACKPRESSURE_LEVELS.NONE,
BACKPRESSURE_LEVELS.LOW,
BACKPRESSURE_LEVELS.MEDIUM,
BACKPRESSURE_LEVELS.HIGH,
BACKPRESSURE_LEVELS.CRITICAL,
];
const index1 = levelOrder.indexOf(level1);
const index2 = levelOrder.indexOf(level2);
return levelOrder[Math.max(index1, index2)];
}
/**
* Stops monitoring for backpressure
* @returns {Promise<void>}
* @throws {Error} When monitoring cleanup fails
*/
async stopMonitoring() {
if (this.isMonitoring !== MONITORING_STATES.ENABLED) {
logger.logWarning(`Backpressure monitor for ${this.getBrokerType()} is not active`);
return await Promise.resolve();
}
try {
if (this.monitoringInterval) {
clearInterval(this.monitoringInterval);
this.monitoringInterval = null;
}
this.lastMetrics = null;
this.lastMetricsTime = null;
this.isMonitoring = MONITORING_STATES.DISABLED;
logger.logDebug(`Backpressure monitoring stopped for ${this.getBrokerType()}`);
} catch (error) {
logger.logError(`Error stopping backpressure monitoring for ${this.getBrokerType()}`, error);
throw error;
}
}
/**
* Gets current backpressure monitoring status
* @returns {Promise<Object>} Status object with monitoring state, metrics, and configuration
*/
async getBackpressureStatus() {
try {
const metrics = await this.collectCurrentMetrics();
const backpressureLevel = this.calculateBackpressureLevel(metrics);
const shouldPause =
backpressureLevel === BACKPRESSURE_LEVELS.CRITICAL || backpressureLevel === BACKPRESSURE_LEVELS.HIGH;
const delayMultipliers = {
[BACKPRESSURE_LEVELS.NONE]: 0,
[BACKPRESSURE_LEVELS.LOW]: 1,
[BACKPRESSURE_LEVELS.MEDIUM]: 2,
[BACKPRESSURE_LEVELS.HIGH]: 4,
[BACKPRESSURE_LEVELS.CRITICAL]: 8,
};
const multiplier = delayMultipliers[backpressureLevel] || 0;
const delay = Math.min(this.config.initialDelay * multiplier, this.config.maxDelay);
return {
backpressureLevel,
shouldPause,
recommendedDelay: delay,
metrics: {
lag: {
total: metrics.totalLag,
max: metrics.maxPartitionLag,
average: metrics.avgLag,
threshold: this.config.lagThreshold,
},
resources: {
memory: metrics.memoryUsage,
cpu: metrics.cpuUsage,
network: metrics.networkLatency,
},
},
timestamp: metrics.timestamp,
brokerType: metrics.brokerType,
};
} catch (error) {
logger.logError("Error getting backpressure status", error);
return {
backpressureLevel: BACKPRESSURE_LEVELS.NONE,
shouldPause: false,
recommendedDelay: 0,
error: error.message,
timestamp: Date.now(),
brokerType: this.getBrokerType(),
};
}
}
/**
* Checks if system should pause processing
* @returns {Promise<boolean>} True if should pause
* @throws {Error} When status check fails
*/
async shouldPauseProcessing() {
const status = await this.getBackpressureStatus();
return status.shouldPause === true;
}
/**
* Gets the recommended delay before next operation
* @returns {Promise<number>} Delay in milliseconds
* @throws {Error} When status check fails
*/
async getRecommendedDelay() {
const status = await this.getBackpressureStatus();
return status.recommendedDelay || 0;
}
/**
* Gets environment value with fallback to default
* @param {Array<string>} keys - Environment variable keys to check
* @param {*} defaultValue - Default value if no environment variable found
* @returns {*} Environment value or default
*/
getEnvironmentValueOrDefault(keys, defaultValue) {
for (const key of keys) {
const value = process.env[key];
if (value !== undefined) {
return parseInt(value);
}
}
return defaultValue;
}
}
module.exports = AbstractMonitorService;