Relax freshness limits and tolerate transient RCON failures

This commit is contained in:
2026-10-05 16:36:23 -04:00
parent d8b972cfc3
commit b1b3e8b958
28 changed files with 221 additions and 58 deletions
+13 -4
View File
@@ -14,6 +14,8 @@ export interface GameObservation {
last_gametype: string | null;
last_seen_at: string | null;
last_error: string | null;
/** Consecutive failed monitoring checks; absent on older snapshots. */
rcon_failures?: number;
roster: StatusPlayer[] | null;
}
export interface ServerObservation extends GameObservation {
@@ -32,6 +34,12 @@ export interface ServerObservation extends GameObservation {
game_ended_at?: string | null;
}
/** Keep a first transient monitoring failure in diagnostics without demoting health. */
export function rconHealthError(status: Pick<GameObservation, 'last_error' | 'rcon_failures'>): string | null {
const reason = status.last_error?.split(':')[0];
return status.rcon_failures === 1 && (reason === 'timeout' || reason === 'send-error') ? null : status.last_error;
}
export function freshTelemetry(report: GameTelemetry | undefined, now: number, after?: string | null): report is GameTelemetry {
if (!report || !freshStatusTimestamp(report.receivedAt, now)) return false;
if (after && (!Number.isFinite(Date.parse(after)) || Date.parse(report.receivedAt) <= Date.parse(after))) return false;
@@ -42,7 +50,7 @@ export function freshTelemetry(report: GameTelemetry | undefined, now: number, a
export function selectServerObservation<T extends ServerObservation>(server: T, report: GameTelemetry | undefined, now: number): T {
if (server.telemetry && (!report || Date.parse(server.telemetry.receivedAt) > Date.parse(report.receivedAt))) report = server.telemetry;
const raw = server.rcon_status ?? server;
const result: T = { ...server, ...raw, telemetry: report, status_source: raw.last_seen_at ? 'rcon' : null };
const result: T = { ...server, ...raw, last_error: rconHealthError(raw), telemetry: report, status_source: raw.last_seen_at ? 'rcon' : null };
const containerUnknown = server.container_state === 'unknown';
if (['provisioning','starting'].includes(server.health) || (server.container_state && server.container_state !== 'running' && !containerUnknown) || (!server.container_state && server.health === 'offline')) {
return { ...result, health: server.health, last_error: null, last_players: null, roster: null, status_source: null };
@@ -69,11 +77,12 @@ export function selectServerObservation<T extends ServerObservation>(server: T,
game_ended_at: players === 0 ? null : timing.gameEndedAt };
}
const after = !server.telemetry_after || Date.parse(raw.last_seen_at ?? '') > Date.parse(server.telemetry_after);
const fresh = after && freshStatusTimestamp(raw.last_seen_at, now) && !raw.last_error;
const fresh = after && freshStatusTimestamp(raw.last_seen_at, now) && !result.last_error;
// A current authenticated RCON reply can establish game liveness when
// inspection is unavailable, without claiming the container was inspected.
const containerExpired = !containerUnknown && !!server.container_checked_at && !containerFresh;
const age = now - Date.parse(raw.last_seen_at ?? '');
return { ...result, health: fresh && !containerExpired ? 'online'
: after && age >= 0 && age <= 120_000 ? 'stale' : 'offline' };
const health = fresh && !containerExpired ? 'online'
: after && age >= 0 && age <= 120_000 ? 'stale' : 'offline';
return { ...result, health, last_error: health === 'online' ? result.last_error : raw.last_error };
}