@sailboat-computer/resilience
Version:
Enhanced resilience patterns for sailboat computer v3 with marine-specific adaptations
540 lines (469 loc) • 15.3 kB
text/typescript
/**
* 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()
};
}
}