UNPKG

@iflow-mcp/ejmockler-brutalist

Version:

Deploy Claude, Codex & Gemini CLI agents to demolish your work before users do. Real file analysis. Brutal honesty. Now with conversation continuation & intelligent pagination.

463 lines 15.6 kB
import { EventEmitter } from 'events'; import { logger } from '../logger.js'; /** * Circuit breaker states */ export var CircuitState; (function (CircuitState) { CircuitState["CLOSED"] = "closed"; CircuitState["OPEN"] = "open"; CircuitState["HALF_OPEN"] = "half_open"; // Testing if service has recovered })(CircuitState || (CircuitState = {})); /** * Circuit breaker with intelligent fallback handling * * Features: * - Automatic failure detection and recovery * - Configurable thresholds and timeouts * - Multiple fallback strategies with priority * - Real-time statistics and monitoring * - Graceful degradation patterns * - Request queuing during recovery */ export class CircuitBreaker extends EventEmitter { config; name; state = CircuitState.CLOSED; failures = 0; successes = 0; totalRequests = 0; lastFailureTime; lastSuccessTime; recoveryTimer; pendingRequests = new Map(); fallbackStrategies = []; responseTimesWindow = []; requestTimesWindow = []; constructor(config, name = 'CircuitBreaker') { super(); this.config = config; this.name = name; logger.info(`🔌 Circuit breaker '${this.name}' initialized`, { failureThreshold: config.failureThreshold, recoveryTimeout: config.recoveryTimeout, timeout: config.timeout }); } /** * Execute function with circuit breaker protection */ async execute(fn, context) { const requestId = context?.id || `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const startTime = Date.now(); this.totalRequests++; // Check circuit state if (this.state === CircuitState.OPEN) { const error = new Error(`Circuit breaker '${this.name}' is OPEN`); this.emit('requestBlocked', { requestId, reason: 'circuit_open' }); return this.handleFallback(requestId, startTime, error); } // Create request context const requestContext = { id: requestId, startTime, timeout: setTimeout(() => { this.handleTimeout(requestContext); }, this.config.timeout), resolve: () => { }, reject: () => { } }; this.pendingRequests.set(requestId, requestContext); try { logger.debug(`🔌 Circuit breaker executing request ${requestId}`); const result = await Promise.race([ fn(), new Promise((_, reject) => { requestContext.reject = reject; }) ]); this.handleSuccess(requestContext); return result; } catch (error) { return this.handleFailure(requestContext, error); } finally { clearTimeout(requestContext.timeout); this.pendingRequests.delete(requestId); } } /** * Handle successful request */ handleSuccess(context) { const responseTime = Date.now() - context.startTime; this.successes++; this.lastSuccessTime = Date.now(); // Track response time this.responseTimesWindow.push(responseTime); if (this.responseTimesWindow.length > 100) { this.responseTimesWindow.shift(); } // Track request in monitoring window this.trackRequest(true); logger.debug(`✅ Circuit breaker success: ${context.id} (${responseTime}ms)`); // Handle state transitions if (this.state === CircuitState.HALF_OPEN) { if (this.successes >= this.config.successThreshold) { this.transitionToClosed(); } } this.emit('requestSuccess', { requestId: context.id, responseTime, state: this.state }); } /** * Handle failed request */ async handleFailure(context, error) { const responseTime = Date.now() - context.startTime; this.failures++; this.lastFailureTime = Date.now(); // Track request in monitoring window this.trackRequest(false); logger.warn(`❌ Circuit breaker failure: ${context.id} (${responseTime}ms) - ${error.message}`); // Check if we should open the circuit if (this.shouldOpenCircuit()) { this.transitionToOpen(); } this.emit('requestFailure', { requestId: context.id, error: error.message, responseTime, state: this.state }); // Try fallback strategies return this.handleFallback(context.id, context.startTime, error); } /** * Handle request timeout */ handleTimeout(context) { const error = new Error(`Request ${context.id} timed out after ${this.config.timeout}ms`); context.reject(error); } /** * Handle fallback execution */ async handleFallback(requestId, startTime, error) { // Sort strategies by priority const sortedStrategies = [...this.fallbackStrategies].sort((a, b) => a.priority - b.priority); for (const strategy of sortedStrategies) { if (strategy.canHandle(error)) { try { logger.info(`🔄 Executing fallback strategy for ${requestId}: ${strategy.constructor.name}`); const result = await strategy.execute({ id: requestId, startTime, timeout: setTimeout(() => { }, 0), // Dummy timeout resolve: () => { }, reject: () => { } }, error); this.emit('fallbackSuccess', { requestId, strategy: strategy.constructor.name, originalError: error.message }); return result; } catch (fallbackError) { logger.warn(`Fallback strategy failed for ${requestId}:`, fallbackError); continue; } } } // No fallback worked, throw original error this.emit('fallbackExhausted', { requestId, originalError: error.message, triedStrategies: sortedStrategies.length }); throw error; } /** * Check if circuit should be opened */ shouldOpenCircuit() { if (this.state === CircuitState.OPEN) { return false; } // Check failure threshold if (this.failures >= this.config.failureThreshold) { return true; } // Check failure rate within monitoring window const recentRequests = this.getRecentRequests(); if (recentRequests.length >= this.config.minimumRequests) { const recentFailures = recentRequests.filter(r => !r.success).length; const failureRate = recentFailures / recentRequests.length; // Open if failure rate > 50% if (failureRate > 0.5) { logger.warn(`📊 High failure rate detected: ${Math.round(failureRate * 100)}%`); return true; } } return false; } /** * Get recent requests within monitoring window */ getRecentRequests() { const cutoff = Date.now() - this.config.monitoringWindow; return this.requestTimesWindow.filter(r => r.timestamp > cutoff); } /** * Track request in monitoring window */ trackRequest(success) { this.requestTimesWindow.push({ timestamp: Date.now(), success }); // Keep window size manageable if (this.requestTimesWindow.length > 1000) { this.requestTimesWindow.shift(); } } /** * Transition to OPEN state */ transitionToOpen() { if (this.state === CircuitState.OPEN) { return; } logger.warn(`🔴 Circuit breaker '${this.name}' opened (failures: ${this.failures})`); this.state = CircuitState.OPEN; this.successes = 0; // Reset success counter // Set recovery timer this.recoveryTimer = setTimeout(() => { this.transitionToHalfOpen(); }, this.config.recoveryTimeout); this.emit('stateChanged', { state: this.state, reason: 'failure_threshold_exceeded', failures: this.failures }); } /** * Transition to HALF_OPEN state */ transitionToHalfOpen() { logger.info(`🟡 Circuit breaker '${this.name}' half-open (testing recovery)`); this.state = CircuitState.HALF_OPEN; this.successes = 0; // Reset for testing this.emit('stateChanged', { state: this.state, reason: 'recovery_timeout_reached' }); } /** * Transition to CLOSED state */ transitionToClosed() { logger.info(`🟢 Circuit breaker '${this.name}' closed (recovered)`); this.state = CircuitState.CLOSED; this.failures = 0; // Reset failure counter if (this.recoveryTimer) { clearTimeout(this.recoveryTimer); this.recoveryTimer = undefined; } this.emit('stateChanged', { state: this.state, reason: 'recovery_successful', successes: this.successes }); } /** * Add fallback strategy */ addFallbackStrategy(strategy) { this.fallbackStrategies.push(strategy); this.fallbackStrategies.sort((a, b) => a.priority - b.priority); logger.info(`📋 Added fallback strategy: ${strategy.constructor.name} (priority: ${strategy.priority})`); } /** * Remove fallback strategy */ removeFallbackStrategy(strategyClass) { const index = this.fallbackStrategies.findIndex(s => s instanceof strategyClass); if (index >= 0) { const removed = this.fallbackStrategies.splice(index, 1)[0]; logger.info(`🗑️ Removed fallback strategy: ${removed.constructor.name}`); } } /** * Get current circuit breaker statistics */ getStats() { const recentRequests = this.getRecentRequests(); const recentFailures = recentRequests.filter(r => !r.success).length; const failureRate = recentRequests.length > 0 ? recentFailures / recentRequests.length : 0; const averageResponseTime = this.responseTimesWindow.length > 0 ? this.responseTimesWindow.reduce((sum, time) => sum + time, 0) / this.responseTimesWindow.length : 0; return { state: this.state, failures: this.failures, successes: this.successes, totalRequests: this.totalRequests, lastFailureTime: this.lastFailureTime, lastSuccessTime: this.lastSuccessTime, uptime: this.lastSuccessTime ? Date.now() - this.lastSuccessTime : 0, failureRate, averageResponseTime }; } /** * Force circuit state change (for testing) */ forceState(state) { logger.warn(`⚠️ Forcing circuit breaker '${this.name}' to ${state}`); if (this.recoveryTimer) { clearTimeout(this.recoveryTimer); this.recoveryTimer = undefined; } this.state = state; if (state === CircuitState.OPEN) { this.recoveryTimer = setTimeout(() => { this.transitionToHalfOpen(); }, this.config.recoveryTimeout); } this.emit('stateChanged', { state: this.state, reason: 'forced_state_change' }); } /** * Reset circuit breaker to initial state */ reset() { logger.info(`🔄 Resetting circuit breaker '${this.name}'`); if (this.recoveryTimer) { clearTimeout(this.recoveryTimer); this.recoveryTimer = undefined; } this.state = CircuitState.CLOSED; this.failures = 0; this.successes = 0; this.totalRequests = 0; this.lastFailureTime = undefined; this.lastSuccessTime = undefined; this.responseTimesWindow = []; this.requestTimesWindow = []; // Cancel pending requests for (const [requestId, context] of this.pendingRequests) { clearTimeout(context.timeout); context.reject(new Error('Circuit breaker reset')); } this.pendingRequests.clear(); this.emit('reset'); } /** * Cleanup and shutdown */ shutdown() { logger.info(`🛑 Shutting down circuit breaker '${this.name}'`); if (this.recoveryTimer) { clearTimeout(this.recoveryTimer); } // Cancel all pending requests for (const [requestId, context] of this.pendingRequests) { clearTimeout(context.timeout); context.reject(new Error('Circuit breaker shutdown')); } this.removeAllListeners(); } } /** * Default fallback strategies */ /** * Cache fallback - return cached response if available */ export class CachedResponseFallback { cache; priority = 1; constructor(cache) { this.cache = cache; } canHandle(_error) { return true; // Can handle any error if cache is available } async execute(context, _error) { const cached = this.cache.get(context.id); if (cached) { logger.info(`📋 Using cached response for ${context.id}`); return cached; } throw new Error('No cached response available'); } } /** * Degraded service fallback - return simplified response */ export class DegradedServiceFallback { degradedResponse; priority = 2; constructor(degradedResponse) { this.degradedResponse = degradedResponse; } canHandle(_error) { return true; } async execute(context, error) { logger.info(`🔻 Using degraded service response for ${context.id}`); return { ...this.degradedResponse, metadata: { fallback: true, originalError: error.message, timestamp: Date.now() } }; } } /** * Retry with delay fallback */ export class RetryFallback { retryFn; maxRetries; delay; priority = 3; constructor(retryFn, maxRetries = 3, delay = 1000) { this.retryFn = retryFn; this.maxRetries = maxRetries; this.delay = delay; } canHandle(error) { // Only retry on certain error types return !error.message.includes('timeout') && !error.message.includes('validation'); } async execute(context, _error) { for (let i = 0; i < this.maxRetries; i++) { try { logger.info(`🔄 Retry attempt ${i + 1}/${this.maxRetries} for ${context.id}`); await new Promise(resolve => setTimeout(resolve, this.delay * (i + 1))); return await this.retryFn(); } catch (retryError) { if (i === this.maxRetries - 1) { throw retryError; } } } throw new Error('All retry attempts failed'); } } //# sourceMappingURL=circuit-breaker.js.map