UNPKG

@sailboat-computer/resilience

Version:

Enhanced resilience patterns for sailboat computer v3 with marine-specific adaptations

540 lines (469 loc) 15.3 kB
/** * Marine-specific circuit breaker implementation */ import { CircuitBreakerConfig, CircuitBreakerMetrics, ResilienceResult, ResilienceEvent, MarineEnvironmentStatus } from '../types'; import { CircuitBreakerState, CircuitBreakerStateType, MarineFailureType, MarineFailureTypeType, OperationalContext, OperationalContextType } from '../types/marine-constants'; /** * Enhanced circuit breaker with marine-specific adaptations */ export class MarineCircuitBreaker { private config: CircuitBreakerConfig; private metrics: CircuitBreakerMetrics; private eventListeners: ((event: ResilienceEvent) => void)[] = []; private marineEnvironment?: MarineEnvironmentStatus; constructor(name: string, config: Partial<CircuitBreakerConfig> = {}) { this.config = { failureThreshold: 5, successThreshold: 3, timeout: 60000, // 1 minute operationalContext: OperationalContext.SAILING, failureTypes: [MarineFailureType.SENSOR_FAILURE, MarineFailureType.TIMEOUT], environmentalAdjustments: { roughSeas: false, lowPower: false, limitedConnectivity: false }, volumeThreshold: 10, errorPercentageThreshold: 50, slowCallDurationThreshold: 5000, // 5 seconds slowCallRateThreshold: 50, ...config }; this.metrics = this.initializeMetrics(); } /** * Execute operation with circuit breaker protection */ async execute<T>( operation: () => Promise<T>, operationName: string = 'unknown' ): Promise<ResilienceResult<T>> { const startTime = Date.now(); // Check if circuit breaker allows execution if (!this.canExecute()) { this.metrics.rejectedRequests++; this.emitEvent('circuit_breaker_opened', operationName, { reason: 'Circuit breaker is open', state: this.metrics.state }, 'warning'); return { success: false, error: new Error(`Circuit breaker is ${this.metrics.state} for ${operationName}`), metrics: { executionTime: 0, retryCount: 0, circuitBreakerState: this.metrics.state }, marineContext: this.getMarineContext() }; } this.metrics.totalRequests++; try { // Execute the operation with timeout const timeoutMs = this.calculateTimeout(); const result = await this.executeWithTimeout(operation, timeoutMs); const executionTime = Date.now() - startTime; // Check if call was slow const isSlow = executionTime > this.config.slowCallDurationThreshold; if (isSlow) { this.metrics.slowCalls++; } // Record success this.onSuccess(executionTime); return { success: true, data: result, metrics: { executionTime, retryCount: 0, circuitBreakerState: this.metrics.state, timeoutApplied: timeoutMs }, marineContext: this.getMarineContext() }; } catch (error) { const executionTime = Date.now() - startTime; // Classify the failure type const failureType = this.classifyFailure(error as Error); // Record failure this.onFailure(failureType, executionTime); return { success: false, error: error as Error, metrics: { executionTime, retryCount: 0, circuitBreakerState: this.metrics.state }, marineContext: this.getMarineContext() }; } } /** * Update marine environment status for adaptive behavior */ updateMarineEnvironment(environment: MarineEnvironmentStatus): void { this.marineEnvironment = environment; // Adjust configuration based on marine conditions this.adjustForMarineConditions(); } /** * Get current circuit breaker metrics */ getMetrics(): CircuitBreakerMetrics { return { ...this.metrics }; } /** * Get current circuit breaker state */ getState(): CircuitBreakerStateType { return this.metrics.state; } /** * Force circuit breaker to specific state (for testing/emergency) */ forceState(state: CircuitBreakerStateType, reason: string = 'Manual override'): void { const previousState = this.metrics.state; this.metrics.state = state; this.metrics.stateChangedAt = new Date(); if (state === CircuitBreakerState.OPEN) { this.metrics.nextAttemptAt = new Date(Date.now() + this.config.timeout); } else { delete this.metrics.nextAttemptAt; } this.emitEvent('circuit_breaker_opened', 'manual', { previousState, newState: state, reason }, 'info'); } /** * Add event listener for circuit breaker events */ onEvent(listener: (event: ResilienceEvent) => void): void { this.eventListeners.push(listener); } /** * Reset circuit breaker metrics (for testing/maintenance) */ reset(): void { this.metrics = this.initializeMetrics(); } /** * Check if circuit breaker allows execution */ private canExecute(): boolean { const now = new Date(); switch (this.metrics.state) { case CircuitBreakerState.CLOSED: return true; case CircuitBreakerState.OPEN: // Check if timeout period has passed if (this.metrics.nextAttemptAt && now >= this.metrics.nextAttemptAt) { this.transitionToHalfOpen(); return true; } return false; case CircuitBreakerState.HALF_OPEN: return true; default: return false; } } /** * Execute operation with timeout */ private async executeWithTimeout<T>( operation: () => Promise<T>, timeoutMs: number ): Promise<T> { return new Promise((resolve, reject) => { const timer = setTimeout(() => { reject(new Error(`Operation timed out after ${timeoutMs}ms`)); }, timeoutMs); operation() .then(result => { clearTimeout(timer); resolve(result); }) .catch(error => { clearTimeout(timer); reject(error); }); }); } /** * Calculate timeout based on marine conditions */ private calculateTimeout(): number { let timeout = this.config.slowCallDurationThreshold; // Adjust for marine environment if (this.marineEnvironment) { timeout *= this.marineEnvironment.recommendedTimeoutMultiplier; } // Adjust for operational context switch (this.config.operationalContext) { case OperationalContext.EMERGENCY: timeout *= 0.5; // Faster response needed in emergency break; case OperationalContext.MAINTENANCE: timeout *= 2.0; // More tolerance during maintenance break; case OperationalContext.ANCHORED: timeout *= 1.5; // More relaxed when anchored break; } // Adjust for environmental conditions if (this.config.environmentalAdjustments.roughSeas) { timeout *= 1.5; } if (this.config.environmentalAdjustments.limitedConnectivity) { timeout *= 2.0; } return Math.max(1000, Math.min(timeout, 30000)); // Between 1s and 30s } /** * Classify failure type for marine-specific handling */ private classifyFailure(error: Error): MarineFailureTypeType { const message = error.message.toLowerCase(); if (message.includes('timeout')) { return MarineFailureType.TIMEOUT; } if (message.includes('network') || message.includes('connection')) { return MarineFailureType.CONNECTIVITY_LOSS; } if (message.includes('sensor') || message.includes('reading')) { return MarineFailureType.SENSOR_FAILURE; } if (message.includes('power') || message.includes('voltage')) { return MarineFailureType.POWER_FLUCTUATION; } if (message.includes('validation') || message.includes('quality')) { return MarineFailureType.DATA_QUALITY; } if (message.includes('resource') || message.includes('memory')) { return MarineFailureType.RESOURCE_EXHAUSTION; } return MarineFailureType.ENVIRONMENTAL; // Default for unknown failures } /** * Handle successful operation */ private onSuccess(executionTime: number): void { this.metrics.successfulRequests++; this.metrics.lastSuccessTime = new Date(); this.updateAverageResponseTime(executionTime); if (this.metrics.state === CircuitBreakerState.HALF_OPEN) { // Count successes in half-open state const recentSuccesses = this.calculateRecentSuccesses(); if (recentSuccesses >= this.config.successThreshold) { this.transitionToClosed(); } } } /** * Handle failed operation */ private onFailure(failureType: MarineFailureTypeType, executionTime: number): void { this.metrics.failedRequests++; this.metrics.lastFailureTime = new Date(); this.updateAverageResponseTime(executionTime); // Update marine-specific failure counters this.updateMarineFailureCounters(failureType); // Check if circuit should open if (this.shouldOpenCircuit()) { this.transitionToOpen(failureType); } } /** * Update marine-specific failure counters */ private updateMarineFailureCounters(failureType: MarineFailureTypeType): void { switch (failureType) { case MarineFailureType.ENVIRONMENTAL: this.metrics.environmentalFailures++; break; case MarineFailureType.POWER_FLUCTUATION: this.metrics.powerRelatedFailures++; break; case MarineFailureType.CONNECTIVITY_LOSS: this.metrics.connectivityFailures++; break; case MarineFailureType.SENSOR_FAILURE: this.metrics.sensorFailures++; break; } } /** * Check if circuit breaker should open */ private shouldOpenCircuit(): boolean { // Don't open if we haven't reached minimum volume if (this.metrics.totalRequests < this.config.volumeThreshold) { return false; } // Check failure count threshold if (this.metrics.failedRequests >= this.config.failureThreshold) { return true; } // Check error percentage threshold const errorPercentage = (this.metrics.failedRequests / this.metrics.totalRequests) * 100; if (errorPercentage >= this.config.errorPercentageThreshold) { return true; } // Check slow call rate threshold const slowCallPercentage = (this.metrics.slowCalls / this.metrics.totalRequests) * 100; if (slowCallPercentage >= this.config.slowCallRateThreshold) { return true; } return false; } /** * Transition to OPEN state */ private transitionToOpen(failureType: MarineFailureTypeType): void { this.metrics.state = CircuitBreakerState.OPEN; this.metrics.stateChangedAt = new Date(); this.metrics.nextAttemptAt = new Date(Date.now() + this.config.timeout); this.emitEvent('circuit_breaker_opened', 'automatic', { failureType, failureCount: this.metrics.failedRequests, totalRequests: this.metrics.totalRequests, errorPercentage: (this.metrics.failedRequests / this.metrics.totalRequests) * 100 }, 'warning'); } /** * Transition to HALF_OPEN state */ private transitionToHalfOpen(): void { this.metrics.state = CircuitBreakerState.HALF_OPEN; this.metrics.stateChangedAt = new Date(); delete this.metrics.nextAttemptAt; this.emitEvent('circuit_breaker_opened', 'automatic', { previousState: CircuitBreakerState.OPEN, newState: CircuitBreakerState.HALF_OPEN, reason: 'Timeout period elapsed, testing service' }, 'info'); } /** * Transition to CLOSED state */ private transitionToClosed(): void { this.metrics.state = CircuitBreakerState.CLOSED; this.metrics.stateChangedAt = new Date(); delete this.metrics.nextAttemptAt; this.emitEvent('circuit_breaker_closed', 'automatic', { successCount: this.calculateRecentSuccesses(), reason: 'Service recovered successfully' }, 'info'); } /** * Calculate recent successes (for half-open state) */ private calculateRecentSuccesses(): number { // Simplified: return recent success count // In a full implementation, this would track recent operations return this.metrics.successfulRequests; } /** * Update average response time */ private updateAverageResponseTime(executionTime: number): void { const totalTime = this.metrics.averageResponseTime * (this.metrics.totalRequests - 1); this.metrics.averageResponseTime = (totalTime + executionTime) / this.metrics.totalRequests; } /** * Adjust configuration for marine conditions */ private adjustForMarineConditions(): void { if (!this.marineEnvironment) return; // Adjust thresholds based on expected failure rate const baseFailureThreshold = this.config.failureThreshold; const adjustedThreshold = Math.ceil(baseFailureThreshold * this.marineEnvironment.expectedFailureRate); // Don't make it too sensitive or too tolerant this.config.failureThreshold = Math.max(3, Math.min(adjustedThreshold, 20)); // Adjust timeout based on conditions if (this.marineEnvironment.criticalOperationsOnly) { this.config.environmentalAdjustments.roughSeas = true; this.config.environmentalAdjustments.limitedConnectivity = true; } } /** * Get current marine context */ private getMarineContext() { const connectivityQuality = this.marineEnvironment?.connectivityQuality ?? 0; const connectivityStatus: 'online' | 'intermittent' | 'offline' = connectivityQuality > 0.7 ? 'online' : connectivityQuality > 0.3 ? 'intermittent' : 'offline'; return { operationalContext: this.config.operationalContext, environmentalConditions: this.marineEnvironment || {}, powerStatus: this.marineEnvironment?.powerStatus || 'normal', connectivityStatus }; } /** * Emit resilience event */ private emitEvent( eventType: ResilienceEvent['eventType'], component: string, details: Record<string, any>, severity: ResilienceEvent['severity'] ): void { const event: ResilienceEvent = { timestamp: new Date(), eventType, component, details, severity, marineContext: { operationalContext: this.config.operationalContext, failureType: details['failureType'], environmentalImpact: this.config.environmentalAdjustments.roughSeas, safetyImpact: this.config.operationalContext === OperationalContext.EMERGENCY } }; this.eventListeners.forEach(listener => { try { listener(event); } catch (error) { console.error('Error in circuit breaker event listener:', error); } }); } /** * Initialize metrics */ private initializeMetrics(): CircuitBreakerMetrics { return { state: CircuitBreakerState.CLOSED, totalRequests: 0, successfulRequests: 0, failedRequests: 0, rejectedRequests: 0, averageResponseTime: 0, slowCalls: 0, environmentalFailures: 0, powerRelatedFailures: 0, connectivityFailures: 0, sensorFailures: 0, stateChangedAt: new Date() }; } }