const { logWarn, logInfo, logError } = require('./logger'); const { secondsToMs } = require('./utils'); /** * Circuit Breaker implementation for preventing cascading failures */ class CircuitBreaker { constructor(options = {}) { this.failureThreshold = options.failureThreshold || 5; this.resetTimeout = options.resetTimeout || secondsToMs(60); // 1 minute this.monitoringWindow = options.monitoringWindow || secondsToMs(60); // 1 minute this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN this.failureCount = 0; this.successCount = 0; this.lastFailureTime = null; this.failures = []; // Track failures with timestamps } /** * Execute a function with circuit breaker protection * @param {Function} fn - Function to execute * @param {string} context - Context for logging * @returns {Promise} - Result of function */ async execute(fn, context = 'Unknown') { // Clean old failures this.cleanOldFailures(); // Check circuit state if (this.state === 'OPEN') { if (Date.now() - this.lastFailureTime > this.resetTimeout) { this.state = 'HALF_OPEN'; this.successCount = 0; logInfo('CircuitBreaker', `Circuit breaker for ${context} moved to HALF_OPEN`); } else { throw new Error(`Circuit breaker is OPEN for ${context}`); } } try { const result = await fn(); // On success if (this.state === 'HALF_OPEN') { this.successCount++; if (this.successCount >= 2) { this.state = 'CLOSED'; this.failureCount = 0; this.successCount = 0; logInfo('CircuitBreaker', `Circuit breaker for ${context} moved to CLOSED`); } } else { // Reset failure count on success in CLOSED state this.failureCount = 0; } return result; } catch (err) { this.recordFailure(); if (this.state === 'HALF_OPEN') { this.state = 'OPEN'; this.lastFailureTime = Date.now(); logWarn('CircuitBreaker', `Circuit breaker for ${context} moved to OPEN (failed in HALF_OPEN)`); } else if (this.failureCount >= this.failureThreshold) { this.state = 'OPEN'; this.lastFailureTime = Date.now(); logWarn('CircuitBreaker', `Circuit breaker for ${context} moved to OPEN (threshold reached)`); } throw err; } } recordFailure() { this.failureCount++; this.failures.push(Date.now()); } cleanOldFailures() { const cutoff = Date.now() - this.monitoringWindow; this.failures = this.failures.filter(time => time > cutoff); this.failureCount = this.failures.length; } reset() { this.state = 'CLOSED'; this.failureCount = 0; this.successCount = 0; this.failures = []; this.lastFailureTime = null; } getState() { return { state: this.state, failureCount: this.failureCount, lastFailureTime: this.lastFailureTime }; } } // Global circuit breakers const circuitBreakers = new Map(); /** * Get or create a circuit breaker for a service * @param {string} serviceName - Name of the service * @param {object} options - Circuit breaker options * @returns {CircuitBreaker} */ function getCircuitBreaker(serviceName, options = {}) { if (!circuitBreakers.has(serviceName)) { circuitBreakers.set(serviceName, new CircuitBreaker(options)); } return circuitBreakers.get(serviceName); } module.exports = { CircuitBreaker, getCircuitBreaker, };