@sirmrmarty/n8n-nodes-tmux-orchestrator
Version:
n8n nodes for orchestrating Claude AI agents through tmux sessions
397 lines (346 loc) • 10.3 kB
text/typescript
/**
* 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();