diff --git a/electron/gateway/manager.ts b/electron/gateway/manager.ts index 059dc13d..7f4bcf85 100644 --- a/electron/gateway/manager.ts +++ b/electron/gateway/manager.ts @@ -196,7 +196,7 @@ export class GatewayManager extends EventEmitter { private reconnectSuccessTotal = 0; private static readonly HEARTBEAT_INTERVAL_MS = 60_000; private static readonly HEARTBEAT_TIMEOUT_MS = 30_000; - private static readonly HEARTBEAT_MAX_MISSES = 10; + private static readonly HEARTBEAT_MAX_MISSES = 4; public static readonly RESTART_COOLDOWN_MS = 5_000; private static readonly GATEWAY_READY_FALLBACK_PROBE_DELAYS_MS = [1_500, 3_000, 5_000, 8_000, 12_000, 30_000] as const; private lastRestartAt = 0; diff --git a/electron/utils/gateway-health.ts b/electron/utils/gateway-health.ts index a6d37165..4005f2d2 100644 --- a/electron/utils/gateway-health.ts +++ b/electron/utils/gateway-health.ts @@ -13,7 +13,7 @@ type BuildGatewayHealthSummaryOptions = { }; const CHANNEL_STATUS_FAILURE_WINDOW_MS = 2 * 60_000; -const HEARTBEAT_MISS_THRESHOLD = 10; +const HEARTBEAT_MISS_THRESHOLD = 4; export function buildGatewayHealthSummary( options: BuildGatewayHealthSummaryOptions, diff --git a/tests/unit/gateway-manager-diagnostics.test.ts b/tests/unit/gateway-manager-diagnostics.test.ts index 696fa63c..ff52cc23 100644 --- a/tests/unit/gateway-manager-diagnostics.test.ts +++ b/tests/unit/gateway-manager-diagnostics.test.ts @@ -215,7 +215,7 @@ describe('GatewayManager diagnostics', () => { expect(snapshot.core.rpcRouter).toBe('blocked'); }); - it('marks windows gateway health unresponsive and restarts after ten heartbeat misses', async () => { + it('marks windows gateway health unresponsive and restarts after four heartbeat misses', async () => { Object.defineProperty(process, 'platform', { value: 'win32' }); const { GatewayManager } = await import('@electron/gateway/manager'); @@ -239,10 +239,10 @@ describe('GatewayManager diagnostics', () => { const restartSpy = vi.spyOn(manager, 'restart').mockResolvedValue(); (manager as unknown as { startPing: () => void }).startPing(); - vi.advanceTimersByTime(660_000); + vi.advanceTimersByTime(300_000); expect(restartSpy).toHaveBeenCalledTimes(1); - expect(manager.getDiagnostics().consecutiveHeartbeatMisses).toBe(10); + expect(manager.getDiagnostics().consecutiveHeartbeatMisses).toBe(4); const health = buildGatewayHealthSummary({ status: { state: 'running', port: 18789 }, diff --git a/tests/unit/gateway-manager-heartbeat.test.ts b/tests/unit/gateway-manager-heartbeat.test.ts index 4ac9d016..68ffd238 100644 --- a/tests/unit/gateway-manager-heartbeat.test.ts +++ b/tests/unit/gateway-manager-heartbeat.test.ts @@ -28,7 +28,7 @@ describe('GatewayManager heartbeat recovery', () => { Object.defineProperty(process, 'platform', { value: originalPlatform }); }); - it('restarts only after ten consecutive heartbeat misses', async () => { + it('restarts only after four consecutive heartbeat misses', async () => { const { GatewayManager } = await import('@electron/gateway/manager'); const manager = new GatewayManager(); @@ -49,13 +49,13 @@ describe('GatewayManager heartbeat recovery', () => { const restartSpy = vi.spyOn(manager, 'restart').mockResolvedValue(); (manager as unknown as { startPing: () => void }).startPing(); - vi.advanceTimersByTime(659_999); + vi.advanceTimersByTime(299_999); - expect(ws.ping).toHaveBeenCalledTimes(10); + expect(ws.ping).toHaveBeenCalledTimes(4); expect( (manager as unknown as { connectionMonitor: { getConsecutiveMisses: () => number } }) .connectionMonitor.getConsecutiveMisses(), - ).toBe(9); + ).toBe(3); expect(restartSpy).not.toHaveBeenCalled(); expect(manager.getDiagnostics().consecutiveHeartbeatMisses).toBe(0); @@ -63,7 +63,7 @@ describe('GatewayManager heartbeat recovery', () => { expect(ws.terminate).not.toHaveBeenCalled(); expect(restartSpy).toHaveBeenCalledTimes(1); - expect(manager.getDiagnostics().consecutiveHeartbeatMisses).toBe(10); + expect(manager.getDiagnostics().consecutiveHeartbeatMisses).toBe(4); expect(manager.getDiagnostics().lastHeartbeatTimeoutAt).toBe(Date.now()); vi.advanceTimersByTime(180_000); @@ -72,7 +72,7 @@ describe('GatewayManager heartbeat recovery', () => { (manager as unknown as { connectionMonitor: { clear: () => void } }).connectionMonitor.clear(); }); - it('does not restart after ten misses when auto-reconnect is disabled', async () => { + it('does not restart after four misses when auto-reconnect is disabled', async () => { const { GatewayManager } = await import('@electron/gateway/manager'); const manager = new GatewayManager(); @@ -92,16 +92,16 @@ describe('GatewayManager heartbeat recovery', () => { const restartSpy = vi.spyOn(manager, 'restart').mockResolvedValue(); (manager as unknown as { startPing: () => void }).startPing(); - vi.advanceTimersByTime(660_000); + vi.advanceTimersByTime(300_000); - expect(manager.getDiagnostics().consecutiveHeartbeatMisses).toBe(10); + expect(manager.getDiagnostics().consecutiveHeartbeatMisses).toBe(4); expect(ws.terminate).not.toHaveBeenCalled(); expect(restartSpy).not.toHaveBeenCalled(); (manager as unknown as { connectionMonitor: { clear: () => void } }).connectionMonitor.clear(); }); - it('requires ten new consecutive misses after responsiveness recovers', async () => { + it('requires four new consecutive misses after responsiveness recovers', async () => { const { GatewayManager } = await import('@electron/gateway/manager'); const manager = new GatewayManager(); @@ -121,29 +121,29 @@ describe('GatewayManager heartbeat recovery', () => { const restartSpy = vi.spyOn(manager, 'restart').mockResolvedValue(); (manager as unknown as { startPing: () => void }).startPing(); - vi.advanceTimersByTime(600_000); + vi.advanceTimersByTime(240_000); expect( (manager as unknown as { connectionMonitor: { getConsecutiveMisses: () => number } }) .connectionMonitor.getConsecutiveMisses(), - ).toBe(9); + ).toBe(3); (manager as unknown as { handleMessage: (message: unknown) => void }).handleMessage('alive'); expect(manager.getDiagnostics().consecutiveHeartbeatMisses).toBe(0); expect(manager.getDiagnostics().lastAliveAt).toBe(Date.now()); - vi.advanceTimersByTime(600_000); + vi.advanceTimersByTime(240_000); expect(restartSpy).not.toHaveBeenCalled(); vi.advanceTimersByTime(60_000); expect(ws.terminate).not.toHaveBeenCalled(); expect(restartSpy).toHaveBeenCalledTimes(1); - expect(manager.getDiagnostics().consecutiveHeartbeatMisses).toBe(10); + expect(manager.getDiagnostics().consecutiveHeartbeatMisses).toBe(4); (manager as unknown as { connectionMonitor: { clear: () => void } }).connectionMonitor.clear(); }); - it('restarts after ten consecutive heartbeat misses on windows', async () => { + it('restarts after four consecutive heartbeat misses on windows', async () => { Object.defineProperty(process, 'platform', { value: 'win32' }); const { GatewayManager } = await import('@electron/gateway/manager'); @@ -165,12 +165,12 @@ describe('GatewayManager heartbeat recovery', () => { const restartSpy = vi.spyOn(manager, 'restart').mockResolvedValue(); (manager as unknown as { startPing: () => void }).startPing(); - vi.advanceTimersByTime(660_000); + vi.advanceTimersByTime(300_000); - expect(ws.ping).toHaveBeenCalledTimes(10); + expect(ws.ping).toHaveBeenCalledTimes(4); expect(ws.terminate).not.toHaveBeenCalled(); expect(restartSpy).toHaveBeenCalledTimes(1); - expect(manager.getDiagnostics().consecutiveHeartbeatMisses).toBe(10); + expect(manager.getDiagnostics().consecutiveHeartbeatMisses).toBe(4); (manager as unknown as { connectionMonitor: { clear: () => void } }).connectionMonitor.clear(); });