circuit-breaker.js•12.1 kB
/**
* Circuit breaker pattern implementation for handling data source failures
*/
import { EventEmitter } from 'events';
import { logger } from './logger.js';
import { AppError, ErrorCode, ServiceUnavailableError, TimeoutError } from './errors.js';
export var CircuitState;
(function (CircuitState) {
CircuitState["CLOSED"] = "CLOSED";
CircuitState["OPEN"] = "OPEN";
CircuitState["HALF_OPEN"] = "HALF_OPEN"; // Testing if service is back
})(CircuitState || (CircuitState = {}));
/**
* Circuit breaker implementation for protecting against cascading failures
*/
export class CircuitBreaker extends EventEmitter {
config;
state = CircuitState.CLOSED;
failureCount = 0;
successCount = 0;
lastFailureTime;
lastSuccessTime;
nextAttemptTime;
halfOpenCalls = 0;
totalRequests = 0;
totalFailures = 0;
totalSuccesses = 0;
monitoringWindow = [];
constructor(config) {
super();
this.config = config;
logger.info(`Circuit breaker "${config.name}" initialized`, {
service: 'CircuitBreaker',
circuitName: config.name,
config
});
}
/**
* Execute a function with circuit breaker protection
*/
async execute(fn, context) {
this.totalRequests++;
// Check if circuit is open
if (this.state === CircuitState.OPEN) {
if (this.shouldAttemptReset()) {
this.moveToHalfOpen();
}
else {
const error = new ServiceUnavailableError(this.config.name, `Circuit breaker is OPEN. Next attempt at ${this.nextAttemptTime?.toISOString()}`, { ...context, circuitName: this.config.name, circuitState: this.state });
this.emit('callRejected', { error, stats: this.getStats() });
throw error;
}
}
// Check half-open state limits
if (this.state === CircuitState.HALF_OPEN) {
if (this.halfOpenCalls >= this.config.halfOpenMaxCalls) {
const error = new ServiceUnavailableError(this.config.name, 'Circuit breaker is HALF_OPEN and at call limit', { ...context, circuitName: this.config.name, circuitState: this.state });
this.emit('callRejected', { error, stats: this.getStats() });
throw error;
}
this.halfOpenCalls++;
}
const startTime = Date.now();
try {
// Execute with timeout
const result = await this.executeWithTimeout(fn);
const duration = Date.now() - startTime;
this.onSuccess(duration, context);
return result;
}
catch (error) {
const duration = Date.now() - startTime;
this.onFailure(error, duration, context);
throw error;
}
}
/**
* Execute function with timeout
*/
async executeWithTimeout(fn) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new TimeoutError(this.config.name, this.config.requestTimeout, { circuitName: this.config.name }));
}, this.config.requestTimeout);
fn()
.then(result => {
clearTimeout(timeoutId);
resolve(result);
})
.catch(error => {
clearTimeout(timeoutId);
reject(error);
});
});
}
/**
* Handle successful execution
*/
onSuccess(duration, context) {
this.successCount++;
this.totalSuccesses++;
this.lastSuccessTime = new Date();
this.addToMonitoringWindow(true);
logger.debug(`Circuit breaker "${this.config.name}" - Success`, {
service: 'CircuitBreaker',
circuitName: this.config.name,
duration,
state: this.state,
...context
});
if (this.state === CircuitState.HALF_OPEN) {
// If we've had enough successful calls, close the circuit
if (this.successCount >= this.config.halfOpenMaxCalls) {
this.moveToClosed();
}
}
this.emit('callSuccess', { duration, stats: this.getStats() });
}
/**
* Handle failed execution
*/
onFailure(error, duration, context) {
this.failureCount++;
this.totalFailures++;
this.lastFailureTime = new Date();
this.addToMonitoringWindow(false);
const appError = error instanceof AppError ? error : new AppError({
code: ErrorCode.SERVICE_ERROR,
message: error instanceof Error ? error.message : 'Unknown error',
cause: error instanceof Error ? error : undefined,
context: { ...context, circuitName: this.config.name }
});
logger.warn(`Circuit breaker "${this.config.name}" - Failure`, {
service: 'CircuitBreaker',
circuitName: this.config.name,
duration,
state: this.state,
failureCount: this.failureCount,
error: appError.message,
...context
});
// Check if we should open the circuit
if (this.shouldOpenCircuit()) {
this.moveToOpen();
}
this.emit('callFailure', { error: appError, duration, stats: this.getStats() });
}
/**
* Add result to monitoring window
*/
addToMonitoringWindow(success) {
const now = new Date();
this.monitoringWindow.push({ timestamp: now, success });
// Remove old entries outside monitoring period
const cutoff = new Date(now.getTime() - this.config.monitoringPeriod);
this.monitoringWindow = this.monitoringWindow.filter(entry => entry.timestamp > cutoff);
}
/**
* Check if circuit should be opened
*/
shouldOpenCircuit() {
if (this.state === CircuitState.OPEN) {
return false;
}
// Check failure threshold within monitoring period
const recentFailures = this.monitoringWindow.filter(entry => !entry.success).length;
return recentFailures >= this.config.failureThreshold;
}
/**
* Check if we should attempt to reset from open state
*/
shouldAttemptReset() {
if (!this.nextAttemptTime) {
return true;
}
return new Date() >= this.nextAttemptTime;
}
/**
* Move circuit to CLOSED state
*/
moveToClosed() {
const previousState = this.state;
this.state = CircuitState.CLOSED;
this.failureCount = 0;
this.successCount = 0;
this.halfOpenCalls = 0;
this.nextAttemptTime = undefined;
logger.info(`Circuit breaker "${this.config.name}" moved to CLOSED`, {
service: 'CircuitBreaker',
circuitName: this.config.name,
previousState,
newState: this.state
});
this.emit('stateChange', {
from: previousState,
to: this.state,
stats: this.getStats()
});
}
/**
* Move circuit to OPEN state
*/
moveToOpen() {
const previousState = this.state;
this.state = CircuitState.OPEN;
this.nextAttemptTime = new Date(Date.now() + this.config.recoveryTimeout);
this.halfOpenCalls = 0;
logger.warn(`Circuit breaker "${this.config.name}" moved to OPEN`, {
service: 'CircuitBreaker',
circuitName: this.config.name,
previousState,
newState: this.state,
nextAttemptTime: this.nextAttemptTime.toISOString(),
failureCount: this.failureCount
});
this.emit('stateChange', {
from: previousState,
to: this.state,
stats: this.getStats()
});
}
/**
* Move circuit to HALF_OPEN state
*/
moveToHalfOpen() {
const previousState = this.state;
this.state = CircuitState.HALF_OPEN;
this.halfOpenCalls = 0;
this.successCount = 0;
this.failureCount = 0;
logger.info(`Circuit breaker "${this.config.name}" moved to HALF_OPEN`, {
service: 'CircuitBreaker',
circuitName: this.config.name,
previousState,
newState: this.state
});
this.emit('stateChange', {
from: previousState,
to: this.state,
stats: this.getStats()
});
}
/**
* Get current circuit breaker statistics
*/
getStats() {
return {
state: this.state,
failureCount: this.failureCount,
successCount: this.successCount,
lastFailureTime: this.lastFailureTime,
lastSuccessTime: this.lastSuccessTime,
nextAttemptTime: this.nextAttemptTime,
totalRequests: this.totalRequests,
totalFailures: this.totalFailures,
totalSuccesses: this.totalSuccesses
};
}
/**
* Get current state
*/
getState() {
return this.state;
}
/**
* Force circuit to specific state (for testing)
*/
forceState(state) {
const previousState = this.state;
this.state = state;
if (state === CircuitState.CLOSED) {
this.moveToClosed();
}
else if (state === CircuitState.OPEN) {
this.moveToOpen();
}
else if (state === CircuitState.HALF_OPEN) {
this.moveToHalfOpen();
}
}
/**
* Reset circuit breaker statistics
*/
reset() {
this.state = CircuitState.CLOSED;
this.failureCount = 0;
this.successCount = 0;
this.halfOpenCalls = 0;
this.lastFailureTime = undefined;
this.lastSuccessTime = undefined;
this.nextAttemptTime = undefined;
this.totalRequests = 0;
this.totalFailures = 0;
this.totalSuccesses = 0;
this.monitoringWindow = [];
logger.info(`Circuit breaker "${this.config.name}" reset`, {
service: 'CircuitBreaker',
circuitName: this.config.name
});
this.emit('reset', { stats: this.getStats() });
}
}
/**
* Circuit breaker manager for handling multiple circuit breakers
*/
export class CircuitBreakerManager {
breakers = new Map();
static instance;
/**
* Get singleton instance
*/
static getInstance() {
if (!CircuitBreakerManager.instance) {
CircuitBreakerManager.instance = new CircuitBreakerManager();
}
return CircuitBreakerManager.instance;
}
/**
* Create or get circuit breaker
*/
getCircuitBreaker(config) {
if (!this.breakers.has(config.name)) {
const breaker = new CircuitBreaker(config);
this.breakers.set(config.name, breaker);
// Log state changes
breaker.on('stateChange', ({ from, to, stats }) => {
logger.info(`Circuit breaker state change: ${config.name}`, {
service: 'CircuitBreakerManager',
circuitName: config.name,
from,
to,
stats
});
});
}
return this.breakers.get(config.name);
}
/**
* Get all circuit breaker stats
*/
getAllStats() {
const stats = {};
for (const [name, breaker] of this.breakers) {
stats[name] = breaker.getStats();
}
return stats;
}
/**
* Reset all circuit breakers
*/
resetAll() {
for (const breaker of this.breakers.values()) {
breaker.reset();
}
logger.info('All circuit breakers reset', {
service: 'CircuitBreakerManager',
breakerCount: this.breakers.size
});
}
}
// Export singleton instance
export const circuitBreakerManager = CircuitBreakerManager.getInstance();
//# sourceMappingURL=circuit-breaker.js.map