Files
p2ns/includes/infrastructure/circuit_breaker.js
T
2025-12-17 20:05:50 -05:00

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,
};