UNPKG

devflow-ai

Version:

Enterprise-grade AI agent orchestration with swarm management UI dashboard

318 lines (263 loc) 8.07 kB
/** * Connection Health Monitor for MCP * Monitors connection health and triggers recovery when needed */ import { EventEmitter } from 'node:events'; import type { ILogger } from '../../core/logger.js'; import type { MCPClient } from '../client.js'; export interface HealthStatus { healthy: boolean; lastHeartbeat: Date; missedHeartbeats: number; latency: number; connectionState: 'connected' | 'disconnected' | 'reconnecting'; error?: string; } export interface HealthMonitorConfig { heartbeatInterval: number; heartbeatTimeout: number; maxMissedHeartbeats: number; enableAutoRecovery: boolean; } export class ConnectionHealthMonitor extends EventEmitter { private heartbeatTimer?: NodeJS.Timeout; private timeoutTimer?: NodeJS.Timeout; private lastHeartbeat: Date = new Date(); private missedHeartbeats = 0; private currentLatency = 0; private isMonitoring = false; private healthStatus: HealthStatus; private readonly defaultConfig: HealthMonitorConfig = { heartbeatInterval: 5000, heartbeatTimeout: 10000, maxMissedHeartbeats: 3, enableAutoRecovery: true, }; constructor( private client: MCPClient, private logger: ILogger, config?: Partial<HealthMonitorConfig>, ) { super(); this.config = { ...this.defaultConfig, ...config }; this.healthStatus = { healthy: false, lastHeartbeat: new Date(), missedHeartbeats: 0, latency: 0, connectionState: 'disconnected', }; } private config: HealthMonitorConfig; /** * Start health monitoring */ async start(): Promise<void> { if (this.isMonitoring) { this.logger.warn('Health monitor already running'); return; } this.logger.info('Starting connection health monitor', { config: this.config, }); this.isMonitoring = true; this.missedHeartbeats = 0; this.lastHeartbeat = new Date(); // Start heartbeat cycle this.scheduleHeartbeat(); // Update initial status this.updateHealthStatus('connected'); this.emit('started'); } /** * Stop health monitoring */ async stop(): Promise<void> { if (!this.isMonitoring) { return; } this.logger.info('Stopping connection health monitor'); this.isMonitoring = false; if (this.heartbeatTimer) { clearTimeout(this.heartbeatTimer); this.heartbeatTimer = undefined; } if (this.timeoutTimer) { clearTimeout(this.timeoutTimer); this.timeoutTimer = undefined; } this.updateHealthStatus('disconnected'); this.emit('stopped'); } /** * Get current health status */ getHealthStatus(): HealthStatus { return { ...this.healthStatus }; } /** * Check connection health immediately */ async checkHealth(): Promise<HealthStatus> { try { const startTime = Date.now(); // Send heartbeat ping await this.sendHeartbeat(); // Calculate latency this.currentLatency = Date.now() - startTime; this.lastHeartbeat = new Date(); this.missedHeartbeats = 0; this.updateHealthStatus('connected', true); return this.getHealthStatus(); } catch (error) { this.logger.error('Health check failed', error); this.handleHeartbeatFailure(error as Error); return this.getHealthStatus(); } } /** * Force a health check */ async forceCheck(): Promise<void> { this.logger.debug('Forcing health check'); // Cancel current timers if (this.heartbeatTimer) { clearTimeout(this.heartbeatTimer); } if (this.timeoutTimer) { clearTimeout(this.timeoutTimer); } // Perform immediate check await this.performHeartbeat(); } private scheduleHeartbeat(): void { if (!this.isMonitoring) { return; } this.heartbeatTimer = setTimeout(() => { this.performHeartbeat().catch((error) => { this.logger.error('Heartbeat error', error); }); }, this.config.heartbeatInterval); } private async performHeartbeat(): Promise<void> { if (!this.isMonitoring) { return; } this.logger.debug('Performing heartbeat'); try { // Set timeout for heartbeat response this.setHeartbeatTimeout(); const startTime = Date.now(); await this.sendHeartbeat(); // Clear timeout on success this.clearHeartbeatTimeout(); // Update metrics this.currentLatency = Date.now() - startTime; this.lastHeartbeat = new Date(); this.missedHeartbeats = 0; this.logger.debug('Heartbeat successful', { latency: this.currentLatency, }); this.updateHealthStatus('connected', true); // Schedule next heartbeat this.scheduleHeartbeat(); } catch (error) { this.handleHeartbeatFailure(error as Error); } } private async sendHeartbeat(): Promise<void> { // Send heartbeat notification via MCP await this.client.notify('heartbeat', { timestamp: Date.now(), sessionId: this.generateSessionId(), }); } private setHeartbeatTimeout(): void { this.timeoutTimer = setTimeout(() => { this.handleHeartbeatTimeout(); }, this.config.heartbeatTimeout); } private clearHeartbeatTimeout(): void { if (this.timeoutTimer) { clearTimeout(this.timeoutTimer); this.timeoutTimer = undefined; } } private handleHeartbeatTimeout(): void { this.logger.warn('Heartbeat timeout'); this.handleHeartbeatFailure(new Error('Heartbeat timeout')); } private handleHeartbeatFailure(error: Error): void { this.clearHeartbeatTimeout(); this.missedHeartbeats++; this.logger.warn('Heartbeat failed', { missedHeartbeats: this.missedHeartbeats, maxMissed: this.config.maxMissedHeartbeats, error: error instanceof Error ? error.message : String(error), }); if (this.missedHeartbeats >= this.config.maxMissedHeartbeats) { this.logger.error('Max missed heartbeats exceeded, connection unhealthy'); this.updateHealthStatus( 'disconnected', false, error instanceof Error ? error.message : String(error), ); if (this.config.enableAutoRecovery) { this.emit('connectionLost', { error }); } } else { // Schedule next heartbeat with backoff const backoffDelay = this.config.heartbeatInterval * (this.missedHeartbeats + 1); this.logger.debug('Scheduling heartbeat with backoff', { delay: backoffDelay }); this.heartbeatTimer = setTimeout(() => { this.performHeartbeat().catch((err) => { this.logger.error('Backoff heartbeat error', err); }); }, backoffDelay); } } private updateHealthStatus( connectionState: 'connected' | 'disconnected' | 'reconnecting', healthy?: boolean, error?: string, ): void { const previousStatus = { ...this.healthStatus }; this.healthStatus = { healthy: healthy ?? connectionState === 'connected', lastHeartbeat: this.lastHeartbeat, missedHeartbeats: this.missedHeartbeats, latency: this.currentLatency, connectionState, error, }; // Emit event if health changed if ( previousStatus.healthy !== this.healthStatus.healthy || previousStatus.connectionState !== this.healthStatus.connectionState ) { this.logger.info('Health status changed', { from: previousStatus.connectionState, to: this.healthStatus.connectionState, healthy: this.healthStatus.healthy, }); this.emit('healthChange', this.healthStatus, previousStatus); } } private generateSessionId(): string { return `session-${Date.now()}-${Math.random().toString(36).slice(2)}`; } /** * Reset monitor state */ reset(): void { this.missedHeartbeats = 0; this.currentLatency = 0; this.lastHeartbeat = new Date(); if (this.isMonitoring) { this.logger.debug('Resetting health monitor'); this.clearHeartbeatTimeout(); this.scheduleHeartbeat(); } } }