Idle timeout default 60 s; lazy periodic autoreconnect every 5 min (v0.21.3)
CI / build + unit (pull_request) Failing after 22s
CI / build + unit (pull_request) Failing after 22s
The v0.21.2 frame-based watchdog works in production (it fired today: 'no decoded bridge frames for 628474 ms — forcing reconnect', and the reconnect delivered queued packets), but the bridge/radio side at 192.168.1.4 keeps killing inbound RX within ~20 min of a fresh sync, repeatedly. Two tuning changes requested by the owner: - MESH_IDLE_TIMEOUT_MS default lowered from 600000 (10 min) to 60000 (1 min). - New lazy periodic autoreconnect: MESH_RECONNECT_INTERVAL_MS (new env, default 300000 = 5 min, read at construction like the others). An interval started in onModuleInit unconditionally logs and destroys the socket so the existing close -> scheduleReconnect loop re-establishes with a fresh sync on schedule; cleared in onModuleDestroy. When not connected at tick time, destroy is a no-op and the retry loop owns recovery — no special-casing. Tests: 171 -> 172 — periodic reconnect observed end-to-end with an overridden short interval (fake bridge disconnect + re-establish); existing watchdog tests pass unchanged, comments updated to the new 60 s default.
This commit is contained in:
@@ -258,7 +258,7 @@ describe('MeshService idle watchdog', () => {
|
||||
process.env.MESH_HOST = '127.0.0.1';
|
||||
process.env.MESH_PORT = String(bridge.port);
|
||||
process.env.MESH_RECONNECT_MS = '50'; // fast retry so the re-establishment is observable
|
||||
process.env.MESH_IDLE_TIMEOUT_MS = '100'; // override — the default (600000) would never fire in a test
|
||||
process.env.MESH_IDLE_TIMEOUT_MS = '100'; // override — the default (60000) would never fire in a test
|
||||
const svc = new MeshService();
|
||||
try {
|
||||
svc.onModuleInit();
|
||||
@@ -285,7 +285,7 @@ describe('MeshService idle watchdog', () => {
|
||||
process.env.MESH_HOST = '127.0.0.1';
|
||||
process.env.MESH_PORT = String(bridge.port);
|
||||
process.env.MESH_RECONNECT_MS = '50'; // fast retry so the re-establishment is observable
|
||||
process.env.MESH_IDLE_TIMEOUT_MS = '100'; // override — the default (600000) would never fire in a test
|
||||
process.env.MESH_IDLE_TIMEOUT_MS = '100'; // override — the default (60000) would never fire in a test
|
||||
const svc = new MeshService();
|
||||
try {
|
||||
svc.onModuleInit();
|
||||
@@ -376,7 +376,7 @@ describe('MeshService idle watchdog', () => {
|
||||
const bridge = await startBridge();
|
||||
process.env.MESH_HOST = '127.0.0.1';
|
||||
process.env.MESH_PORT = String(bridge.port);
|
||||
delete process.env.MESH_IDLE_TIMEOUT_MS; // default 600000 — a test can't wait that long, so silence must NOT fire
|
||||
delete process.env.MESH_IDLE_TIMEOUT_MS; // default 60000 — a test can't wait that long, so silence must NOT fire
|
||||
const svc = new MeshService();
|
||||
try {
|
||||
svc.onModuleInit();
|
||||
@@ -414,6 +414,31 @@ describe('MeshService idle watchdog', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('MeshService periodic autoreconnect', () => {
|
||||
it('unconditionally reconnects when MESH_RECONNECT_INTERVAL_MS elapses', async () => {
|
||||
const bridge = await startBridge();
|
||||
process.env.MESH_HOST = '127.0.0.1';
|
||||
process.env.MESH_PORT = String(bridge.port);
|
||||
process.env.MESH_RECONNECT_MS = '50'; // fast retry so the re-establishment is observable
|
||||
process.env.MESH_RECONNECT_INTERVAL_MS = '100'; // override — the default (300000) would never elapse in a test
|
||||
const svc = new MeshService();
|
||||
try {
|
||||
svc.onModuleInit();
|
||||
await waitFor(
|
||||
() => bridge.sockets.length === 1 && svc.status().connected,
|
||||
);
|
||||
|
||||
// interval fires → destroy → existing close/retry loop re-establishes with a fresh session
|
||||
await waitFor(() => bridge.sockets.length >= 2 && svc.status().connected);
|
||||
} finally {
|
||||
delete process.env.MESH_RECONNECT_MS;
|
||||
delete process.env.MESH_RECONNECT_INTERVAL_MS;
|
||||
svc.onModuleDestroy();
|
||||
bridge.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('MeshService send', () => {
|
||||
it('sends on the test channel (index 3) by default, never primary', async () => {
|
||||
const bridge = await startBridge();
|
||||
|
||||
@@ -212,7 +212,12 @@ export class MeshService implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly pollMs = parseInt(process.env.MESH_POLL_MS ?? '1000', 10);
|
||||
// idle watchdog: force a reconnect when an ESTABLISHED connection stops decoding frames — the zombie-bridge case where outbound sends keep working but inbound frame flow is dead (raw non-frame bytes do NOT reset the clock). Read at construction like the other env config.
|
||||
private readonly idleTimeoutMs = parseInt(
|
||||
process.env.MESH_IDLE_TIMEOUT_MS ?? '600000',
|
||||
process.env.MESH_IDLE_TIMEOUT_MS ?? '60000',
|
||||
10,
|
||||
);
|
||||
// lazy periodic autoreconnect: unconditionally destroy + re-establish every N ms so a fresh sync happens on schedule — the bridge/radio side tends to kill inbound RX within ~20 min of a fresh sync. Read at construction like the other env config.
|
||||
private readonly reconnectIntervalMs = parseInt(
|
||||
process.env.MESH_RECONNECT_INTERVAL_MS ?? '300000',
|
||||
10,
|
||||
);
|
||||
// cap on undecoded frame buffer: a desynced stream that never forms frames would grow it without bound — force a reconnect instead. Env-overridable so tests can trigger it (Node caps socket 'data' events at ~64 KB).
|
||||
@@ -270,6 +275,7 @@ export class MeshService implements OnModuleInit, OnModuleDestroy {
|
||||
/** epoch ms when the last valid frame was decoded from the bridge; reset on connect — idle watchdog baseline */
|
||||
private lastPacketAt = Date.now();
|
||||
private idleTimer: NodeJS.Timeout | null = null;
|
||||
private periodicReconnectTimer: NodeJS.Timeout | null = null;
|
||||
private shuttingDown = false;
|
||||
|
||||
onModuleInit(): void {
|
||||
@@ -281,6 +287,13 @@ export class MeshService implements OnModuleInit, OnModuleDestroy {
|
||||
}
|
||||
// idle watchdog: catches established-but-silent connections (zombie bridge) that the connect/retry loop never sees
|
||||
this.idleTimer = setInterval(() => this.checkIdle(), IDLE_CHECK_MS);
|
||||
// lazy periodic autoreconnect: unconditional fresh sync on a fixed schedule; when not connected, destroy is a no-op and the retry loop owns recovery
|
||||
this.periodicReconnectTimer = setInterval(() => {
|
||||
this.logger.log(
|
||||
'periodic reconnect — MESH_RECONNECT_INTERVAL_MS elapsed',
|
||||
);
|
||||
this.socket?.destroy();
|
||||
}, this.reconnectIntervalMs);
|
||||
this.connect();
|
||||
}
|
||||
|
||||
@@ -289,6 +302,7 @@ export class MeshService implements OnModuleInit, OnModuleDestroy {
|
||||
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
||||
if (this.pollTimer) clearInterval(this.pollTimer);
|
||||
if (this.idleTimer) clearInterval(this.idleTimer);
|
||||
if (this.periodicReconnectTimer) clearInterval(this.periodicReconnectTimer);
|
||||
this.socket?.destroy();
|
||||
this.db?.close();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user