@sailboat-computer/resilience
Version:
Enhanced resilience patterns for sailboat computer v3 with marine-specific adaptations
773 lines (674 loc) • 19.7 kB
text/typescript
/**
* Retry mechanism with marine-specific adaptations
*/
import {
MarineEnvironmentStatus,
ResilienceEvent,
ResilienceResult
} from '../types';
import {
OperationalContext,
OperationalContextType,
MarineFailureType,
MarineFailureTypeType
} from '../types/marine-constants';
/**
* Retry configuration
*/
export interface RetryConfig {
/**
* Maximum number of retry attempts
*/
maxRetries: number;
/**
* Base delay between retries in ms
*/
baseDelayMs: number;
/**
* Multiplier for exponential backoff
*/
backoffMultiplier: number;
/**
* Maximum delay between retries in ms
*/
maxDelayMs: number;
/**
* Whether to add jitter to retry delays
*/
useJitter: boolean;
/**
* Failure types that should trigger retry
*/
retryableFailures: MarineFailureTypeType[];
/**
* Marine-specific settings
*/
marineSettings: {
/**
* Whether to adapt retry behavior to marine conditions
*/
adaptToMarineConditions: boolean;
/**
* Whether to adapt to power status
*/
powerAware: boolean;
/**
* Operational contexts where retries are allowed
*/
allowedContexts: OperationalContextType[];
/**
* Whether to limit retries in rough seas
*/
limitInRoughSeas: boolean;
};
}
/**
* Retry execution context
*/
export interface RetryExecutionContext {
/**
* Operation name for logging and metrics
*/
operationName: string;
/**
* Current operational context
*/
operationalContext: OperationalContextType;
/**
* Marine system type
*/
marineSystemType: 'navigation' | 'safety' | 'comfort' | 'maintenance';
/**
* Operation priority
*/
priority: 'critical' | 'normal' | 'low';
/**
* Custom retry configuration overrides
*/
customRetryConfig?: Partial<RetryConfig>;
}
/**
* Retry execution result
*/
export interface RetryExecutionResult<T> {
/**
* Whether the operation was successful
*/
success: boolean;
/**
* Result data if successful
*/
data?: T;
/**
* Error if failed
*/
error?: Error;
/**
* Number of retry attempts made
*/
retryCount: number;
/**
* Total execution time including retries
*/
totalExecutionTime: number;
/**
* Delays between retry attempts
*/
retryDelays: number[];
/**
* Whether maximum retries were reached
*/
maxRetriesReached: boolean;
/**
* Marine context during execution
*/
marineContext: {
operationalContext: OperationalContextType;
environmentalConditions?: Partial<MarineEnvironmentStatus>;
};
}
/**
* Retry metrics
*/
export interface RetryMetrics {
/**
* Total operations attempted
*/
totalOperations: number;
/**
* Operations that succeeded without retry
*/
successWithoutRetry: number;
/**
* Operations that succeeded after retry
*/
successWithRetry: number;
/**
* Operations that failed after all retries
*/
failedAfterRetry: number;
/**
* Total retry attempts
*/
totalRetryAttempts: number;
/**
* Average retries per operation
*/
averageRetriesPerOperation: number;
/**
* Maximum retries for a single operation
*/
maxRetriesForOperation: number;
/**
* Average delay between retries
*/
averageRetryDelay: number;
/**
* Marine-specific metrics
*/
marineMetrics: {
/**
* Retries due to environmental conditions
*/
environmentalRetries: number;
/**
* Retries due to connectivity issues
*/
connectivityRetries: number;
/**
* Retries due to power issues
*/
powerRetries: number;
/**
* Retries due to sensor failures
*/
sensorRetries: number;
};
}
/**
* Retry manager for marine operations
*/
export class RetryManager {
private config: RetryConfig;
private metrics: RetryMetrics;
private eventListeners: ((event: ResilienceEvent) => void)[] = [];
private marineEnvironment?: MarineEnvironmentStatus;
constructor(config: Partial<RetryConfig> = {}) {
this.config = {
maxRetries: 3,
baseDelayMs: 1000,
backoffMultiplier: 2.0,
maxDelayMs: 30000,
useJitter: true,
retryableFailures: [
MarineFailureType.CONNECTIVITY_LOSS,
MarineFailureType.TIMEOUT,
MarineFailureType.ENVIRONMENTAL,
MarineFailureType.SENSOR_FAILURE
],
marineSettings: {
adaptToMarineConditions: true,
powerAware: true,
allowedContexts: [
OperationalContext.SAILING,
OperationalContext.MOTORING,
OperationalContext.ANCHORED,
OperationalContext.DOCKED,
OperationalContext.MAINTENANCE
],
limitInRoughSeas: true
},
...config
};
this.metrics = this.initializeMetrics();
}
/**
* Execute operation with retry
*/
async execute<T>(
operation: () => Promise<T>,
context: RetryExecutionContext
): Promise<RetryExecutionResult<T>> {
const startTime = Date.now();
let retryCount = 0;
let lastError: Error | undefined;
const retryDelays: number[] = [];
// Apply custom config if provided
const effectiveConfig = this.getEffectiveConfig(context);
// Check if retries are allowed in current context
if (!this.isRetryAllowed(context.operationalContext)) {
try {
const result = await operation();
this.updateMetrics(0, true);
return {
success: true,
data: result,
retryCount: 0,
totalExecutionTime: Date.now() - startTime,
retryDelays: [],
maxRetriesReached: false,
marineContext: {
operationalContext: context.operationalContext,
environmentalConditions: this.marineEnvironment ? { ...this.marineEnvironment } : undefined as unknown as Partial<MarineEnvironmentStatus>
}
};
} catch (error) {
this.updateMetrics(0, false);
return {
success: false,
error: error as Error,
retryCount: 0,
totalExecutionTime: Date.now() - startTime,
retryDelays: [],
maxRetriesReached: false,
marineContext: {
operationalContext: context.operationalContext,
environmentalConditions: this.marineEnvironment ? { ...this.marineEnvironment } : undefined as unknown as Partial<MarineEnvironmentStatus>
}
};
}
}
// Execute with retry
while (retryCount <= effectiveConfig.maxRetries) {
try {
const result = await operation();
// Success - update metrics and return
if (retryCount === 0) {
this.updateMetrics(0, true);
} else {
this.updateMetrics(retryCount, true);
}
return {
success: true,
data: result,
retryCount,
totalExecutionTime: Date.now() - startTime,
retryDelays,
maxRetriesReached: false,
marineContext: {
operationalContext: context.operationalContext,
environmentalConditions: this.marineEnvironment ? { ...this.marineEnvironment } : undefined as unknown as Partial<MarineEnvironmentStatus>
}
};
} catch (error) {
lastError = error as Error;
// Check if we should retry this error
if (!this.isRetryableError(error as Error, effectiveConfig)) {
break;
}
// Check if we have retries remaining
if (retryCount < effectiveConfig.maxRetries) {
retryCount++;
// Calculate delay for this retry
const delay = this.calculateRetryDelay(retryCount, effectiveConfig, context);
retryDelays.push(delay);
// Emit retry event
this.emitEvent('retry', context.operationName, {
retryCount,
delay,
error: (error as Error).message,
marineSystemType: context.marineSystemType,
priority: context.priority
}, 'warning');
// Wait before retry
await this.delay(delay);
} else {
break;
}
}
}
// All retries exhausted or non-retryable error
this.updateMetrics(retryCount, false);
return {
success: false,
error: lastError || new Error('Unknown error'),
retryCount,
totalExecutionTime: Date.now() - startTime,
retryDelays,
maxRetriesReached: retryCount >= effectiveConfig.maxRetries,
marineContext: {
operationalContext: context.operationalContext,
environmentalConditions: this.marineEnvironment ? { ...this.marineEnvironment } : undefined as unknown as Partial<MarineEnvironmentStatus>
}
};
}
/**
* Update marine environment for adaptive retry behavior
*/
updateMarineEnvironment(environment: MarineEnvironmentStatus): void {
this.marineEnvironment = environment;
this.emitEvent('recovery', 'retry-manager', {
action: 'marine_environment_updated',
seaState: environment.seaState,
powerStatus: environment.powerStatus,
criticalOperationsOnly: environment.criticalOperationsOnly
}, 'info');
}
/**
* Get current retry metrics
*/
getMetrics(): RetryMetrics {
return { ...this.metrics };
}
/**
* Add event listener
*/
onEvent(listener: (event: ResilienceEvent) => void): void {
this.eventListeners.push(listener);
}
/**
* Remove event listener
*/
removeEventListener(listener: (event: ResilienceEvent) => void): void {
const index = this.eventListeners.indexOf(listener);
if (index > -1) {
this.eventListeners.splice(index, 1);
}
}
/**
* Get effective configuration with overrides applied
*/
private getEffectiveConfig(context: RetryExecutionContext): RetryConfig {
if (!context.customRetryConfig) {
return this.config;
}
return {
...this.config,
...context.customRetryConfig,
marineSettings: {
...this.config.marineSettings,
...(context.customRetryConfig.marineSettings || {})
}
};
}
/**
* Check if retry is allowed in current operational context
*/
private isRetryAllowed(operationalContext: OperationalContextType): boolean {
// Don't retry in emergency context
if (operationalContext === OperationalContext.EMERGENCY) {
return false;
}
// Check if context is in allowed contexts
if (!this.config.marineSettings.allowedContexts.includes(operationalContext)) {
return false;
}
// Check marine environment conditions
if (this.marineEnvironment) {
// Check sea conditions if configured
if (this.config.marineSettings.limitInRoughSeas &&
(this.marineEnvironment.seaState === 'rough' ||
this.marineEnvironment.seaState === 'very_rough')) {
return false;
}
// Check power status if configured
if (this.config.marineSettings.powerAware &&
this.marineEnvironment.powerStatus === 'critical') {
return false;
}
// Check critical operations only
if (this.marineEnvironment.criticalOperationsOnly) {
return false;
}
}
return true;
}
/**
* Check if error is retryable
*/
private isRetryableError(error: Error, config: RetryConfig): boolean {
const errorMessage = error.message.toLowerCase();
// Check for specific marine failure types
for (const failureType of config.retryableFailures) {
if (errorMessage.includes(failureType.toLowerCase())) {
return true;
}
}
// Common retryable errors
if (errorMessage.includes('timeout') ||
errorMessage.includes('connection') ||
errorMessage.includes('network') ||
errorMessage.includes('temporary')) {
return true;
}
// Non-retryable errors
if (errorMessage.includes('validation') ||
errorMessage.includes('not found') ||
errorMessage.includes('permission') ||
errorMessage.includes('unauthorized')) {
return false;
}
// Default to retryable for unknown errors
return true;
}
/**
* Calculate retry delay with exponential backoff and jitter
*/
private calculateRetryDelay(
retryCount: number,
config: RetryConfig,
context: RetryExecutionContext
): number {
// Base delay with exponential backoff
let delay = config.baseDelayMs * Math.pow(config.backoffMultiplier, retryCount - 1);
// Add jitter if configured
if (config.useJitter) {
const jitter = Math.random() * 0.2 * delay; // 0-20% jitter
delay += jitter;
}
// Apply marine environment adjustments
if (this.marineEnvironment && config.marineSettings.adaptToMarineConditions) {
// Adjust for sea state
if (this.marineEnvironment.seaState === 'moderate') {
delay *= 1.2; // 20% longer delays in moderate seas
}
// Adjust for power status
if (this.marineEnvironment.powerStatus === 'conservation') {
delay *= 1.5; // 50% longer delays in power conservation mode
}
// Adjust for connectivity
if (this.marineEnvironment.connectivityQuality < 0.5) {
delay *= 1.3; // 30% longer delays with poor connectivity
}
}
// Adjust for operational context
switch (context.operationalContext) {
case OperationalContext.MAINTENANCE:
delay *= 0.8; // Faster retries during maintenance
break;
case OperationalContext.ANCHORED:
delay *= 1.2; // Slower retries when anchored
break;
case OperationalContext.DOCKED:
delay *= 0.9; // Slightly faster retries when docked
break;
}
// Adjust for priority
switch (context.priority) {
case 'critical':
delay *= 0.7; // Faster retries for critical operations
break;
case 'low':
delay *= 1.3; // Slower retries for low priority operations
break;
}
// Ensure delay is within bounds
return Math.min(Math.max(delay, config.baseDelayMs), config.maxDelayMs);
}
/**
* Update metrics
*/
private updateMetrics(retryCount: number, success: boolean): void {
this.metrics.totalOperations++;
if (success) {
if (retryCount === 0) {
this.metrics.successWithoutRetry++;
} else {
this.metrics.successWithRetry++;
this.metrics.totalRetryAttempts += retryCount;
this.metrics.maxRetriesForOperation = Math.max(
this.metrics.maxRetriesForOperation,
retryCount
);
// Update marine-specific metrics
if (this.marineEnvironment) {
if (this.marineEnvironment.seaState === 'rough' ||
this.marineEnvironment.seaState === 'very_rough') {
this.metrics.marineMetrics.environmentalRetries += retryCount;
}
if (this.marineEnvironment.connectivityQuality < 0.5) {
this.metrics.marineMetrics.connectivityRetries += retryCount;
}
if (this.marineEnvironment.powerStatus === 'conservation' ||
this.marineEnvironment.powerStatus === 'critical') {
this.metrics.marineMetrics.powerRetries += retryCount;
}
}
}
} else {
this.metrics.failedAfterRetry++;
this.metrics.totalRetryAttempts += retryCount;
}
// Update average metrics
if (this.metrics.totalOperations > 0) {
this.metrics.averageRetriesPerOperation =
this.metrics.totalRetryAttempts / this.metrics.totalOperations;
}
}
/**
* 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: details['operationalContext'] || OperationalContext.SAILING,
environmentalImpact: this.marineEnvironment?.seaState === 'very_rough',
safetyImpact: details['marineSystemType'] === 'safety'
}
};
this.eventListeners.forEach(listener => {
try {
listener(event);
} catch (error) {
console.error('Error in retry manager event listener:', error);
}
});
}
/**
* Initialize metrics
*/
private initializeMetrics(): RetryMetrics {
return {
totalOperations: 0,
successWithoutRetry: 0,
successWithRetry: 0,
failedAfterRetry: 0,
totalRetryAttempts: 0,
averageRetriesPerOperation: 0,
maxRetriesForOperation: 0,
averageRetryDelay: 0,
marineMetrics: {
environmentalRetries: 0,
connectivityRetries: 0,
powerRetries: 0,
sensorRetries: 0
}
};
}
}
/**
* Default retry manager instance
*/
export const defaultRetryManager = new RetryManager();
/**
* Convenience function to execute with retry
*/
export async function executeWithRetry<T>(
operation: () => Promise<T>,
context: RetryExecutionContext
): Promise<RetryExecutionResult<T>> {
return defaultRetryManager.execute(operation, context);
}
/**
* Pre-configured retry contexts for marine operations
*/
export const RetryContexts = {
/**
* Navigation sensor operations
*/
navigationSensor: (operationName: string, operationalContext: OperationalContextType = OperationalContext.SAILING): RetryExecutionContext => ({
operationName,
operationalContext,
marineSystemType: 'navigation',
priority: 'critical'
}),
/**
* Safety system operations
*/
safetyOperation: (operationName: string, operationalContext: OperationalContextType = OperationalContext.SAILING): RetryExecutionContext => ({
operationName,
operationalContext,
marineSystemType: 'safety',
priority: 'critical'
}),
/**
* Network operations
*/
networkOperation: (operationName: string, operationalContext: OperationalContextType = OperationalContext.SAILING): RetryExecutionContext => ({
operationName,
operationalContext,
marineSystemType: 'comfort',
priority: 'normal',
customRetryConfig: {
maxRetries: 5,
baseDelayMs: 2000,
retryableFailures: [
MarineFailureType.CONNECTIVITY_LOSS,
MarineFailureType.TIMEOUT
]
}
}),
/**
* Maintenance operations
*/
maintenanceOperation: (operationName: string): RetryExecutionContext => ({
operationName,
operationalContext: OperationalContext.MAINTENANCE,
marineSystemType: 'maintenance',
priority: 'normal',
customRetryConfig: {
maxRetries: 10,
baseDelayMs: 5000,
maxDelayMs: 60000
}
}),
/**
* Comfort system operations
*/
comfortOperation: (operationName: string, operationalContext: OperationalContextType = OperationalContext.SAILING): RetryExecutionContext => ({
operationName,
operationalContext,
marineSystemType: 'comfort',
priority: 'low',
customRetryConfig: {
maxRetries: 2,
baseDelayMs: 3000
}
})
};