UNPKG

@sailboat-computer/health-monitoring

Version:

Comprehensive health monitoring system for sailboat computer v3 with marine-specific health checks

452 lines (400 loc) 13.4 kB
/** * Recovery orchestrator for self-healing mechanisms */ import { v4 as uuidv4 } from 'uuid'; import { HealthCheckResult, HealthStatus, HealthStatusType, OperationalContext, OperationalContextType, MarineSystemTypeType, MarineEnvironmentStatus } from '../types'; import { BaseRecoveryAction, RecoveryActionConfig, RecoveryActionResult, RecoveryPolicy, RecoveryEvent, RecoveryStatus, RecoveryPriority } from './RecoveryAction'; /** * Recovery orchestrator configuration */ export interface RecoveryOrchestratorConfig { /** * Whether automated recovery is enabled */ automatedRecoveryEnabled: boolean; /** * Maximum concurrent recovery actions */ maxConcurrentActions: number; /** * Default timeout for recovery actions in ms */ defaultActionTimeout: number; /** * Default retry count for recovery actions */ defaultRetryCount: number; /** * Default retry delay in ms */ defaultRetryDelay: number; /** * Whether to prioritize safety-critical systems */ prioritizeSafetyCritical: boolean; /** * Marine-specific settings */ marineSettings: { /** * Whether to adapt recovery to marine conditions */ adaptToMarineConditions: boolean; /** * Whether to adapt to power status */ powerAware: boolean; /** * Whether to limit recovery actions in rough seas */ limitInRoughSeas: boolean; /** * Whether to require approval in emergency situations */ requireApprovalInEmergency: boolean; }; } /** * Recovery orchestrator for managing self-healing actions */ export class RecoveryOrchestrator { private config: RecoveryOrchestratorConfig; private recoveryActions: Map<string, BaseRecoveryAction> = new Map(); private recoveryPolicies: Map<string, RecoveryPolicy> = new Map(); private activeRecoveries: Map<string, Promise<RecoveryActionResult>> = new Map(); private recoveryHistory: RecoveryActionResult[] = []; private eventListeners: ((event: RecoveryEvent) => void)[] = []; private currentOperationalContext: OperationalContextType = OperationalContext.DOCKED; private currentMarineEnvironment?: MarineEnvironmentStatus; constructor(config: RecoveryOrchestratorConfig) { this.config = config; } /** * Register a recovery action */ registerRecoveryAction(action: BaseRecoveryAction): void { const config = action.getConfig(); this.recoveryActions.set(config.actionId, action); } /** * Register a recovery policy */ registerRecoveryPolicy(policy: RecoveryPolicy): void { this.recoveryPolicies.set(policy.policyId, policy); } /** * Update operational context */ updateOperationalContext(context: OperationalContextType): void { this.currentOperationalContext = context; } /** * Update marine environment */ updateMarineEnvironment(environment: MarineEnvironmentStatus): void { this.currentMarineEnvironment = environment; } /** * Process a health check result and trigger recovery if needed */ async processHealthCheckResult(result: HealthCheckResult): Promise<void> { // Find applicable policies for this health check const applicablePolicies = Array.from(this.recoveryPolicies.values()) .filter(policy => policy.checkId === result.checkId && policy.enabled); if (applicablePolicies.length === 0) { return; // No policies for this health check } // Check if recovery is needed based on policies const triggeringPolicies = applicablePolicies.filter(policy => (result.status === policy.triggerStatus || result.status === HealthStatus.CRITICAL) && result.score <= policy.triggerScore ); if (triggeringPolicies.length === 0) { return; // No triggered policies } // Sort policies by priority (critical systems first) triggeringPolicies.sort((a, b) => { // Safety-critical systems first if configured if (this.config.prioritizeSafetyCritical) { const aSafety = a.marineSettings.safetyImpact; const bSafety = b.marineSettings.safetyImpact; if (aSafety && !bSafety) return -1; if (!aSafety && bSafety) return 1; } // Then by trigger score (lower score = higher priority) return a.triggerScore - b.triggerScore; }); // Execute recovery for the highest priority policy const policy = triggeringPolicies[0]; // Check if policy is applicable in current context if (!policy || !this.isPolicyApplicable(policy)) { this.emitEvent({ eventId: uuidv4(), timestamp: new Date(), eventType: 'policy_triggered', systemId: result.marineSystemType, checkId: result.checkId, policyId: policy ? policy.policyId : 'unknown', data: { message: `Recovery policy triggered but not applicable in current context: ${this.currentOperationalContext}`, marineContext: { operationalContext: this.currentOperationalContext, environmentalConditions: this.currentMarineEnvironment ? { ...this.currentMarineEnvironment } : undefined } } }); return; } // Check if we can start a new recovery (max concurrent actions) if (this.activeRecoveries.size >= this.config.maxConcurrentActions) { this.emitEvent({ eventId: uuidv4(), timestamp: new Date(), eventType: 'recovery_failed', systemId: result.marineSystemType, checkId: result.checkId, policyId: policy.policyId, data: { message: `Recovery delayed: maximum concurrent actions (${this.config.maxConcurrentActions}) reached`, marineContext: { operationalContext: this.currentOperationalContext, environmentalConditions: this.currentMarineEnvironment ? { ...this.currentMarineEnvironment } : undefined } } }); return; } // Start recovery process await this.executeRecoveryPolicy(policy, result); } /** * Execute a recovery policy */ private async executeRecoveryPolicy(policy: RecoveryPolicy, triggeringCheck: HealthCheckResult): Promise<void> { const recoveryId = uuidv4(); this.emitEvent({ eventId: uuidv4(), timestamp: new Date(), eventType: 'recovery_started', systemId: triggeringCheck.marineSystemType, checkId: triggeringCheck.checkId, policyId: policy.policyId, data: { message: `Starting recovery for ${triggeringCheck.name} (${triggeringCheck.status}, score: ${triggeringCheck.score})`, marineContext: { operationalContext: this.currentOperationalContext, environmentalConditions: this.currentMarineEnvironment ? { ...this.currentMarineEnvironment } : undefined } } }); // Execute actions in sequence let recoverySucceeded = false; for (const actionId of policy.actions) { const action = this.recoveryActions.get(actionId); if (!action) { this.emitEvent({ eventId: uuidv4(), timestamp: new Date(), eventType: 'recovery_failed', systemId: triggeringCheck.marineSystemType, checkId: triggeringCheck.checkId, policyId: policy.policyId, actionId, data: { message: `Recovery action not found: ${actionId}`, marineContext: { operationalContext: this.currentOperationalContext, environmentalConditions: this.currentMarineEnvironment ? { ...this.currentMarineEnvironment } : undefined } } }); continue; } // Execute the action const actionPromise = action.execute( triggeringCheck, this.currentOperationalContext, this.currentMarineEnvironment ); // Track active recovery this.activeRecoveries.set(recoveryId, actionPromise); try { const result = await actionPromise; this.recoveryHistory.push(result); // Emit event this.emitEvent({ eventId: uuidv4(), timestamp: new Date(), eventType: 'action_executed', systemId: triggeringCheck.marineSystemType, checkId: triggeringCheck.checkId, policyId: policy.policyId, actionId, data: { message: result.message, details: result.details, marineContext: { operationalContext: this.currentOperationalContext, environmentalConditions: this.currentMarineEnvironment ? { ...this.currentMarineEnvironment } : undefined } }, result }); if (result.success) { recoverySucceeded = true; if (policy.stopOnSuccess) { break; } } } catch (error) { this.emitEvent({ eventId: uuidv4(), timestamp: new Date(), eventType: 'recovery_failed', systemId: triggeringCheck.marineSystemType, checkId: triggeringCheck.checkId, policyId: policy.policyId, actionId, data: { message: `Recovery action error: ${(error as Error).message}`, marineContext: { operationalContext: this.currentOperationalContext, environmentalConditions: this.currentMarineEnvironment ? { ...this.currentMarineEnvironment } : undefined } } }); } finally { // Remove from active recoveries this.activeRecoveries.delete(recoveryId); } } // Emit final event this.emitEvent({ eventId: uuidv4(), timestamp: new Date(), eventType: recoverySucceeded ? 'recovery_completed' : 'recovery_failed', systemId: triggeringCheck.marineSystemType, checkId: triggeringCheck.checkId, policyId: policy.policyId, data: { message: recoverySucceeded ? `Recovery completed successfully for ${triggeringCheck.name}` : `Recovery failed for ${triggeringCheck.name}`, marineContext: { operationalContext: this.currentOperationalContext, environmentalConditions: this.currentMarineEnvironment ? { ...this.currentMarineEnvironment } : undefined } } }); } /** * Check if a policy is applicable in the current context */ private isPolicyApplicable(policy: RecoveryPolicy): boolean { // Check operational context if (!policy.marineSettings.activeContexts.includes(this.currentOperationalContext)) { return false; } // Check sea conditions if configured if (this.config.marineSettings.limitInRoughSeas && !policy.marineSettings.activeInRoughSeas && this.currentMarineEnvironment) { const seaState = this.currentMarineEnvironment.seaState; if (seaState === 'rough' || seaState === 'very_rough') { return false; } } // Check power status if configured if (this.config.marineSettings.powerAware && policy.marineSettings.powerAware && this.currentMarineEnvironment?.powerStatus === 'critical') { // Only allow safety-critical policies when power is critical return policy.marineSettings.safetyImpact; } // Check emergency context if (this.currentOperationalContext === OperationalContext.EMERGENCY && this.config.marineSettings.requireApprovalInEmergency) { // In emergency, only automated policies are allowed return policy.marineSettings.safetyImpact; } return true; } /** * Subscribe to recovery events */ onEvent(listener: (event: RecoveryEvent) => void): () => void { this.eventListeners.push(listener); return () => { this.eventListeners = this.eventListeners.filter(l => l !== listener); }; } /** * Emit a recovery event */ private emitEvent(event: RecoveryEvent): void { for (const listener of this.eventListeners) { try { listener(event); } catch (error) { console.error('Error in recovery event listener:', error); } } } /** * Get recovery history */ getRecoveryHistory(): RecoveryActionResult[] { return [...this.recoveryHistory]; } /** * Get active recoveries */ getActiveRecoveries(): string[] { return Array.from(this.activeRecoveries.keys()); } /** * Get registered recovery actions */ getRegisteredActions(): string[] { return Array.from(this.recoveryActions.keys()); } /** * Get registered recovery policies */ getRegisteredPolicies(): string[] { return Array.from(this.recoveryPolicies.keys()); } } /** * Create a default recovery orchestrator */ export const defaultRecoveryOrchestrator = new RecoveryOrchestrator({ automatedRecoveryEnabled: true, maxConcurrentActions: 3, defaultActionTimeout: 30000, defaultRetryCount: 2, defaultRetryDelay: 5000, prioritizeSafetyCritical: true, marineSettings: { adaptToMarineConditions: true, powerAware: true, limitInRoughSeas: true, requireApprovalInEmergency: true } });