UNPKG

@sirmrmarty/n8n-nodes-tmux-orchestrator

Version:

n8n nodes for orchestrating Claude AI agents through tmux sessions

397 lines (346 loc) 10.3 kB
/** * Circuit Breaker Pattern Implementation * Prevents cascading failures in autonomous loops and external service calls */ export enum CircuitState { CLOSED = 'CLOSED', // Normal operation OPEN = 'OPEN', // Circuit is open, calls fail fast HALF_OPEN = 'HALF_OPEN' // Testing if service has recovered } export interface CircuitBreakerConfig { failureThreshold?: number; // Number of failures before opening circuit recoveryTimeout?: number; // Time in ms before attempting recovery successThreshold?: number; // Number of successes needed to close circuit in half-open state monitoringWindow?: number; // Time window for failure counting in ms maxRetryAttempts?: number; // Maximum retry attempts before giving up exponentialBackoff?: boolean; // Use exponential backoff for retries onStateChange?: (state: CircuitState, reason: string) => void; onFailure?: (error: Error) => void; onSuccess?: () => void; } export interface CircuitBreakerStats { state: CircuitState; failures: number; successes: number; totalCalls: number; failureRate: number; lastFailureTime?: number; lastSuccessTime?: number; uptime: number; stateChanges: number; } export interface FailureRecord { timestamp: number; error: string; attempts: number; } export class CircuitBreaker { private state: CircuitState = CircuitState.CLOSED; private failures: number = 0; private successes: number = 0; private totalCalls: number = 0; private lastFailureTime?: number; private lastSuccessTime?: number; private nextAttemptTime: number = 0; private stateChanges: number = 0; private createdAt: number = Date.now(); private config: Required<CircuitBreakerConfig>; private recentFailures: FailureRecord[] = []; private recoveryTimer?: NodeJS.Timeout; constructor(private name: string, config: CircuitBreakerConfig = {}) { this.config = { failureThreshold: 5, recoveryTimeout: 60000, // 1 minute successThreshold: 3, monitoringWindow: 300000, // 5 minutes maxRetryAttempts: 3, exponentialBackoff: true, onStateChange: () => {}, onFailure: () => {}, onSuccess: () => {}, ...config }; } /** * Execute a function with circuit breaker protection */ public async execute<T>( operation: () => Promise<T>, fallback?: () => Promise<T> | T ): Promise<T> { this.totalCalls++; // If circuit is open, fail fast if (this.state === CircuitState.OPEN) { if (Date.now() < this.nextAttemptTime) { const error = new Error(`Circuit breaker ${this.name} is OPEN. Next attempt at ${new Date(this.nextAttemptTime)}`); this.config.onFailure(error); if (fallback) { return await Promise.resolve(fallback()); } throw error; } else { // Transition to half-open to test recovery this.setState(CircuitState.HALF_OPEN, 'Recovery timeout reached'); } } try { const result = await this.executeWithRetry(operation); this.onSuccess(); return result; } catch (error) { this.onFailure(error); if (fallback) { try { return await Promise.resolve(fallback()); } catch (fallbackError) { throw error; // Throw original error if fallback fails } } throw error; } } /** * Execute operation with retry logic */ private async executeWithRetry<T>(operation: () => Promise<T>): Promise<T> { let lastError: Error; let baseDelay = 1000; // Start with 1 second for (let attempt = 1; attempt <= this.config.maxRetryAttempts; attempt++) { try { return await operation(); } catch (error) { lastError = error; // Don't retry on the last attempt if (attempt === this.config.maxRetryAttempts) { break; } // Calculate delay for next attempt const delay = this.config.exponentialBackoff ? baseDelay * Math.pow(2, attempt - 1) + Math.random() * 1000 : baseDelay; await new Promise(resolve => setTimeout(resolve, Math.min(delay, 10000))); } } throw lastError!; } /** * Handle successful operation */ private onSuccess(): void { this.successes++; this.lastSuccessTime = Date.now(); this.config.onSuccess(); if (this.state === CircuitState.HALF_OPEN) { if (this.successes >= this.config.successThreshold) { this.setState(CircuitState.CLOSED, 'Success threshold reached in half-open state'); this.resetCounters(); } } else if (this.state === CircuitState.CLOSED) { // Reset failure count on success in closed state this.failures = 0; this.recentFailures = []; } } /** * Handle failed operation */ private onFailure(error: Error): void { this.failures++; this.lastFailureTime = Date.now(); this.config.onFailure(error); // Record failure for analysis this.recentFailures.push({ timestamp: Date.now(), error: error.message, attempts: this.config.maxRetryAttempts }); // Clean up old failures outside monitoring window const cutoff = Date.now() - this.config.monitoringWindow; this.recentFailures = this.recentFailures.filter(f => f.timestamp > cutoff); // Check if we should open the circuit if (this.state === CircuitState.CLOSED || this.state === CircuitState.HALF_OPEN) { if (this.recentFailures.length >= this.config.failureThreshold) { this.setState(CircuitState.OPEN, `Failure threshold reached: ${this.recentFailures.length} failures`); this.scheduleRecovery(); } } } /** * Change circuit state with logging */ private setState(newState: CircuitState, reason: string): void { const oldState = this.state; this.state = newState; this.stateChanges++; console.log(`Circuit breaker ${this.name}: ${oldState} -> ${newState} (${reason})`); this.config.onStateChange(newState, reason); } /** * Schedule recovery attempt */ private scheduleRecovery(): void { if (this.recoveryTimer) { clearTimeout(this.recoveryTimer); } this.nextAttemptTime = Date.now() + this.config.recoveryTimeout; this.recoveryTimer = setTimeout(() => { if (this.state === CircuitState.OPEN) { this.setState(CircuitState.HALF_OPEN, 'Recovery timer expired'); } }, this.config.recoveryTimeout); } /** * Reset failure/success counters */ private resetCounters(): void { this.failures = 0; this.successes = 0; this.recentFailures = []; } /** * Manually open the circuit (for emergency situations) */ public open(reason: string = 'Manual intervention'): void { this.setState(CircuitState.OPEN, reason); this.scheduleRecovery(); } /** * Manually close the circuit (for recovery scenarios) */ public close(reason: string = 'Manual intervention'): void { this.setState(CircuitState.CLOSED, reason); this.resetCounters(); if (this.recoveryTimer) { clearTimeout(this.recoveryTimer); this.recoveryTimer = undefined; } } /** * Get current circuit breaker statistics */ public getStats(): CircuitBreakerStats { const now = Date.now(); const windowStart = now - this.config.monitoringWindow; const recentFailureCount = this.recentFailures.filter(f => f.timestamp > windowStart).length; const recentCallCount = Math.max(recentFailureCount + this.successes, 1); return { state: this.state, failures: this.failures, successes: this.successes, totalCalls: this.totalCalls, failureRate: recentFailureCount / recentCallCount, lastFailureTime: this.lastFailureTime, lastSuccessTime: this.lastSuccessTime, uptime: now - this.createdAt, stateChanges: this.stateChanges }; } /** * Get recent failure history */ public getFailureHistory(): FailureRecord[] { return [...this.recentFailures]; } /** * Check if circuit is healthy */ public isHealthy(): boolean { const stats = this.getStats(); return stats.state === CircuitState.CLOSED && stats.failureRate < 0.1; // Less than 10% failure rate } /** * Clean up resources */ public destroy(): void { if (this.recoveryTimer) { clearTimeout(this.recoveryTimer); this.recoveryTimer = undefined; } } } /** * Circuit Breaker Registry for managing multiple circuit breakers */ export class CircuitBreakerRegistry { private static instance: CircuitBreakerRegistry; private breakers = new Map<string, CircuitBreaker>(); private constructor() {} public static getInstance(): CircuitBreakerRegistry { if (!CircuitBreakerRegistry.instance) { CircuitBreakerRegistry.instance = new CircuitBreakerRegistry(); } return CircuitBreakerRegistry.instance; } /** * Get or create a circuit breaker */ public getBreaker(name: string, config?: CircuitBreakerConfig): CircuitBreaker { if (!this.breakers.has(name)) { this.breakers.set(name, new CircuitBreaker(name, config)); } return this.breakers.get(name)!; } /** * Remove a circuit breaker */ public removeBreaker(name: string): boolean { const breaker = this.breakers.get(name); if (breaker) { breaker.destroy(); return this.breakers.delete(name); } return false; } /** * Get all circuit breaker statistics */ public getAllStats(): Record<string, CircuitBreakerStats> { const stats: Record<string, CircuitBreakerStats> = {}; for (const [name, breaker] of this.breakers.entries()) { stats[name] = breaker.getStats(); } return stats; } /** * Get health status of all circuit breakers */ public getHealthStatus(): { healthy: string[]; unhealthy: string[] } { const healthy: string[] = []; const unhealthy: string[] = []; for (const [name, breaker] of this.breakers.entries()) { if (breaker.isHealthy()) { healthy.push(name); } else { unhealthy.push(name); } } return { healthy, unhealthy }; } /** * Close all circuit breakers (emergency stop) */ public emergencyStop(): void { for (const [name, breaker] of this.breakers.entries()) { breaker.open(`Emergency stop triggered`); } } /** * Reset all circuit breakers */ public resetAll(): void { for (const [name, breaker] of this.breakers.entries()) { breaker.close(`Global reset triggered`); } } /** * Clean up all circuit breakers */ public destroy(): void { for (const breaker of this.breakers.values()) { breaker.destroy(); } this.breakers.clear(); } } // Global registry instance export const circuitRegistry = CircuitBreakerRegistry.getInstance();