import { DyFM_Metadata, DyFM_RelativeDate, DyFM_Time, DyFM_ServerStatus, DyFM_ErrorLevel, DyFM_Log, DyFM_Error, DyFM_Error_Statistics, DyFM_Errors, DyFM_Paged, hour, minute, DyFM_ServerConnectionCheckResult, DyFM_ServerConnection_Status, DyFM_Array } from '@futdevpro/fsm-dynamo'; import { Subscription, timer } from 'rxjs'; import * as mongoose from 'mongoose'; import { version as _serverVersion } from '../../../../package.json'; import { DyNTS_SingletonService } from '../../../_services/base/singleton.service'; import { DyNTS_global_settings } from '../../../_collections/global-settings.const'; import { DyNTS_Errors_ControlService } from '../errors/errors.control-service'; import { DyNTS_ServerStatusSnapshot_ControlService } from './server-status-snapshot.control-service'; import { DyNTS_GlobalService } from '../../../_services/core/global.service'; import { DyNTS_ApiService_Base } from '../../../_services/base/api.service-base'; export interface DyNTS_ServerConnection { name: string, apiService: DyNTS_ApiService_Base, accessible?: boolean, lastCheck?: Date, } /** * A readiness-probe ({@link DyNTS_ServerStatus_ControlService.checkDbReadiness}) eredménye. * `ready=true` CSAK ha a DB-connection él (readyState=1) ÉS a valós ping round-trip sikerült. */ export interface DyNTS_DbReadiness { /** ready: a DB elérhető (readyState=1 + sikeres ping). Erre adjon az endpoint 200-at, különben 503-at. */ ready: boolean; /** mongoose connection.readyState: 0=disconnected, 1=connected, 2=connecting, 3=disconnecting. */ dbReadyState: number; /** Sikerült-e a valós DB ping round-trip (csak readyState=1-nél próbáljuk). */ dbPingOk: boolean; /** A szerver uptime ms-ban. */ uptime: number; /** A BEDROCK (nts-dynamo) verziója — NEM az app sajátja. */ serverVersion: string; /** AZ APP saját verziója (`DyNTS_global_settings.systemVersion`) — deploy-verifikációhoz. */ appVersion?: string; // --- DIAGNOSZTIKA (2026-07-22) — a flotta-szintű "readiness 503, de az app kiszolgál" lelet miatt --- // Élesben mérve: az adventor / token-service / master-prompter MIND `dbReadyState:0`-t adott, miközben // kiszolgáltak, a Mongo bizonyítottan élt, és a reconnect-guard (amely UGYANEZT a readyState-et olvassa) // egyetlen warningot sem logolt. Öt hipotézist zártunk ki méréssel (több replika · két mongoose-példány · // esModule default-export · leszármazott-override · halott DB) — a gyökér-ok NEM dőlt el. Ezek a mezők // teszik a KÖVETKEZŐ mérést döntővé, találgatás nélkül. Mind ADDITÍV + opcionális → nem törnek meg semmit. /** Hány mongoose-connection létezik ebben a process-ben (`mongoose.connections.length`). >1 → nem a default-on megy a data-layer. */ dbConnectionCount?: number; /** A default connection cél-hosztja (`connection.host`) — üres/undefined, ha a connect sosem futott le rajta. */ dbHost?: string; /** A default connection adatbázis-neve (`connection.name`). */ dbName?: string; /** Hány model van a default connectionre regisztrálva. 0 → a data-layer NEM ezt a connectiont használja. */ dbModelCount?: number; /** A NEM-default connectionök readyState-jei — ha itt van 1-es, a probe rossz handle-t mér. */ dbOtherReadyStates?: number[]; } export abstract class DyNTS_ServerStatus_ControlService< T_ServerStatus extends DyFM_ServerStatus, T_Error extends DyFM_Error, T_Errors extends DyFM_Errors, T_Errors_ControlService extends DyNTS_Errors_ControlService, T_ServerStatusSnapshot extends DyFM_ServerStatus, T_ServerStatusSnapshot_ControlService extends DyNTS_ServerStatusSnapshot_ControlService > extends DyNTS_SingletonService { protected abstract getErrorControlService?( set?: { data?: T_Errors, issuer?: string } ): T_Errors_ControlService; protected abstract getServerStatusSnapshotControlService?( set?: { data?: T_ServerStatus, issuer?: string } ): T_ServerStatusSnapshot_ControlService; protected readonly up: Date = new Date(); protected readonly _serverVersionNumeric: number = this.getVersionNumeric(_serverVersion); /** * A BEDROCK (nts-dynamo) verzioja — a package.json-bol importalva. * FIGYELEM: ez NEM az app sajat verzioja! (Lasd `appVersion`.) */ get serverVersion(): string { return _serverVersion; } get serverVersionNumeric(): number { return this._serverVersionNumeric; } /** * AZ APP sajat verzioja (2026-07-20) — a `DyNTS_App_Params.version`-bol, amit az * app-params a `DyNTS_global_settings.systemVersion`-be tolt (app-params.control-model). * * MIERT KELLETT: a `serverVersion` az nts-dynamo package.json-jabol jon, ezert a TELJES * flottan UGYANAZT az erteket adja (pl. 01.15.104), fuggetlenul attol, hogy az app melyik * verzioja fut. Emiatt minden verzio-alapu deploy-ellenorzes vak volt: * - a CI/CD `deploy-verify` polling SOHA nem egyezne (app 01.15.142 vs serverVersion 01.15.104), * - az Overseer verzio-drift monitorozasa nem latja az ELAVULT app-deployt, * - a `fdp deploy-service --skip-if-version` gate mindig eltérést lát → sosem skippel. * * ADDITIV mezo: a `serverVersion` szemantikaja VALTOZATLAN marad (nulla regresszio a * meglevo fogyasztoknal / e2e-knel); az uj fogyasztok az `appVersion`-t olvassak. */ get appVersion(): string { return DyNTS_global_settings.systemVersion; } protected _latestClientVersion: string; protected _latestClientVersionNumeric: number; get latestClientVersion(): string { return this._latestClientVersion; } get latestClientVersionNumeric(): number { return this._latestClientVersionNumeric; } protected abstract requiredServerConnections: DyNTS_ServerConnection[]; protected connectionMaxCheckFrequency = 15 * minute; override readonly defaultErrorUserMsg = 'Something went wrong'; readonly overseerTickTime = 6 * hour; overseer: Subscription; protected constructor() { super(); /* if (DyNTS_global_settings.log_settings.setup) { DyFM_Log.log( 'Required server connections:', JSON.stringify(this.requiredServerConnections.map( (connection: DyNTS_ServerConnection): { name: string, baseUrl: string, testEndpoint: string } => ({ name: connection.name, baseUrl: connection.apiService.baseUrl, testEndpoint: connection.apiService.testEndpoint ?? '', }) ), null, 2) ); } */ this.overseer = timer(this.overseerTickTime, this.overseerTickTime).subscribe(async () => { try { await this.triggerCreateSnapshot('overseer-system-tick'); } catch (error) { DyFM_Error.logSimple(' ! Error in overseer (server status):', error); DyNTS_GlobalService.globalErrorHandler( new DyFM_Error({ ...this.getDefaultErrorSettings('overseer tick', error, 'overseer-system-tick'), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-SSCS-OT0`, }) ).catch(); } }); } async getServerStatus(issuer: string, clientVersion?: string): Promise { try { return { status: 'ready', systemName: DyNTS_global_settings.systemName, systemShortCode: DyNTS_global_settings.systemShortCodeName, clientVersionOk: clientVersion ? this.clientVersionIsLatest(clientVersion) : false, latestClientVersion: this.latestClientVersion, latestClientVersionNumeric: this.latestClientVersionNumeric, serverVersion: this.serverVersion, serverNumericVersion: this.serverVersionNumeric, // Az APP sajat verzioja (a serverVersion a bedrock-e) — deploy-verifikaciohoz. appVersion: this.appVersion, uptime: +new Date() - +this.up, memoryUsage: process.memoryUsage(), cpuUsage: process.cpuUsage(), serverConnections: await this.checkServerConnections(issuer), } } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('getServerStatus', error, issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-SSCS-GSS0`, }); } } async checkServerConnections(issuer: string): Promise { try { const connections: DyFM_ServerConnectionCheckResult = {}; await DyFM_Array.asyncForEachAllAtOnce( this.requiredServerConnections, async (connection: DyNTS_ServerConnection) => { if (!connection.accessible || Date.now() - connection.lastCheck.getTime() > this.connectionMaxCheckFrequency) { await connection.apiService.checkServerAvailability(issuer).catch((error) => { DyFM_Error.logSimple( `(checkServerConnections) "${connection.name}" server is not available` + `\n baseUrl: ${connection.apiService.baseUrl}` + `\n testEndpoint: ${connection.apiService.testEndpoint ?? ''}`, error ); connection.accessible = false; }).then(() => { connection.accessible = true; }); connection.accessible = true; connection.lastCheck = new Date(); } connections[connection.name] = connection.accessible ? DyFM_ServerConnection_Status.accessible : DyFM_ServerConnection_Status.unaccessible; } ) return connections; } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('checkServerConnections', error, issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-SSCS-CC0`, }); } } /** * A mongoose default-connection. Külön metódus = teszt-seam (spy-jal felülírható, hogy a * {@link checkDbReadiness} DB nélkül is tesztelhető legyen). */ protected getMongooseConnection(): mongoose.Connection { // FIX (2026-07-22) — a `mongoose.connection` NEM feltetlenul az a handle, amit a data-layer hasznal. // // ELESBEN MERVE (adventor, a diagnosztikai mezokkel): `dbConnectionCount: 1`, `dbModelCount: 0`, // `dbOtherReadyStates: [1]` — azaz letezik EGY KAPCSOLODOTT (readyState=1) connection, de a // `mongoose.connection` **nem az**: az disconnected (0), NULLA modellel, `host`/`name` nelkul // (sosem csatlakozott). Emiatt a readiness flotta-szinten **hamis 503**-at adott (adventor, // token-service, master-prompter), mikozben a DB vegig egeszseges volt es az appok kiszolgaltak. // // Ezert a valos, KAPCSOLODOTT connectiont keressuk meg a `mongoose.connections`-bol, es csak // fallbackkent hasznaljuk a `mongoose.connection`-t. Ez minden esetben helyes: // - ha van elo connection -> azt merjuk (readyState=1 + valos ping) -> a readiness igazat mond, // - ha a DB tenyleg halott -> egyik sem 1 -> fallback -> tovabbra is helyesen NEM-ready. // Vagyis a valodi DB-kiesest NEM maszkolja el, csak a hamis-negativot szunteti meg. const all: mongoose.Connection[] = mongoose.connections ?? []; const live: mongoose.Connection | undefined = all.find((c: mongoose.Connection): boolean => c?.readyState === 1); return live ?? mongoose.connection; } /** * Readiness-probe: a `/health` (liveness — "a process fut + kiszolgál") MELLETT a DB-elérhetőséget * ellenőrzi. A readyState (mongoose driver-állapot) + egy valós `ping` round-trip → így a "connected * de a DB nem válaszol" eset is kibukik. **Sosem dob** (a probe-nak determinisztikusan kell válaszolnia); * hiba esetén `ready=false`. Az endpoint 503-at adjon, ha nem ready → az orchestrátor (k8s/docker/LB) * ne irányítson forgalmat a DB-vesztett instance-ra. */ async checkDbReadiness(): Promise { let dbReadyState: number = 0; let dbPingOk: boolean = false; try { const connection: mongoose.Connection = this.getMongooseConnection(); // readyState: 0=disconnected, 1=connected, 2=connecting, 3=disconnecting dbReadyState = connection?.readyState ?? 0; if (dbReadyState === 1) { await connection.db?.admin().ping(); dbPingOk = true; } } catch (error) { DyFM_Log.warn('(checkDbReadiness) DB readiness check failed', error); dbPingOk = false; } // DIAGNOSZTIKA (2026-07-22): sosem dobhat — a probe-nak determinisztikusan kell válaszolnia, // ezért a diag-gyűjtés SAJÁT try-ban van (ha bármelyik mező kiolvasása elszáll, a readiness // válasz attól még ép marad). let diagnostics: Partial = {}; try { const conn: mongoose.Connection = this.getMongooseConnection(); const all: mongoose.Connection[] = mongoose.connections ?? []; diagnostics = { dbConnectionCount: all.length, dbHost: conn?.host, dbName: conn?.name, dbModelCount: Object.keys(conn?.models ?? {}).length, // a default connectiont kihagyjuk — csak a "van-e MÁSIK, élő handle" kérdés érdekes dbOtherReadyStates: all.filter((c: mongoose.Connection): boolean => c !== conn) .map((c: mongoose.Connection): number => c?.readyState ?? -1), }; } catch (error) { DyFM_Log.warn('(checkDbReadiness) diagnostics collection failed (non-fatal)', error); } return { ready: dbReadyState === 1 && dbPingOk, dbReadyState: dbReadyState, dbPingOk: dbPingOk, uptime: +new Date() - +this.up, serverVersion: this.serverVersion, appVersion: this.appVersion, ...diagnostics, }; } async getErrorStatistics( range: DyFM_RelativeDate, issuer: string ): Promise { try { const error_DS: T_Errors_ControlService = this.getErrorControlService({ issuer: issuer }); /* new DyNTS_Error_DataService({ issuer: issuer }); */ const from: Date = DyFM_Time.getDateByRelativeDate(range); const errorsPaged: DyFM_Paged = await error_DS.getErrorsFromDate(from, issuer); const errors: T_Errors[] = errorsPaged.items; return { range: range, allErrors: errors.length, allAllErrors: errors.reduce( (sum: number, error: T_Errors): number => sum + error.count, 0 ), serverErrors: errors.filter( (error: T_Errors): boolean => error.source.includes('SERVER') ).length, clientErrors: errors.filter( (error: T_Errors): boolean => !error.source.includes('SERVER') ).length, allServerErrors: errors.filter( (error: T_Errors): boolean => error.source.includes('SERVER') ).reduce( (sum: number, error: T_Errors): number => sum + error.count, 0 ), allClientErrors: errors.filter( (error: T_Errors): boolean => !error.source.includes('SERVER') ).reduce( (sum: number, error: T_Errors): number => sum + error.count, 0 ), infoErrors: errors.filter( (error: T_Errors): boolean => error.level === DyFM_ErrorLevel.info ).length, warningErrors: errors.filter( (error: T_Errors): boolean => error.level === DyFM_ErrorLevel.warning ).length, errorErrors: errors.filter( (error: T_Errors): boolean => error.level === DyFM_ErrorLevel.error ).length, criticalErrors: errors.filter( (error: T_Errors): boolean => error.level === DyFM_ErrorLevel.critical ).length, allInfoErrors: errors.filter( (error: T_Errors): boolean => error.level === DyFM_ErrorLevel.info ).reduce( (sum: number, error: T_Errors): number => sum + error.count, 0 ), allWarningErrors: errors.filter( (error: T_Errors): boolean => error.level === DyFM_ErrorLevel.warning ).reduce( (sum: number, error: T_Errors): number => sum + error.count, 0 ), allErrorErrors: errors.filter( (error: T_Errors): boolean => error.level === DyFM_ErrorLevel.error ).reduce( (sum: number, error: T_Errors): number => sum + error.count, 0 ), allCriticalErrors: errors.filter( (error: T_Errors): boolean => error.level === DyFM_ErrorLevel.critical ).reduce( (sum: number, error: T_Errors): number => sum + error.count, 0 ), } as DyFM_Error_Statistics; } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings( 'getMatchStatistics', error?.response?.data ?? error, issuer ), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-SSS-GES0`, }); } } protected getVersionNumeric(version: string): number { return +version.replace('v', '') .replace('.', '') .replace('.', '') .replace('-alpha', '') .replace('-beta', '') .replace('-test', '') .replace('-dev', ''); } protected clientVersionIsLatest(clientVersion: string): boolean { const thisClientVersionNumeric: number = this.getVersionNumeric(clientVersion); if (!this._latestClientVersionNumeric) { this.setLatestClientVersion(clientVersion, thisClientVersionNumeric); return true; } else if (thisClientVersionNumeric === this._latestClientVersionNumeric) { return true; } else if (thisClientVersionNumeric < this._latestClientVersionNumeric) { return false; } else if (this._latestClientVersionNumeric < thisClientVersionNumeric) { this.setLatestClientVersion(clientVersion, thisClientVersionNumeric); return true; } DyFM_Log.error( 'Something went wrong, invalid version:', clientVersion, thisClientVersionNumeric, _serverVersion, this.serverVersionNumeric ); return false; } protected setLatestClientVersion(clientVersion: string, clientVersionNumeric: number): void { if (!clientVersion.includes('test') && !clientVersion.includes('dev')) { this._latestClientVersion = clientVersion; this._latestClientVersionNumeric = clientVersionNumeric; } } protected async triggerCreateSnapshot(issuer: string): Promise { try { const snapshot: T_ServerStatusSnapshot = await this.getServerStatusSnapshot(issuer); const serverStatusSnapshot_CS: T_ServerStatusSnapshot_ControlService = this.getServerStatusSnapshotControlService({ issuer: issuer }); await serverStatusSnapshot_CS.saveSnapshot(snapshot, issuer); DyFM_Log.info('ServerStatus snapshot created'); } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('triggerCreateSnapshot', error, 'system-tick'), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-SSCS-TCS0`, }); } } async getServerStatusSnapshot(issuer: string): Promise { try { return ({ ...(await this.getServerStatus(issuer)), }) as T_ServerStatusSnapshot; } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings( 'getServerStatusSnapshot', error?.response?.data ?? error, issuer ), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-SSCS-GSS0`, }); } } }