feat: implement telemetry system for application usage tracking
- Added telemetry utility to capture application events and metrics. - Integrated PostHog for event tracking with distinct user identification. - Implemented telemetry initialization, event capturing, and shutdown procedures. feat: add UV environment setup for Python management - Created utilities to manage Python installation and configuration. - Implemented network optimization checks for Python installation mirrors. - Added functions to set up managed Python environments with error handling. feat: enhance host API communication with token management - Introduced host API token retrieval and management for secure requests. - Updated host API fetch functions to include token in headers. - Added support for creating event sources with authentication. test: add comprehensive tests for gateway protocol and startup helpers - Implemented unit tests for gateway protocol helpers, event dispatching, and state management. - Added tests for startup recovery strategies and process policies. - Ensured coverage for connection monitoring and restart governance logic.
This commit is contained in:
122
electron/gateway/connection-monitor.ts
Normal file
122
electron/gateway/connection-monitor.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import logManager from '@electron/service/logger';
|
||||
|
||||
type HealthResult = { ok: boolean; error?: string };
|
||||
type HeartbeatAliveReason = 'pong' | 'message';
|
||||
|
||||
type PingOptions = {
|
||||
sendPing: () => void;
|
||||
onHeartbeatTimeout: (context: { consecutiveMisses: number; timeoutMs: number }) => void;
|
||||
intervalMs?: number;
|
||||
timeoutMs?: number;
|
||||
maxConsecutiveMisses?: number;
|
||||
};
|
||||
|
||||
export class GatewayConnectionMonitor {
|
||||
private pingInterval: NodeJS.Timeout | null = null;
|
||||
private healthCheckInterval: NodeJS.Timeout | null = null;
|
||||
private lastPingAt = 0;
|
||||
private waitingForAlive = false;
|
||||
private consecutiveMisses = 0;
|
||||
private timeoutTriggered = false;
|
||||
|
||||
startPing(options: PingOptions): void {
|
||||
const intervalMs = options.intervalMs ?? 30000;
|
||||
const timeoutMs = options.timeoutMs ?? 10000;
|
||||
const maxConsecutiveMisses = Math.max(1, options.maxConsecutiveMisses ?? 3);
|
||||
this.resetHeartbeatState();
|
||||
|
||||
if (this.pingInterval) {
|
||||
clearInterval(this.pingInterval);
|
||||
}
|
||||
|
||||
this.pingInterval = setInterval(() => {
|
||||
const now = Date.now();
|
||||
|
||||
if (this.waitingForAlive && now - this.lastPingAt >= timeoutMs) {
|
||||
this.waitingForAlive = false;
|
||||
this.consecutiveMisses += 1;
|
||||
logManager.warn(
|
||||
`Gateway heartbeat missed (${this.consecutiveMisses}/${maxConsecutiveMisses}, timeout=${timeoutMs}ms)`,
|
||||
);
|
||||
if (this.consecutiveMisses >= maxConsecutiveMisses && !this.timeoutTriggered) {
|
||||
this.timeoutTriggered = true;
|
||||
options.onHeartbeatTimeout({
|
||||
consecutiveMisses: this.consecutiveMisses,
|
||||
timeoutMs,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
options.sendPing();
|
||||
this.waitingForAlive = true;
|
||||
this.lastPingAt = now;
|
||||
}, intervalMs);
|
||||
}
|
||||
|
||||
markAlive(reason: HeartbeatAliveReason): void {
|
||||
if (this.consecutiveMisses > 0) {
|
||||
logManager.debug(`Gateway heartbeat recovered via ${reason} (misses=${this.consecutiveMisses})`);
|
||||
}
|
||||
this.waitingForAlive = false;
|
||||
this.consecutiveMisses = 0;
|
||||
this.timeoutTriggered = false;
|
||||
}
|
||||
|
||||
handlePong(): void {
|
||||
this.markAlive('pong');
|
||||
}
|
||||
|
||||
getConsecutiveMisses(): number {
|
||||
return this.consecutiveMisses;
|
||||
}
|
||||
|
||||
startHealthCheck(options: {
|
||||
shouldCheck: () => boolean;
|
||||
checkHealth: () => Promise<HealthResult>;
|
||||
onUnhealthy: (errorMessage: string) => void;
|
||||
onError: (error: unknown) => void;
|
||||
intervalMs?: number;
|
||||
}): void {
|
||||
if (this.healthCheckInterval) {
|
||||
clearInterval(this.healthCheckInterval);
|
||||
}
|
||||
|
||||
this.healthCheckInterval = setInterval(async () => {
|
||||
if (!options.shouldCheck()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const health = await options.checkHealth();
|
||||
if (!health.ok) {
|
||||
const errorMessage = health.error ?? 'Health check failed';
|
||||
logManager.warn(`Gateway health check failed: ${errorMessage}`);
|
||||
options.onUnhealthy(errorMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
logManager.error('Gateway health check error:', error);
|
||||
options.onError(error);
|
||||
}
|
||||
}, options.intervalMs ?? 30000);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
if (this.pingInterval) {
|
||||
clearInterval(this.pingInterval);
|
||||
this.pingInterval = null;
|
||||
}
|
||||
if (this.healthCheckInterval) {
|
||||
clearInterval(this.healthCheckInterval);
|
||||
this.healthCheckInterval = null;
|
||||
}
|
||||
this.resetHeartbeatState();
|
||||
}
|
||||
|
||||
private resetHeartbeatState(): void {
|
||||
this.lastPingAt = 0;
|
||||
this.waitingForAlive = false;
|
||||
this.consecutiveMisses = 0;
|
||||
this.timeoutTriggered = false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user