import * as v8 from 'v8'; import { PerformanceObserver } from 'perf_hooks'; import { DyFM_Error, DyFM_ErrorLevel, DyFM_Log } from '@futdevpro/fsm-dynamo'; import { DyNTS_global_settings } from '../../_collections/global-settings.const'; import { DyNTS_SingletonServiceBase } from '../base/singleton.service-base'; import { DyNTS_GlobalService } from './global.service'; /** * FR-193 (2026-06-15) — bedrock OOM korai-figyelmeztető heap-watchdog. * * MIÉRT: a Node heap-OOM (`--max-old-space-size` túllépés, pl. egy óriási JSON.parse) * a process-t AZONNAL megöli — `process.on('uncaughtException')` NEM kapja el, így a * crash pillanatában már nincs mód a hibát rögzíteni. Az EGYETLEN megbízható idő a * rögzítésre a crash ELŐTT van. E watchdog periodikusan figyeli a heap-kihasználtságot, * és amikor egy konfigurálható küszöböt átlép, TARTÓS rekordot hagy (DyFM_Log + * `DyNTS_GlobalService.globalErrorHandler` → az Errors-sink, amit minden szerver megkap) * — így egy OOM-hajlamos szerver NYOMOT hagy mielőtt elszáll, és a kuszob-atlepés * lathato a dashboard-on / Discord-on. * * MIT NEM csinál: NEM állítja le/újraindítja a process-t (az a konténer-szintű * `restart:` policy dolga), és NEM próbálja elkapni a fatal OOM-ot (nem lehet). Tisztán * MEGFIGYELÉS + RÖGZÍTÉS + opcionális `onCritical` hook (amivel a fogyasztó terhet dobhat). * * Hiszterézis: a küszöböket CSAK állapot-VÁLTÁSkor jelzi (normal→warning→critical), és * a `recoveryMargin`-nyit a warning ALÁ esve jelez 'recovered'-et — így nem spamel a * küszöb körül oszcilláló heap. * * Használat (opt-in, mint a `DyNTS_Logs_Service`): * DyNTS_MemoryGuard.getInstance().install(); // default settings * DyNTS_MemoryGuard.getInstance().install({ onCritical }); // custom hook * VAGY automatikusan: a base `App` startup feltelepíti, ha * `DyNTS_global_settings.memoryGuard.enabled === true` (alapértelmezés szerint igen). */ /** Egy MemoryGuard esemény — küszöb-átlépés vagy helyreállás. */ export interface DyNTS_MemoryGuard_Event { /** `warning` / `critical` küszöb-átlépés, vagy `recovered` (vissza normal-ba). */ level: 'warning' | 'critical' | 'recovered'; /** Heap használat MB-ban (`process.memoryUsage().heapUsed`). */ heapUsedMb: number; /** Heap plafon MB-ban (`v8.getHeapStatistics().heap_size_limit` — a `--max-old-space-size`). */ heapLimitMb: number; /** heapUsed / heapLimit * 100 (egész %-ra kerekítve). */ heapPct: number; /** Resident Set Size MB-ban (teljes process-memória, heap + non-heap). */ rssMb: number; /** External (C++ / Buffer) memória MB-ban. */ externalMb: number; /** A közelmúlt GC-hányada (GC-ben töltött idő / poll-ablak; 0..1) — a mark-compact-thrash / OOM-precursor jelzője. */ gcFraction: number; /** Mi váltotta ki az esemény-szintet: a heap-% küszöb, a GC-thrash, vagy mindkettő. */ trigger: 'heap' | 'gc' | 'heap+gc' | 'recovery'; /** Esemény időbélyege (ISO). */ at: string; } /** `install()` opcionális override-jai (a `DyNTS_global_settings.memoryGuard` FÖLÉ). */ export interface DyNTS_MemoryGuard_Config { /** Poll-intervallum ms-ben. Default: 10000. */ pollIntervalMs?: number; /** Warning küszöb a heap-plafon %-ában. Default: 85. */ heapWarningThreshold?: number; /** Critical küszöb a heap-plafon %-ában. Default: 95. */ heapCriticalThreshold?: number; /** A warning küszöb ALATTI margó %-ban a 'recovered'-hez (hiszterézis). Default: 10. */ recoveryMargin?: number; /** Megőrzött események max száma (ring-buffer). Default: 100. */ maxHistoryCount?: number; /** GC-hányad-küszöb a `warning`-hoz (0..1). Default: 0.40. */ gcWarningFraction?: number; /** GC-hányad-küszöb a `critical`-hoz (0..1). Default: 0.60. */ gcCriticalFraction?: number; /** `uncaughtException` crash-handler telepítése (teljes-részletű error-entry + tiszta exit). Default: true. */ installCrashHandlers?: boolean; /** Tartós critical → tiszta exit (a wrapper újraindít). Default: false. */ exitOnSustainedCritical?: boolean; /** Hány egymást követő critical poll után lép ki. Default: 3. */ sustainedCriticalPolls?: number; /** A graceful-exit kódja. Default: 137. */ exitCode?: number; /** Boot-grace ms az `exitOnSustainedCritical`-hez (a guard-install utáni ennyi ms-ben NEM lép ki). Default: 60000. */ bootGraceMs?: number; /** Hook, amit a `critical` küszöb átlépésekor hívunk (pl. terhelés-dobás). */ onCritical?: (event: DyNTS_MemoryGuard_Event) => void; } type DyNTS_MemoryGuard_State = 'normal' | 'warning' | 'critical'; const BYTES_PER_MB: number = 1024 * 1024; export class DyNTS_MemoryGuard extends DyNTS_SingletonServiceBase { static getInstance(): DyNTS_MemoryGuard { return DyNTS_MemoryGuard.getSingletonInstance() as DyNTS_MemoryGuard; } private timer: ReturnType | null = null; private installed: boolean = false; private state: DyNTS_MemoryGuard_State = 'normal'; // Feloldott (effektív) konfiguráció — install()-kor töltjük. private pollIntervalMs: number = 10000; private warnPct: number = 85; private critPct: number = 95; private recoveryMargin: number = 10; private maxHistoryCount: number = 100; private gcWarnFraction: number = 0.40; private gcCritFraction: number = 0.60; private exitOnSustainedCritical: boolean = false; private sustainedCriticalPolls: number = 3; private exitCode: number = 137; private bootGraceMs: number = 60000; /** A guard-install időbélyege (a boot-grace számításához; 0 = még nem telepített). */ private installedAt: number = 0; private onCritical?: (event: DyNTS_MemoryGuard_Event) => void; private readonly history: DyNTS_MemoryGuard_Event[] = []; private peakHeapPct: number = 0; private peakRssMb: number = 0; // GC-thrash követés (perf_hooks 'gc') — a GC-ben töltött ms gördülő ablaka + a critical-streak számláló. private gcObserver: PerformanceObserver | null = null; private gcEvents: { at: number; ms: number }[] = []; private criticalStreak: number = 0; // A telepített crash-handler ref-ek (a teardown eltávolításához). private crashHandler: ((error: Error) => void) | null = null; protected constructor() { super(); } /** * Elindítja a heap-watchdogot. Idempotens (többszöri hívás no-op). A `config` * felülírja a `DyNTS_global_settings.memoryGuard` értékeit, ami a beépített * defaultokat írja felül. SOHA nem dob — egy figyelő-réteg nem dönthet be egy * szervert (minden hiba try/catch-elt + descriptive-en logolt). */ install(config?: DyNTS_MemoryGuard_Config): void { if (this.installed) { return; } try { const g: NonNullable = DyNTS_global_settings.memoryGuard ?? { enabled: true }; this.pollIntervalMs = config?.pollIntervalMs ?? g.pollIntervalMs ?? 10000; this.warnPct = config?.heapWarningThreshold ?? g.heapWarningThreshold ?? 85; this.critPct = config?.heapCriticalThreshold ?? g.heapCriticalThreshold ?? 95; this.recoveryMargin = config?.recoveryMargin ?? g.recoveryMargin ?? 10; this.maxHistoryCount = config?.maxHistoryCount ?? g.maxHistoryCount ?? 100; this.gcWarnFraction = config?.gcWarningFraction ?? g.gcWarningFraction ?? 0.40; this.gcCritFraction = config?.gcCriticalFraction ?? g.gcCriticalFraction ?? 0.60; this.exitOnSustainedCritical = config?.exitOnSustainedCritical ?? g.exitOnSustainedCritical ?? false; this.sustainedCriticalPolls = config?.sustainedCriticalPolls ?? g.sustainedCriticalPolls ?? 3; this.exitCode = config?.exitCode ?? g.exitCode ?? 137; this.bootGraceMs = config?.bootGraceMs ?? g.bootGraceMs ?? 60000; this.installedAt = Date.now(); this.onCritical = config?.onCritical; this.installed = true; this.installGcObserver(); // Crash-handler (uncaughtException) — teljes-részletű error-entry a crash ELŐTT, majd tiszta exit. if (config?.installCrashHandlers ?? g.installCrashHandlers ?? true) { this.installCrashHandlers(); } this.timer = setInterval((): void => { this.poll(); }, this.pollIntervalMs); // unref(): a watchdog-timer NE tartsa életben a process-t önmagában (graceful // exit-kor a Node ki tud lépni a függő interval ellenére is). if (typeof this.timer.unref === 'function') { this.timer.unref(); } DyFM_Log.info( `[DyNTS_MemoryGuard] installed — poll ${this.pollIntervalMs}ms, ` + `warn ${this.warnPct}%/${Math.round(this.gcWarnFraction * 100)}%GC, ` + `crit ${this.critPct}%/${Math.round(this.gcCritFraction * 100)}%GC (heap limit ` + `${Math.round(this.getHeapLimitBytes() / BYTES_PER_MB)}MB)` + (this.exitOnSustainedCritical ? `, graceful-exit@${this.sustainedCriticalPolls} critical polls (boot-grace ${this.bootGraceMs}ms)` : ''), ); } catch (err: unknown) { // Telepítési hiba SEM lehet fatal — a guard hiánya nem ér annyit, hogy a // szerver-indulást megakassza. DyFM_Log.warn('[DyNTS_MemoryGuard] install failed (non-fatal):', err); } } /** A heap-plafon byte-ban (`--max-old-space-size`). Külön metódus a spec-mockoláshoz. */ private getHeapLimitBytes(): number { return v8.getHeapStatistics().heap_size_limit; } /** * Egy poll-ciklus: kiolvassa a heap-kihasználtságot, frissíti a csúcsokat, és a * hiszterézis-állapotgép szerint esemény-átlépéskor rögzít + logol. SOHA nem dob. */ private poll(): void { try { const limitBytes: number = this.getHeapLimitBytes(); if (!limitBytes || limitBytes <= 0) { return; } const mu: NodeJS.MemoryUsage = process.memoryUsage(); const pct: number = (mu.heapUsed / limitBytes) * 100; const gcFraction: number = this.recentGcFraction(); const rssMb: number = mu.rss / BYTES_PER_MB; if (pct > this.peakHeapPct) { this.peakHeapPct = pct; } if (rssMb > this.peakRssMb) { this.peakRssMb = rssMb; } // A szintet a heap-% ÉS a GC-hányad közül a ROSSZABB dönti el (a GC-thrash a %-küszöb ALATT is OOM-ot jelez). const heapCrit: boolean = pct >= this.critPct; const gcCrit: boolean = gcFraction >= this.gcCritFraction; const heapWarn: boolean = pct >= this.warnPct; const gcWarn: boolean = gcFraction >= this.gcWarnFraction; // Hiszterézis-állapotgép — esemény CSAK állapot-váltáskor. const prev: DyNTS_MemoryGuard_State = this.state; let next: DyNTS_MemoryGuard_State = prev; if (heapCrit || gcCrit) { next = 'critical'; } else if ((heapWarn || gcWarn) && prev === 'normal') { next = 'warning'; } else if (pct <= this.warnPct - this.recoveryMargin && gcFraction < this.gcWarnFraction) { next = 'normal'; } // Egyébként marad a jelenlegi állapot (hiszterézis-sáv) — nincs re-emit. // Sustained-critical streak + opcionális graceful-exit (állapot-váltástól FÜGGETLENÜL számolva). A boot-grace // alatt (install + bootGraceMs) NEM lép ki — a nehéz-boot (pool-hidratáló) appok eleve GC-thrash-elnek a boot // alatt, így a sustained-exit boot-loop-ot okozna; a grace UTÁN a tartós critical a VALÓDI runtime-OOM-ot kezeli. if (next === 'critical') { this.criticalStreak += 1; const pastBootGrace: boolean = (Date.now() - this.installedAt) >= this.bootGraceMs; if (this.exitOnSustainedCritical && pastBootGrace && this.criticalStreak >= this.sustainedCriticalPolls) { this.gracefulExit({ pct: pct, gcFraction: gcFraction, limitBytes: limitBytes, mu: mu }); return; } } else { this.criticalStreak = 0; } if (next === prev) { return; } this.state = next; const trigger: DyNTS_MemoryGuard_Event['trigger'] = next === 'normal' ? 'recovery' : (heapCrit || heapWarn) && (gcCrit || gcWarn) ? 'heap+gc' : (gcCrit || gcWarn) ? 'gc' : 'heap'; const event: DyNTS_MemoryGuard_Event = { level: next === 'normal' ? 'recovered' : next, heapUsedMb: Math.round(mu.heapUsed / BYTES_PER_MB), heapLimitMb: Math.round(limitBytes / BYTES_PER_MB), heapPct: Math.round(pct), rssMb: Math.round(rssMb), externalMb: Math.round((mu.external ?? 0) / BYTES_PER_MB), gcFraction: Math.round(gcFraction * 100) / 100, trigger: trigger, at: new Date().toISOString(), }; this.recordEvent(event); } catch (err: unknown) { // Poll-hiba sosem fatal — a következő tick újrapróbálja. DyFM_Log.warn('[DyNTS_MemoryGuard] poll error (non-fatal):', err); } } /** Esemény ring-bufferbe + log + (warning/critical esetén) tartós error-sink. */ private recordEvent(event: DyNTS_MemoryGuard_Event): void { this.history.push(event); while (this.history.length > this.maxHistoryCount) { this.history.shift(); } const summary: string = `heap ${event.heapPct}% (${event.heapUsedMb}/${event.heapLimitMb}MB), rss ${event.rssMb}MB, ` + `GC ${Math.round(event.gcFraction * 100)}% [${event.trigger}]`; if (event.level === 'recovered') { DyFM_Log.success(`[DyNTS_MemoryGuard] RECOVERED — ${summary}`); return; } if (event.level === 'critical') { DyFM_Log.H_error(`[DyNTS_MemoryGuard] CRITICAL heap pressure — ${summary} — OOM imminent`); } else { DyFM_Log.warn(`[DyNTS_MemoryGuard] WARNING heap pressure — ${summary}`); } // Tartós rögzítés a központi error-sinkbe (Errors-rendszer + Discord, ha bekötve). try { DyNTS_GlobalService.globalErrorHandler?.( new DyFM_Error({ errorCode: `${DyNTS_global_settings.systemShortCodeName ?? 'DyNTS'}|DyNTS-MG0-HEAP-${event.level.toUpperCase()}`, message: `Heap pressure ${event.level.toUpperCase()}: ${summary}. ` + `Process approaching the --max-old-space-size ceiling — OOM crash risk.`, additionalContent: { memoryGuardEvent: event, peakHeapPct: Math.round(this.peakHeapPct) }, systemVersion: DyNTS_global_settings.systemVersion, level: event.level === 'critical' ? DyFM_ErrorLevel.critical : DyFM_ErrorLevel.warning, }), ); } catch (sinkErr: unknown) { DyFM_Log.warn('[DyNTS_MemoryGuard] error-sink record failed (non-fatal):', sinkErr); } if (event.level === 'critical' && this.onCritical) { try { this.onCritical(event); } catch (hookErr: unknown) { DyFM_Log.warn('[DyNTS_MemoryGuard] onCritical hook threw (non-fatal):', hookErr); } } } /** * A pillanatnyi heap-nyomás (heap-% + GC-hányad → szint) — a FOGYASZTÓK (pl. egy API-controller) ezzel * gate-elhetik a nehéz műveleteket (load-shed). Olvasás-only, mellékhatás nélkül; nem igényli az install-t. */ pressure(): { level: 'ok' | 'warning' | 'critical'; heapUsedMb: number; heapLimitMb: number; heapPct: number; rssMb: number; gcFraction: number; } { const limitBytes: number = this.getHeapLimitBytes(); const mu: NodeJS.MemoryUsage = process.memoryUsage(); const pct: number = limitBytes > 0 ? (mu.heapUsed / limitBytes) * 100 : 0; const gcFraction: number = this.recentGcFraction(); const level: 'ok' | 'warning' | 'critical' = (pct >= this.critPct || gcFraction >= this.gcCritFraction) ? 'critical' : (pct >= this.warnPct || gcFraction >= this.gcWarnFraction) ? 'warning' : 'ok'; return { level: level, heapUsedMb: Math.round(mu.heapUsed / BYTES_PER_MB), heapLimitMb: Math.round(limitBytes / BYTES_PER_MB), heapPct: Math.round(pct), rssMb: Math.round(mu.rss / BYTES_PER_MB), gcFraction: Math.round(gcFraction * 100) / 100, }; } /** `true`, ha a NEHÉZ műveleteket le kell shed-elni (critical nyomás) — a fogyasztó load-shed gate-jének. */ shouldShedLoad(): boolean { return this.installed && this.pressure().level === 'critical'; } /** A GC-hányad az utolsó `pollIntervalMs` ablakban (GC-ms / ablak-ms; clamp [0..1]). Nincs GC-observer → 0. */ private recentGcFraction(): number { const windowMs: number = this.pollIntervalMs > 0 ? this.pollIntervalMs : 10000; const cutoff: number = Date.now() - windowMs; this.gcEvents = this.gcEvents.filter((event: { at: number; ms: number }): boolean => event.at >= cutoff); const gcMs: number = this.gcEvents.reduce((sum: number, event: { at: number; ms: number }): number => sum + event.ms, 0); return Math.min(gcMs / windowMs, 1); } /** A 'gc' perf-entry-k figyelője — a GC-ben töltött időt gyűjti a gördülő ablakhoz. Best-effort (nem kritikus). */ private installGcObserver(): void { try { this.gcObserver = new PerformanceObserver((list): void => { const now: number = Date.now(); for (const entry of list.getEntries()) { this.gcEvents.push({ at: now, ms: entry.duration }); } }); this.gcObserver.observe({ entryTypes: ['gc'] }); } catch (err: unknown) { // A GC-observer hiánya nem kritikus — a heap-%-jel marad. Best-effort. DyFM_Log.warn('[DyNTS_MemoryGuard] gc-observer install failed (non-fatal):', err); } } /** * Process-szintű `uncaughtException` crash-handler: a (különben néma) crash ELŐTT TELJES-RÉSZLETŰ error-entry * (message + stack + memória-snapshot) az error-sinkbe, majd tiszta exit (a wrapper/restart-policy újraindít). * Az `unhandledRejection`-t a base App már rögzíti; ez az eddig-fedetlen `uncaughtException`-t fedi le. */ private installCrashHandlers(): void { this.crashHandler = (error: Error): void => { try { const mu: NodeJS.MemoryUsage = process.memoryUsage(); DyFM_Log.H_error('[DyNTS_MemoryGuard] FATAL uncaughtException:', error?.stack ?? error); DyNTS_GlobalService.globalErrorHandler?.( new DyFM_Error({ errorCode: `${DyNTS_global_settings.systemShortCodeName ?? 'DyNTS'}|DyNTS-MG0-UNCAUGHT`, message: `Uncaught Exception (process-fatal): ${error?.message ?? String(error)}`, error: error, additionalContent: { stack: error?.stack, heapUsedMb: Math.round(mu.heapUsed / BYTES_PER_MB), rssMb: Math.round(mu.rss / BYTES_PER_MB), peakHeapPct: Math.round(this.peakHeapPct), recentMemoryGuardEvents: this.history.slice(-5), }, systemVersion: DyNTS_global_settings.systemVersion, level: DyFM_ErrorLevel.critical, }), ); } catch { // a rögzítés se buktasson — best-effort. } // uncaughtException → a process undefined állapotban van: rögzítés UTÁN tiszta exit (a log/sink flush-re haladék). setTimeout((): void => { process.exit(1); }, 250).unref?.(); }; process.on('uncaughtException', this.crashHandler); } /** Tartós-critical graceful-exit: végső error-entry + tiszta exit (a wrapper újraindít a kriptikus 134 helyett). */ private gracefulExit(set: { pct: number; gcFraction: number; limitBytes: number; mu: NodeJS.MemoryUsage }): void { const summary: string = `heap ${Math.round(set.pct)}% (${Math.round(set.mu.heapUsed / BYTES_PER_MB)}/` + `${Math.round(set.limitBytes / BYTES_PER_MB)}MB), GC ${Math.round(set.gcFraction * 100)}%`; DyFM_Log.H_error(`[DyNTS_MemoryGuard] SUSTAINED CRITICAL — ${summary} — graceful exit(${this.exitCode}) a kriptikus OOM (exit 134) helyett.`); try { DyNTS_GlobalService.globalErrorHandler?.( new DyFM_Error({ errorCode: `${DyNTS_global_settings.systemShortCodeName ?? 'DyNTS'}|DyNTS-MG0-GRACEFUL-EXIT`, message: `Sustained critical heap pressure (${this.criticalStreak} polls) — graceful exit to let the wrapper/restart-policy recover. ${summary}.`, additionalContent: { peakHeapPct: Math.round(this.peakHeapPct), peakRssMb: Math.round(this.peakRssMb), recentEvents: this.history.slice(-5) }, systemVersion: DyNTS_global_settings.systemVersion, level: DyFM_ErrorLevel.critical, }), ); } catch { // best-effort } setTimeout((): void => { process.exit(this.exitCode); }, 250).unref?.(); } /** A megőrzött események (legrégebbi → legújabb). Csak olvasásra. */ getHistory(): DyNTS_MemoryGuard_Event[] { return this.history.slice(); } /** Pillanatnyi állapot + csúcsok diagnosztikához (pl. egy status-endpoint). */ getStatus(): { installed: boolean; state: DyNTS_MemoryGuard_State; peakHeapPct: number; peakRssMb: number; eventCount: number; } { return { installed: this.installed, state: this.state, peakHeapPct: Math.round(this.peakHeapPct), peakRssMb: Math.round(this.peakRssMb), eventCount: this.history.length, }; } /** Telepítve van-e a watchdog. */ isInstalled(): boolean { return this.installed; } /** * Leállítja a watchdogot + visszaállítja az állapotot. Graceful shutdown-hoz ÉS a * spec-ek `afterEach`-éhez (a singleton-state ne szivárogjon a tesztek közt). */ _teardownForTesting(): void { if (this.timer) { clearInterval(this.timer); this.timer = null; } if (this.gcObserver) { try { this.gcObserver.disconnect(); } catch { /* best-effort */ } this.gcObserver = null; } if (this.crashHandler) { try { process.removeListener('uncaughtException', this.crashHandler); } catch { /* best-effort */ } this.crashHandler = null; } this.installed = false; this.state = 'normal'; this.history.length = 0; this.peakHeapPct = 0; this.peakRssMb = 0; this.gcEvents = []; this.criticalStreak = 0; this.installedAt = 0; this.onCritical = undefined; } }