import { DyFM_Log, second } from '@futdevpro/fsm-dynamo'; /** * DyNTS Mongo reconnect-guard (2026-06-20) — TÚLÉLI a Mongo-konténer IP-VÁLTÁSÁT. * * PROBLÉMA (élőben, 2026-06-19): a MongoDB-driver a hostnevet (`mongodb`) connect-kor EGYSZER * resolválja és cache-eli az IP-t (pl. 172.18.0.20); standalone-nál disconnect után UGYANAZT az * IP-t pingeli, NEM re-resolve-ol. Ha a Mongo-konténert recreate-elik (új docker-network IP), a * driver örökre a HALOTT IP-n lóg → `connect ECONNREFUSED :29017` + `buffering timed out`, * és a service magától SOHA nem áll vissza (csak külső restart-tal). Ez a 2026-06-19 incidens fél * test-stacket levitt (auth/ftp/helocia/futdevpro/… mind a régi Mongo-IP-n ragadt). * * MEGOLDÁS: a guard figyeli a `mongoose.connection.readyState`-et; ha TARTÓSAN nem-connected * (a driver saját reconnect-grace-e UTÁN is), **TELJES `disconnect()` + `connect()`-et** csinál * → ÚJ MongoClient → ÚJ DNS-resolve → új IP → reconnect. Így minden DyNTS_App-service magától * túléli a Mongo IP-váltását, KÜLSŐ beavatkozás nélkül. Best-effort, sose dob; csak konténer-IP- * frissítés, NEM törli/írja az adatot. * * SAFE always-on: egy app újracsatlakozása a SAJÁT DB-jéhez korrekt, nem-destruktív viselkedés * (nincs az infra-restart-érzékenység, ami a külső watchdog-oknál — FR-229/237). Csak akkor * avatkozik be, ha readyState !== 1 a grace + cooldown letelte UTÁN is; healthy connection-t (===1) * SOHA nem bánt. * * A döntés-mag (`shouldForceMongoReconnect`) PURE + unit-tesztelt; a futtató injektált dep-ekkel * dolgozik (readyState-olvasás / reconnect / log) a tesztelhetőségért. */ /** A pure döntés inputja (minden numerikus → determinisztikus + tesztelhető). */ export interface MongoReconnectState { /** mongoose.connection.readyState: 0=disconnected, 1=connected, 2=connecting, 3=disconnecting. */ readyState: number; /** Mióta (ms) nem-connected a readyState (0, ha épp connected). */ msSinceUnhealthy: number; /** Grace: ennyi ideig hagyjuk a drivert SAJÁT magát visszahozni, mielőtt force-reconnectolnánk. */ unhealthyGraceMs: number; /** Épp folyamatban van-e már egy force-reconnect (ne torlódjon). */ reconnectInFlight: boolean; /** Mióta (ms) volt az utolsó force-reconnect (Infinity, ha még nem volt). */ msSinceLastReconnect: number; /** Két force-reconnect közti minimum (anti-thrash). */ reconnectCooldownMs: number; } /** A pure döntés eredménye. */ export interface MongoReconnectDecision { reconnect: boolean; reason: string; } /** * PURE döntés: kell-e force-reconnect. Healthy (===1) / connecting (===2) / in-flight / grace-en * vagy cooldown-on belül → NEM. Csak tartós nem-connected (disconnected/disconnecting) + grace és * cooldown letelt + nincs in-flight → IGEN. */ export function shouldForceMongoReconnect(s: MongoReconnectState): MongoReconnectDecision { if (s.readyState === 1) { return { reconnect: false, reason: 'connected (readyState=1)' }; } if (s.readyState === 2) { return { reconnect: false, reason: 'connecting (readyState=2) — letting the driver finish' }; } if (s.reconnectInFlight) { return { reconnect: false, reason: 'a force-reconnect is already in flight' }; } if (s.msSinceUnhealthy < s.unhealthyGraceMs) { return { reconnect: false, reason: `unhealthy for ${Math.round(s.msSinceUnhealthy / 1000)}s (< ${Math.round(s.unhealthyGraceMs / 1000)}s grace) — letting the driver self-heal first`, }; } if (s.msSinceLastReconnect < s.reconnectCooldownMs) { return { reconnect: false, reason: `last force-reconnect ${Math.round(s.msSinceLastReconnect / 1000)}s ago (< ${Math.round(s.reconnectCooldownMs / 1000)}s cooldown)`, }; } return { reconnect: true, reason: `Mongo connection unhealthy (readyState=${s.readyState}) for ${Math.round(s.msSinceUnhealthy / 1000)}s past grace — forcing disconnect+connect to re-resolve the host IP`, }; } /** Injektált függőségek a futtatóhoz (best-effort; a futtató sose dob). */ export interface MongoReconnectGuardDeps { /** A jelenlegi `mongoose.connection.readyState`. */ getReadyState: () => number; /** * Force-reconnect: `disconnect()` majd `connect(uri, opts)` — ÚJ MongoClient → ÚJ DNS-resolve. * Dobhat (a connect elbukhat, ha a Mongo épp tényleg down) — a futtató elkapja. */ reconnect: () => Promise; /** Log. */ log: (msg: string) => void; /** Tick-intervallum (ms). Default 10s. */ intervalMs?: number; /** Grace, amíg a driver saját reconnect-jét várjuk (ms). Default 30s. */ unhealthyGraceMs?: number; /** Cooldown két force-reconnect közt (ms). Default 20s. */ reconnectCooldownMs?: number; } /** * Elindítja a periodikus reconnect-guard-ot. A visszaadott függvény leállítja (teszt/teardown). * `.unref()` — nem tartja életben az event-loop-ot. */ export function startMongoReconnectGuard(deps: MongoReconnectGuardDeps): () => void { const intervalMs: number = deps.intervalMs ?? 10 * second; const unhealthyGraceMs: number = deps.unhealthyGraceMs ?? 30 * second; const reconnectCooldownMs: number = deps.reconnectCooldownMs ?? 20 * second; let unhealthySinceMs: number | null = null; let lastReconnectAtMs: number | null = null; let inFlight: boolean = false; let ticking: boolean = false; const tick = async (): Promise => { if (ticking) { return; } ticking = true; try { const nowMs: number = Date.now(); const readyState: number = deps.getReadyState(); if (readyState === 1) { if (unhealthySinceMs !== null) { deps.log(`[mongo-reconnect-guard] Mongo connection healthy again (was unhealthy ${Math.round((nowMs - unhealthySinceMs) / 1000)}s)`); } unhealthySinceMs = null; return; } // Nem-connected: indítsuk/folytassuk az unhealthy-órát. if (unhealthySinceMs === null) { unhealthySinceMs = nowMs; } const decision: MongoReconnectDecision = shouldForceMongoReconnect({ readyState: readyState, msSinceUnhealthy: nowMs - unhealthySinceMs, unhealthyGraceMs: unhealthyGraceMs, reconnectInFlight: inFlight, msSinceLastReconnect: lastReconnectAtMs === null ? Number.POSITIVE_INFINITY : nowMs - lastReconnectAtMs, reconnectCooldownMs: reconnectCooldownMs, }); if (!decision.reconnect) { return; } deps.log(`[mongo-reconnect-guard] ${decision.reason}`); inFlight = true; lastReconnectAtMs = nowMs; try { await deps.reconnect(); deps.log('[mongo-reconnect-guard] force-reconnect dispatched ✓ (re-resolved host; awaiting connection open)'); } catch (e: unknown) { // A connect elbukhat, ha a Mongo épp tényleg elérhetetlen — nem baj, a következő tick // (cooldown után) újrapróbál; amint a Mongo új IP-n elérhető, a re-resolve becsatlakozik. deps.log(`[mongo-reconnect-guard] force-reconnect attempt failed (will retry after cooldown): ${e instanceof Error ? e.message : String(e)}`); } finally { inFlight = false; } } catch (e: unknown) { deps.log(`[mongo-reconnect-guard] tick error (non-fatal): ${e instanceof Error ? e.message : String(e)}`); } finally { ticking = false; } }; const handle: NodeJS.Timeout = setInterval((): void => { void tick(); }, intervalMs); if (typeof handle.unref === 'function') { handle.unref(); } DyFM_Log.info( `[mongo-reconnect-guard] started — check every ${Math.round(intervalMs / 1000)}s, ` + `force disconnect+reconnect after ${Math.round(unhealthyGraceMs / 1000)}s sustained-unhealthy (re-resolves host IP), cooldown ${Math.round(reconnectCooldownMs / 1000)}s`, ); return (): void => { clearInterval(handle); }; }