UNPKG

deepsource-mcp-server

Version:
329 lines 10.8 kB
/** * @fileoverview Circuit breaker implementation for fault tolerance * This module implements the circuit breaker pattern to prevent cascade failures. */ import { createLogger } from '../logging/logger.js'; const logger = createLogger('CircuitBreaker'); /** * Circuit breaker states * @enum * @public */ export var CircuitState; (function (CircuitState) { /** Normal operation - requests pass through */ CircuitState["CLOSED"] = "closed"; /** Circuit is open - requests fail immediately */ CircuitState["OPEN"] = "open"; /** Testing if service has recovered */ CircuitState["HALF_OPEN"] = "half-open"; })(CircuitState || (CircuitState = {})); /** * Default circuit breaker configuration from environment * @private */ const DEFAULT_CONFIG = { failureThreshold: parseInt(process.env.CIRCUIT_BREAKER_THRESHOLD || '5', 10), failureWindow: 60000, // 1 minute recoveryTimeout: parseInt(process.env.CIRCUIT_BREAKER_TIMEOUT_MS || '30000', 10), successThreshold: 3, halfOpenMaxAttempts: 5, }; /** * Circuit breaker implementation * @class * @public */ export class CircuitBreaker { state = CircuitState.CLOSED; failures = []; successes = []; lastStateChange = Date.now(); halfOpenAttempts = 0; totalRequests = 0; config; name; /** * Creates a new circuit breaker instance * @param name The name of this circuit breaker (for logging) * @param config Optional configuration overrides */ constructor(name, config = {}) { this.name = name; this.config = { ...DEFAULT_CONFIG, ...config }; logger.debug('Circuit breaker created', { name, config: this.config }); } /** * Check if a request should be allowed through * @returns True if the request can proceed * @public */ canRequest() { this.cleanupOldEntries(); switch (this.state) { case CircuitState.CLOSED: return true; case CircuitState.OPEN: { // Check if it's time to transition to half-open const timeSinceOpen = Date.now() - this.lastStateChange; if (timeSinceOpen >= this.config.recoveryTimeout) { this.transitionTo(CircuitState.HALF_OPEN); this.halfOpenAttempts = 1; // Count this request as the first attempt return true; } logger.debug('Circuit breaker is open, rejecting request', { name: this.name, timeSinceOpen, recoveryTimeout: this.config.recoveryTimeout, }); return false; } case CircuitState.HALF_OPEN: // Allow limited test requests if (this.halfOpenAttempts < this.config.halfOpenMaxAttempts) { this.halfOpenAttempts++; return true; } logger.debug('Circuit breaker half-open limit reached', { name: this.name, attempts: this.halfOpenAttempts, max: this.config.halfOpenMaxAttempts, }); return false; default: return false; } } /** * Record a successful request * @public */ recordSuccess() { this.totalRequests++; this.successes.push(Date.now()); this.cleanupOldEntries(); switch (this.state) { case CircuitState.HALF_OPEN: { // Check if we have enough successes to close the circuit const recentSuccesses = this.getRecentSuccessCount(); logger.debug('Success in half-open state', { name: this.name, recentSuccesses, threshold: this.config.successThreshold, }); if (recentSuccesses >= this.config.successThreshold) { this.transitionTo(CircuitState.CLOSED); } break; } case CircuitState.CLOSED: // Normal operation, nothing special to do break; case CircuitState.OPEN: // Shouldn't happen, but handle gracefully logger.warn('Success recorded while circuit is open', { name: this.name }); break; default: // Log unexpected state logger.error('Unexpected circuit state in recordSuccess', { name: this.name, state: this.state, }); break; } } /** * Record a failed request * @public */ recordFailure() { this.totalRequests++; this.failures.push(Date.now()); this.cleanupOldEntries(); switch (this.state) { case CircuitState.CLOSED: { // Check if we've exceeded the failure threshold const recentFailures = this.getRecentFailureCount(); logger.debug('Failure in closed state', { name: this.name, recentFailures, threshold: this.config.failureThreshold, }); if (recentFailures >= this.config.failureThreshold) { this.transitionTo(CircuitState.OPEN); } break; } case CircuitState.HALF_OPEN: // Any failure in half-open state reopens the circuit logger.debug('Failure in half-open state, reopening circuit', { name: this.name }); this.transitionTo(CircuitState.OPEN); break; case CircuitState.OPEN: // Already open, nothing to do break; default: // Log unexpected state logger.error('Unexpected circuit state in recordFailure', { name: this.name, state: this.state, }); break; } } /** * Get current circuit breaker statistics * @returns The current statistics * @public */ getStats() { this.cleanupOldEntries(); const failureCount = this.getRecentFailureCount(); const successCount = this.getRecentSuccessCount(); const successRate = successCount + failureCount > 0 ? (successCount / (successCount + failureCount)) * 100 : 0; const stats = { state: this.state, failureCount, successCount, totalRequests: this.totalRequests, successRate: Math.round(successRate * 100) / 100, }; if (this.state === CircuitState.OPEN) { stats.lastOpenTime = this.lastStateChange; } if (this.state === CircuitState.CLOSED) { stats.lastCloseTime = this.lastStateChange; } return stats; } /** * Reset the circuit breaker to closed state * @public */ reset() { this.state = CircuitState.CLOSED; this.failures = []; this.successes = []; this.halfOpenAttempts = 0; this.lastStateChange = Date.now(); logger.info('Circuit breaker reset', { name: this.name }); } /** * Get the current state * @returns The current circuit state * @public */ getState() { return this.state; } /** * Transition to a new state * @param newState The new state to transition to * @private */ transitionTo(newState) { const oldState = this.state; this.state = newState; this.lastStateChange = Date.now(); if (newState === CircuitState.HALF_OPEN) { this.halfOpenAttempts = 0; } logger.info('Circuit breaker state transition', { name: this.name, from: oldState, to: newState, }); } /** * Clean up old entries outside the time window * @private */ cleanupOldEntries() { const cutoff = Date.now() - this.config.failureWindow; this.failures = this.failures.filter((time) => time > cutoff); this.successes = this.successes.filter((time) => time > cutoff); } /** * Get the count of recent failures within the time window * @returns The number of recent failures * @private */ getRecentFailureCount() { const cutoff = Date.now() - this.config.failureWindow; return this.failures.filter((time) => time > cutoff).length; } /** * Get the count of recent successes within the time window * @returns The number of recent successes * @private */ getRecentSuccessCount() { const cutoff = Date.now() - this.config.failureWindow; return this.successes.filter((time) => time > cutoff).length; } } /** * Circuit breaker manager for managing multiple circuit breakers * @class * @public */ export class CircuitBreakerManager { static instance; breakers = new Map(); /** * Get the singleton instance * @returns The circuit breaker manager instance * @public */ static getInstance() { if (!CircuitBreakerManager.instance) { CircuitBreakerManager.instance = new CircuitBreakerManager(); } return CircuitBreakerManager.instance; } /** * Get or create a circuit breaker for an endpoint * @param endpoint The endpoint name * @param config Optional configuration overrides * @returns The circuit breaker instance * @public */ getBreaker(endpoint, config) { let breaker = this.breakers.get(endpoint); if (!breaker) { breaker = new CircuitBreaker(endpoint, config); this.breakers.set(endpoint, breaker); } return breaker; } /** * Get statistics for all circuit breakers * @returns Map of endpoint to statistics * @public */ getAllStats() { const stats = new Map(); for (const [endpoint, breaker] of this.breakers) { stats.set(endpoint, breaker.getStats()); } return stats; } /** * Reset all circuit breakers * @public */ resetAll() { for (const breaker of this.breakers.values()) { breaker.reset(); } logger.info('All circuit breakers reset'); } /** * Clear all circuit breakers (for testing) * @public */ clear() { this.breakers.clear(); } } //# sourceMappingURL=circuit-breaker.js.map