@sailboat-computer/resilience
Version:
Enhanced resilience patterns for sailboat computer v3 with marine-specific adaptations
571 lines (495 loc) • 16.1 kB
text/typescript
/**
* Hierarchical timeout management for marine operations
*/
import {
TimeoutConfig,
TimeoutMetrics,
ResilienceEvent,
MarineEnvironmentStatus,
ResilienceResult
} from '../types';
import {
OperationalContext,
OperationalContextType
} from '../types/marine-constants';
/**
* Timeout execution context
*/
export interface TimeoutExecutionContext {
operationName: string;
operationalContext: OperationalContextType;
marineSystemType: 'navigation' | 'safety' | 'comfort' | 'maintenance';
priority: 'critical' | 'normal' | 'low';
customTimeouts?: Partial<TimeoutConfig>;
}
/**
* Timeout execution result
*/
export interface TimeoutExecutionResult<T> {
success: boolean;
data?: T;
error?: Error;
executionTime: number;
timeoutApplied: number;
timedOut: boolean;
retryCount: number;
}
/**
* Active timeout tracking
*/
interface ActiveTimeout {
id: string;
operationName: string;
startTime: Date;
timeoutMs: number;
context: TimeoutExecutionContext;
timer: NodeJS.Timeout;
}
/**
* Hierarchical timeout manager for marine operations
*/
export class TimeoutManager {
private config: TimeoutConfig;
private metrics: TimeoutMetrics;
private activeTimeouts: Map<string, ActiveTimeout> = new Map();
private eventListeners: ((event: ResilienceEvent) => void)[] = [];
private marineEnvironment?: MarineEnvironmentStatus;
private nextTimeoutId = 1;
constructor(config: Partial<TimeoutConfig> = {}) {
this.config = {
operationTimeout: 5000, // 5 seconds
serviceTimeout: 30000, // 30 seconds
systemTimeout: 120000, // 2 minutes
environmentalMultiplier: 1.0,
operationalContext: OperationalContext.SAILING,
maxRetries: 3,
retryBackoffMultiplier: 2.0,
maxRetryDelay: 30000, // 30 seconds
criticalOperationTimeout: 2000, // 2 seconds
sensorReadTimeout: 3000, // 3 seconds
networkOperationTimeout: 15000, // 15 seconds
...config
};
this.metrics = this.initializeMetrics();
}
/**
* Execute operation with hierarchical timeout protection
*/
async execute<T>(
operation: () => Promise<T>,
context: TimeoutExecutionContext
): Promise<TimeoutExecutionResult<T>> {
const startTime = Date.now();
let retryCount = 0;
let lastError: Error | undefined;
// Calculate timeout based on context and marine conditions
const timeoutMs = this.calculateTimeout(context);
while (retryCount <= this.config.maxRetries) {
try {
const result = await this.executeWithTimeout(operation, timeoutMs, context);
const executionTime = Date.now() - startTime;
// Update metrics for successful operation
this.updateMetrics(executionTime, false, retryCount);
return {
success: true,
data: result,
executionTime,
timeoutApplied: timeoutMs,
timedOut: false,
retryCount
};
} catch (error) {
lastError = error as Error;
const isTimeout = this.isTimeoutError(error as Error);
if (isTimeout) {
this.metrics.timedOutOperations++;
// Update marine-specific timeout counters
this.updateMarineTimeoutCounters(context);
}
// Check if we should retry
if (retryCount < this.config.maxRetries && this.shouldRetry(error as Error, context)) {
retryCount++;
this.metrics.totalRetries++;
// Calculate retry delay with exponential backoff
const retryDelay = this.calculateRetryDelay(retryCount);
this.emitEvent('retry', context.operationName, {
retryCount,
retryDelay,
error: (error as Error).message,
isTimeout,
marineSystemType: context.marineSystemType
}, 'warning');
// Wait before retry
await this.delay(retryDelay);
} else {
break;
}
}
}
// All retries exhausted
const executionTime = Date.now() - startTime;
const isTimeout = lastError ? this.isTimeoutError(lastError) : false;
this.updateMetrics(executionTime, isTimeout, retryCount);
if (retryCount > 0) {
this.metrics.failedRetries++;
}
return {
success: false,
error: lastError || new Error('Unknown error occurred'),
executionTime,
timeoutApplied: timeoutMs,
timedOut: isTimeout,
retryCount
};
}
/**
* Update marine environment for adaptive timeouts
*/
updateMarineEnvironment(environment: MarineEnvironmentStatus): void {
this.marineEnvironment = environment;
// Update environmental multiplier based on conditions
this.config.environmentalMultiplier = environment.recommendedTimeoutMultiplier;
this.emitEvent('recovery', 'timeout-manager', {
action: 'marine_environment_updated',
environmentalMultiplier: this.config.environmentalMultiplier,
seaState: environment.seaState,
powerStatus: environment.powerStatus
}, 'info');
}
/**
* Get current timeout metrics
*/
getMetrics(): TimeoutMetrics {
return { ...this.metrics };
}
/**
* Get active timeouts status
*/
getActiveTimeouts() {
return Array.from(this.activeTimeouts.values()).map(timeout => ({
id: timeout.id,
operationName: timeout.operationName,
startTime: timeout.startTime,
timeoutMs: timeout.timeoutMs,
elapsedTime: Date.now() - timeout.startTime.getTime(),
remainingTime: Math.max(0, timeout.timeoutMs - (Date.now() - timeout.startTime.getTime())),
context: {
operationalContext: timeout.context.operationalContext,
marineSystemType: timeout.context.marineSystemType,
priority: timeout.context.priority
}
}));
}
/**
* Cancel all active timeouts (emergency)
*/
cancelAllTimeouts(reason: string = 'Emergency cancellation'): void {
const cancelledCount = this.activeTimeouts.size;
for (const timeout of this.activeTimeouts.values()) {
clearTimeout(timeout.timer);
}
this.activeTimeouts.clear();
this.emitEvent('recovery', 'timeout-manager', {
action: 'cancel_all_timeouts',
cancelledCount,
reason
}, 'warning');
}
/**
* Add event listener
*/
onEvent(listener: (event: ResilienceEvent) => void): void {
this.eventListeners.push(listener);
}
/**
* Execute operation with timeout
*/
private async executeWithTimeout<T>(
operation: () => Promise<T>,
timeoutMs: number,
context: TimeoutExecutionContext
): Promise<T> {
const timeoutId = `timeout-${this.nextTimeoutId++}`;
return new Promise<T>((resolve, reject) => {
// Create timeout
const timer = setTimeout(() => {
this.activeTimeouts.delete(timeoutId);
reject(new Error(`Operation '${context.operationName}' timed out after ${timeoutMs}ms`));
}, timeoutMs);
// Track active timeout
const activeTimeout: ActiveTimeout = {
id: timeoutId,
operationName: context.operationName,
startTime: new Date(),
timeoutMs,
context,
timer
};
this.activeTimeouts.set(timeoutId, activeTimeout);
// Execute operation
operation()
.then(result => {
clearTimeout(timer);
this.activeTimeouts.delete(timeoutId);
resolve(result);
})
.catch(error => {
clearTimeout(timer);
this.activeTimeouts.delete(timeoutId);
reject(error);
});
});
}
/**
* Calculate timeout based on context and marine conditions
*/
private calculateTimeout(context: TimeoutExecutionContext): number {
let baseTimeout: number;
// Determine base timeout based on operation type and priority
if (context.priority === 'critical') {
baseTimeout = this.config.criticalOperationTimeout;
} else {
switch (context.marineSystemType) {
case 'navigation':
baseTimeout = this.config.sensorReadTimeout;
break;
case 'safety':
baseTimeout = this.config.criticalOperationTimeout;
break;
case 'comfort':
baseTimeout = this.config.networkOperationTimeout;
break;
case 'maintenance':
baseTimeout = this.config.operationTimeout * 3; // Longer for maintenance
break;
default:
baseTimeout = this.config.operationTimeout;
}
}
// Apply custom timeouts if provided
if (context.customTimeouts?.operationTimeout) {
baseTimeout = context.customTimeouts.operationTimeout;
}
// Apply environmental multiplier
let timeout = baseTimeout * this.config.environmentalMultiplier;
// Apply operational context adjustments
switch (context.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;
case OperationalContext.DOCKED:
timeout *= 1.2; // Slightly more relaxed when docked
break;
}
// Apply marine environment specific adjustments
if (this.marineEnvironment) {
if (this.marineEnvironment.criticalOperationsOnly && context.priority !== 'critical') {
timeout *= 0.5; // Reduce timeouts for non-critical operations
}
if (this.marineEnvironment.powerStatus === 'critical') {
timeout *= 0.7; // Reduce timeouts to save power
}
}
// Ensure timeout is within reasonable bounds
const minTimeout = context.priority === 'critical' ? 1000 : 2000; // 1-2 seconds minimum
const maxTimeout = context.marineSystemType === 'maintenance' ? 300000 : 60000; // 1-5 minutes maximum
return Math.max(minTimeout, Math.min(timeout, maxTimeout));
}
/**
* Check if error is a timeout error
*/
private isTimeoutError(error: Error): boolean {
return error.message.toLowerCase().includes('timeout') ||
error.message.toLowerCase().includes('timed out');
}
/**
* Check if operation should be retried
*/
private shouldRetry(error: Error, context: TimeoutExecutionContext): boolean {
// Always retry timeouts
if (this.isTimeoutError(error)) {
return true;
}
// Don't retry critical safety operations - fail fast
if (context.marineSystemType === 'safety' && context.priority === 'critical') {
return false;
}
// Retry network-related errors
if (error.message.toLowerCase().includes('network') ||
error.message.toLowerCase().includes('connection')) {
return true;
}
// Don't retry validation errors
if (error.message.toLowerCase().includes('validation')) {
return false;
}
// Default: retry for most errors
return true;
}
/**
* Calculate retry delay with exponential backoff
*/
private calculateRetryDelay(retryCount: number): number {
const baseDelay = 1000; // 1 second
const delay = baseDelay * Math.pow(this.config.retryBackoffMultiplier, retryCount - 1);
// Add jitter to prevent thundering herd
const jitter = Math.random() * 0.1 * delay;
return Math.min(delay + jitter, this.config.maxRetryDelay);
}
/**
* Update marine-specific timeout counters
*/
private updateMarineTimeoutCounters(context: TimeoutExecutionContext): void {
if (this.marineEnvironment?.seaState === 'very_rough' ||
this.marineEnvironment?.weather === 'storm') {
this.metrics.environmentalTimeouts++;
}
if (context.priority === 'critical') {
this.metrics.criticalOperationTimeouts++;
}
if (context.marineSystemType === 'navigation' ||
context.operationName.toLowerCase().includes('sensor')) {
this.metrics.sensorTimeouts++;
}
}
/**
* Update metrics
*/
private updateMetrics(executionTime: number, timedOut: boolean, retryCount: number): void {
this.metrics.totalOperations++;
if (timedOut) {
this.metrics.timedOutOperations++;
}
// Update execution time metrics
const totalTime = this.metrics.averageOperationTime * (this.metrics.totalOperations - 1);
this.metrics.averageOperationTime = (totalTime + executionTime) / this.metrics.totalOperations;
this.metrics.maxOperationTime = Math.max(this.metrics.maxOperationTime, executionTime);
// Update retry metrics
if (retryCount > 0) {
if (!timedOut) {
this.metrics.successfulRetries++;
}
}
}
/**
* Delay helper function
*/
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* 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,
environmentalImpact: this.marineEnvironment?.seaState === 'very_rough',
safetyImpact: details['marineSystemType'] === 'safety'
}
};
this.eventListeners.forEach(listener => {
try {
listener(event);
} catch (error) {
console.error('Error in timeout manager event listener:', error);
}
});
}
/**
* Initialize metrics
*/
private initializeMetrics(): TimeoutMetrics {
return {
totalOperations: 0,
timedOutOperations: 0,
averageOperationTime: 0,
maxOperationTime: 0,
totalRetries: 0,
successfulRetries: 0,
failedRetries: 0,
environmentalTimeouts: 0,
criticalOperationTimeouts: 0,
sensorTimeouts: 0
};
}
}
/**
* Default timeout manager instance
*/
export const defaultTimeoutManager = new TimeoutManager();
/**
* Convenience function to execute with timeout protection
*/
export async function executeWithTimeout<T>(
operation: () => Promise<T>,
context: TimeoutExecutionContext
): Promise<TimeoutExecutionResult<T>> {
return defaultTimeoutManager.execute(operation, context);
}
/**
* Pre-configured timeout contexts for marine operations
*/
export const TimeoutContexts = {
/**
* Navigation sensor reading
*/
navigationSensor: (operationName: string, operationalContext: OperationalContextType = OperationalContext.SAILING): TimeoutExecutionContext => ({
operationName,
operationalContext,
marineSystemType: 'navigation',
priority: 'critical'
}),
/**
* Safety system operation
*/
safetyOperation: (operationName: string, operationalContext: OperationalContextType = OperationalContext.SAILING): TimeoutExecutionContext => ({
operationName,
operationalContext,
marineSystemType: 'safety',
priority: 'critical'
}),
/**
* Network communication
*/
networkOperation: (operationName: string, operationalContext: OperationalContextType = OperationalContext.SAILING): TimeoutExecutionContext => ({
operationName,
operationalContext,
marineSystemType: 'comfort',
priority: 'normal'
}),
/**
* Maintenance operation
*/
maintenanceOperation: (operationName: string): TimeoutExecutionContext => ({
operationName,
operationalContext: OperationalContext.MAINTENANCE,
marineSystemType: 'maintenance',
priority: 'normal'
}),
/**
* Comfort system operation
*/
comfortOperation: (operationName: string, operationalContext: OperationalContextType = OperationalContext.SAILING): TimeoutExecutionContext => ({
operationName,
operationalContext,
marineSystemType: 'comfort',
priority: 'low'
})
};