@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
text/typescript
/**
* 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
}
});