/** * ContainerLifecycleClassifier * * Pure classification helpers for Docker container lifecycle events. No I/O, * no side effects, no singletons. Consumed by DockerEventService. * * The classifier answers a single question: given a `die` event and the * container's recent lifecycle state, is this exit intentional, a clean exit, * an OOM kill, or a crash worth alerting on? */ export type Classification = 'intentional' | 'clean' | 'crash' | 'oom'; /** Window (ms) after a `kill` event within which a subsequent `die` is considered intentional. */ export const INTENTIONAL_KILL_WINDOW_MS = 60_000; /** * Maximum negative age (ms) tolerated when matching a kill to a later die. * Absorbs small out-of-order deliveries and minor clock skew, but rejects * wildly future-dated Docker timestamps that could otherwise flip a genuine * crash into an "intentional" classification. */ export const MAX_NEGATIVE_SKEW_MS = 10_000; export interface ContainerLifecycleState { /** Timestamp (ms) of the most recent `kill` event for this container, if any. */ lastKillAt?: number; /** True when an `oom` event has been observed and the matching `die` has not yet arrived. */ oomPending?: boolean; } export interface DieEventInput { /** Time the die event occurred (ms). Typically Date.now() when the event was parsed. */ at: number; /** Exit code reported by Docker. May be undefined for malformed events (treated as non-zero). */ exitCode: number | undefined; } /** * Classify a die event against the container's current lifecycle state. * * Priority order: * 1. OOM pending → 'oom' (OOM kills are meaningful even if exitCode looks clean) * 2. Recent kill within window → 'intentional' * 3. Exit code 0 → 'clean' * 4. Anything else → 'crash' */ export function classifyDie( input: DieEventInput, state: ContainerLifecycleState, ): Classification { if (state.oomPending) return 'oom'; if (typeof state.lastKillAt === 'number') { // Use signed age so a die with a wildly future-dated timestamp // (clock skew / bad container clock) cannot match a past kill. // Allow modest negative skew so out-of-order deliveries still // classify as intentional within DockerEventService's grace window. const age = input.at - state.lastKillAt; if (age >= -MAX_NEGATIVE_SKEW_MS && age <= INTENTIONAL_KILL_WINDOW_MS) { return 'intentional'; } } if (input.exitCode === 0) return 'clean'; return 'crash'; } /** * Classify a gap exit discovered during reconciliation (no die event observed * because the stream was disconnected). Uses the container inspect result * rather than event state. */ export function classifyGapExit(inspect: { State?: { OOMKilled?: boolean; ExitCode?: number }; }): Classification { const oom = inspect.State?.OOMKilled === true; if (oom) return 'oom'; const code = inspect.State?.ExitCode; if (code === 0) return 'clean'; return 'crash'; }