From 7c34b4312c8aafc7643a5192d54daea14cfc1559 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Mon, 8 Jun 2026 22:49:26 +0000 Subject: ln/lightning: watchdog recycles zombie receive-listener socket MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port of master bd1ece3 to staging. When cl restarts (e.g. autoheal after a healthcheck timeout), the app's waitanyinvoice long-poll can hang on the now-dead listen socket: listenerActive stays true forever, nothing restarts it, and incoming lightning payments silently stop crediting until a manual app restart. The per-call RPC timeout can't catch this — waitanyinvoice is meant to block. ensureListenerAlive (run every 120s) now also detects a stalled listener — active but blocked in waitanyinvoice for >5min — probes cl via the main socket (getinfo), and only if cl is actually alive treats the listen socket as a zombie: bumps a listenerEpoch, calls lnListen.reset() (new: drops the proxy socket so the next call reconnects), and re-arms. The epoch guard makes the wedged invocation bow out when its waitanyinvoice finally rejects, so we never run two listeners or double-credit. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/ln.ts | 1 + 1 file changed, 1 insertion(+) (limited to 'lib/ln.ts') diff --git a/lib/ln.ts b/lib/ln.ts index 2cbd5120..c125eec4 100644 --- a/lib/ln.ts +++ b/lib/ln.ts @@ -98,6 +98,7 @@ function lightningProxy(rpcPath: string): any { get(_t, prop: string | symbol) { if (prop === "toString") return () => "[LightningProxy]"; if (prop === Symbol.toStringTag) return "LightningProxy"; + if (prop === "reset") return () => { client = null; }; // watchdog: drop a zombie socket return async (...args: any[]) => { const c = ensure(); -- cgit v1.2.3