aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--index.ts3
-rw-r--r--lib/lightning.ts39
-rw-r--r--lib/ln.ts1
3 files changed, 42 insertions, 1 deletions
diff --git a/index.ts b/index.ts
index a664638d..98a9668e 100644
--- a/index.ts
+++ b/index.ts
@@ -5,7 +5,7 @@ import { admin, auth, optional } from "$lib/auth";
import { getDelegateInfo, receiveDelegation } from "$lib/delegator";
import { g, s } from "$lib/db";
-import { fixBolt12, listenForLightning, replay } from "$lib/lightning";
+import { ensureListenerAlive, fixBolt12, listenForLightning, replay } from "$lib/lightning";
import { l } from "$lib/logging";
import { startHealthCheck } from "$lib/health";
import { getLocations } from "$lib/locations";
@@ -49,6 +49,7 @@ setTimeout(listenForLightning, 2000);
listenForDMs();
initMlsIndex();
setInterval(sendHeartbeat, 2000);
+setInterval(ensureListenerAlive, 120_000); // LN listener watchdog
app.get("/balances", info.balances);
app.get("/health", info.health);
diff --git a/lib/lightning.ts b/lib/lightning.ts
index 4eb70e00..9e259ed5 100644
--- a/lib/lightning.ts
+++ b/lib/lightning.ts
@@ -9,8 +9,13 @@ import { getInvoice, getPayment, getUser } from "$lib/utils";
const LISTENER_RETRY_DELAY = 5000; // 5 seconds
const MAX_LISTENER_RETRIES = 10;
+// If the listener has been blocked in waitanyinvoice this long without returning,
+// AND cl is reachable, treat the listen socket as a zombie and recycle it.
+const LISTENER_STALL_MS = 5 * 60 * 1000; // 5 minutes
let listenerRetries = 0;
let listenerActive = false;
+let waitStartedAt = Date.now();
+let listenerEpoch = 0;
export async function listenForLightning() {
if (listenerActive) {
@@ -19,6 +24,8 @@ export async function listenForLightning() {
}
listenerActive = true;
+ waitStartedAt = Date.now();
+ const myEpoch = listenerEpoch;
try {
const payIndex = (await g("pay_index")) || 0;
@@ -36,6 +43,9 @@ export async function listenForLightning() {
payment_preimage: preimage,
} = inv;
+ // watchdog recycled us while blocked — a fresh listener owns the stream now.
+ if (myEpoch !== listenerEpoch) return;
+
await s("pay_index", pay_index);
// Reset retry counter on successful receive
@@ -84,6 +94,7 @@ export async function listenForLightning() {
}
} catch (e: any) {
listenerActive = false;
+ if (myEpoch !== listenerEpoch) return; // watchdog already re-armed a fresh listener
const errorCode = e?.code ?? e?.errno ?? "unknown";
const errorMsg = e?.message ?? String(e);
@@ -116,6 +127,34 @@ export async function listenForLightning() {
}
}
+export async function ensureListenerAlive() {
+ if (!listenerActive) {
+ warn("lightning listener: not active, restarting");
+ listenForLightning();
+ return;
+ }
+ if (Date.now() - waitStartedAt < LISTENER_STALL_MS) return;
+ let clAlive = false;
+ try {
+ const info = await ln.getinfo();
+ clAlive = !!info?.id;
+ } catch (_) {
+ clAlive = false;
+ }
+ if (!clAlive) return; // cl itself down — the retry/exit path handles that
+ err(
+ `lightning listener: stalled ${Math.round(
+ (Date.now() - waitStartedAt) / 1000,
+ )}s while cl is alive — recycling listen socket`,
+ );
+ listenerEpoch++;
+ try {
+ (lnListen as any).reset?.();
+ } catch (_) {}
+ listenerActive = false;
+ setTimeout(listenForLightning);
+}
+
export async function replay(index) {
const inv = await lnListen.waitanyinvoice(index - 1);
const {
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();