UNPKG

@vfarcic/dot-ai

Version:

AI-powered development productivity platform that enhances software development workflows through intelligent automation and AI-driven assistance

331 lines (330 loc) 11.2 kB
"use strict"; /** * Circuit Breaker Implementation * * Provides resilience for external service calls by preventing cascading failures. * Implements the circuit breaker pattern with three states: * - CLOSED: Normal operation, requests flow through * - OPEN: Failure threshold reached, requests are blocked * - HALF_OPEN: Testing recovery, limited requests allowed */ Object.defineProperty(exports, "__esModule", { value: true }); exports.CircuitBreakerFactory = exports.CircuitBreaker = exports.CircuitOpenError = exports.CircuitState = void 0; const error_handling_1 = require("./error-handling"); /** * Circuit breaker states */ var CircuitState; (function (CircuitState) { CircuitState["CLOSED"] = "closed"; CircuitState["OPEN"] = "open"; CircuitState["HALF_OPEN"] = "half_open"; })(CircuitState || (exports.CircuitState = CircuitState = {})); /** * Error thrown when circuit is open and blocking requests */ class CircuitOpenError extends Error { circuitName; remainingCooldownMs; state; constructor(circuitName, remainingCooldownMs) { super(`Circuit '${circuitName}' is open. Retry after ${Math.ceil(remainingCooldownMs)}ms`); this.name = 'CircuitOpenError'; this.circuitName = circuitName; this.remainingCooldownMs = remainingCooldownMs; this.state = CircuitState.OPEN; } } exports.CircuitOpenError = CircuitOpenError; /** * Circuit Breaker implementation * * Usage: * ```typescript * const breaker = new CircuitBreaker('embedding-api', { failureThreshold: 3 }); * * try { * const result = await breaker.execute(() => fetchFromAPI()); * } catch (error) { * if (error instanceof CircuitOpenError) { * // Circuit is open, handle gracefully * } * } * ``` */ class CircuitBreaker { name; config; logger; state = CircuitState.CLOSED; consecutiveFailures = 0; totalFailures = 0; totalSuccesses = 0; lastFailureTime; lastSuccessTime; openedAt; halfOpenAttempts = 0; lastCircuitOpenLogTime; lastFailureLogTime; suppressedFailureLogCount = 0; /** Minimum interval between failure WARN logs (ms) */ static FAILURE_LOG_INTERVAL_MS = 30000; constructor(name, config, logger) { this.name = name; this.config = { failureThreshold: config?.failureThreshold ?? 3, cooldownPeriodMs: config?.cooldownPeriodMs ?? 30000, halfOpenMaxAttempts: config?.halfOpenMaxAttempts ?? 1, onStateChange: config?.onStateChange, }; this.logger = logger ?? new error_handling_1.ConsoleLogger('CircuitBreaker'); } /** * Execute an operation through the circuit breaker * @throws CircuitOpenError if circuit is open * @throws Original error if operation fails */ async execute(operation) { // Check if we should allow the request if (!this.canExecute()) { const remainingCooldown = this.getRemainingCooldown(); // Only log once per circuit open period to avoid log spam if (!this.lastCircuitOpenLogTime || this.lastCircuitOpenLogTime < this.openedAt) { this.logger.warn(`Circuit '${this.name}' is open, blocking requests`, { remainingCooldownMs: remainingCooldown, willRetryAt: new Date(Date.now() + remainingCooldown).toISOString(), }); this.lastCircuitOpenLogTime = new Date(); } throw new CircuitOpenError(this.name, remainingCooldown); } // Track half-open attempts if (this.state === CircuitState.HALF_OPEN) { this.halfOpenAttempts++; this.logger.info(`Circuit '${this.name}' attempting request in half-open state`, { attempt: this.halfOpenAttempts, maxAttempts: this.config.halfOpenMaxAttempts, }); } try { const result = await operation(); this.recordSuccess(); return result; } catch (error) { this.recordFailure(error); throw error; } } /** * Record a successful operation */ recordSuccess() { this.consecutiveFailures = 0; this.totalSuccesses++; this.lastSuccessTime = new Date(); if (this.state === CircuitState.HALF_OPEN) { // Success in half-open state closes the circuit this.transitionTo(CircuitState.CLOSED); this.halfOpenAttempts = 0; this.logger.info(`Circuit '${this.name}' recovered, transitioning to closed`, { totalSuccesses: this.totalSuccesses, }); } } /** * Record a failed operation */ recordFailure(error) { this.consecutiveFailures++; this.totalFailures++; this.lastFailureTime = new Date(); // Rate-limit failure WARN logs to avoid log spam during sustained outages const now = Date.now(); const shouldLogFailure = !this.lastFailureLogTime || now - this.lastFailureLogTime.getTime() >= CircuitBreaker.FAILURE_LOG_INTERVAL_MS; if (shouldLogFailure) { const logData = { consecutiveFailures: this.consecutiveFailures, threshold: this.config.failureThreshold, error: error?.message, }; if (this.suppressedFailureLogCount > 0) { logData.suppressedLogCount = this.suppressedFailureLogCount; } this.logger.warn(`Circuit '${this.name}' recorded failure`, logData); this.lastFailureLogTime = new Date(now); this.suppressedFailureLogCount = 0; } else { this.suppressedFailureLogCount++; } if (this.state === CircuitState.HALF_OPEN) { // Failure in half-open state opens the circuit again this.transitionTo(CircuitState.OPEN); this.openedAt = new Date(); this.halfOpenAttempts = 0; this.logger.warn(`Circuit '${this.name}' failed in half-open state, reopening`); } else if (this.state === CircuitState.CLOSED && this.consecutiveFailures >= this.config.failureThreshold) { // Threshold reached, open the circuit this.transitionTo(CircuitState.OPEN); this.openedAt = new Date(); this.logger.error(`Circuit '${this.name}' opened after ${this.consecutiveFailures} consecutive failures`); } } /** * Reset the circuit breaker to initial state */ reset() { const previousState = this.state; this.state = CircuitState.CLOSED; this.consecutiveFailures = 0; this.halfOpenAttempts = 0; this.openedAt = undefined; this.lastCircuitOpenLogTime = undefined; this.lastFailureLogTime = undefined; this.suppressedFailureLogCount = 0; if (previousState !== CircuitState.CLOSED) { this.logger.info(`Circuit '${this.name}' manually reset to closed`); this.config.onStateChange?.(previousState, CircuitState.CLOSED, this.name); } } /** * Get the current circuit state */ getState() { // Check if we should transition from OPEN to HALF_OPEN if (this.state === CircuitState.OPEN && this.shouldTransitionToHalfOpen()) { this.transitionTo(CircuitState.HALF_OPEN); this.halfOpenAttempts = 0; this.logger.info(`Circuit '${this.name}' cooldown elapsed, transitioning to half-open`); } return this.state; } /** * Get circuit breaker statistics */ getStats() { return { state: this.getState(), consecutiveFailures: this.consecutiveFailures, totalFailures: this.totalFailures, totalSuccesses: this.totalSuccesses, lastFailureTime: this.lastFailureTime, lastSuccessTime: this.lastSuccessTime, openedAt: this.openedAt, halfOpenAttempts: this.halfOpenAttempts, }; } /** * Check if circuit is currently open (blocking requests) */ isOpen() { return this.getState() === CircuitState.OPEN; } /** * Get the circuit breaker name */ getName() { return this.name; } /** * Check if a request can be executed */ canExecute() { const currentState = this.getState(); switch (currentState) { case CircuitState.CLOSED: return true; case CircuitState.OPEN: return false; case CircuitState.HALF_OPEN: // Allow limited requests in half-open state return this.halfOpenAttempts < this.config.halfOpenMaxAttempts; default: return false; } } /** * Check if cooldown period has elapsed */ shouldTransitionToHalfOpen() { if (!this.openedAt) { return false; } const elapsed = Date.now() - this.openedAt.getTime(); return elapsed >= this.config.cooldownPeriodMs; } /** * Get remaining cooldown time in milliseconds */ getRemainingCooldown() { if (!this.openedAt) { return 0; } const elapsed = Date.now() - this.openedAt.getTime(); const remaining = this.config.cooldownPeriodMs - elapsed; return Math.max(0, remaining); } /** * Transition to a new state */ transitionTo(newState) { const previousState = this.state; this.state = newState; this.config.onStateChange?.(previousState, newState, this.name); } } exports.CircuitBreaker = CircuitBreaker; /** * Factory for creating circuit breakers with shared configuration */ class CircuitBreakerFactory { defaultConfig; logger; breakers = new Map(); constructor(defaultConfig, logger) { this.defaultConfig = defaultConfig ?? {}; this.logger = logger ?? new error_handling_1.ConsoleLogger('CircuitBreakerFactory'); } /** * Get or create a circuit breaker by name */ getOrCreate(name, config) { let breaker = this.breakers.get(name); if (!breaker) { breaker = new CircuitBreaker(name, { ...this.defaultConfig, ...config }, this.logger); this.breakers.set(name, breaker); } return breaker; } /** * Get a circuit breaker by name (returns undefined if not exists) */ get(name) { return this.breakers.get(name); } /** * Reset all circuit breakers */ resetAll() { for (const breaker of this.breakers.values()) { breaker.reset(); } } /** * Get stats for all circuit breakers */ getAllStats() { const stats = {}; for (const [name, breaker] of this.breakers) { stats[name] = breaker.getStats(); } return stats; } } exports.CircuitBreakerFactory = CircuitBreakerFactory;