124 lines
3.4 KiB
JavaScript
124 lines
3.4 KiB
JavaScript
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,
|
|
};
|
|
|