// Package runtime defines the runtime-record domain model, status machine, // and sentinel errors owned by Runtime Manager. // // The package mirrors the durable shape of the `runtime_records` // PostgreSQL table (see // `galaxy/rtmanager/internal/adapters/postgres/migrations/00001_init.sql`). // Every status / transition / required-field rule already documented in // `galaxy/rtmanager/README.md` lives here as code so adapter and service // layers do not re-derive it. package runtime import ( "fmt" "strings" "time" ) // Status identifies one runtime-record lifecycle state. type Status string const ( // StatusRunning reports that an engine container is live and bound to // the record. The associated container id and image ref are non-empty // and StartedAt is set. StatusRunning Status = "running" // StatusStopped reports that the engine container has exited (graceful // stop, observed Docker exit, or reconciled exit). The container is // still present in Docker until the cleanup worker removes it. StatusStopped Status = "stopped" // StatusRemoved reports that the container has been removed from // Docker (admin cleanup or reconcile_dispose). The record stays in // PostgreSQL for audit; there is no transition out of this state. StatusRemoved Status = "removed" ) // IsKnown reports whether status belongs to the frozen runtime status // vocabulary. func (status Status) IsKnown() bool { switch status { case StatusRunning, StatusStopped, StatusRemoved: return true default: return false } } // IsTerminal reports whether status can no longer accept lifecycle // transitions. func (status Status) IsTerminal() bool { return status == StatusRemoved } // AllStatuses returns the frozen list of every runtime status value. The // slice order is stable across calls and matches the README §Persistence // Layout listing. func AllStatuses() []Status { return []Status{ StatusRunning, StatusStopped, StatusRemoved, } } // RuntimeRecord stores one durable runtime record owned by Runtime // Manager. It mirrors one row of the `runtime_records` table. // // CurrentContainerID and CurrentImageRef are stored as plain strings; an // empty value represents SQL NULL and is bridged at the adapter layer. // StartedAt, StoppedAt, and RemovedAt are *time.Time so a missing value // is unambiguous and aligns with the jet-generated model. type RuntimeRecord struct { // GameID identifies the platform game owning this runtime record. GameID string // Status stores the current lifecycle state. Status Status // CurrentContainerID identifies the bound Docker container. Empty // when status is removed and after a reconciler observes // disappearance. CurrentContainerID string // CurrentImageRef stores the Docker reference of the currently-bound // engine image. Non-empty when status is running or stopped. CurrentImageRef string // EngineEndpoint stores the stable URL Game Master uses to reach the // engine container, in `http://galaxy-game-{game_id}:8080` form. EngineEndpoint string // StatePath stores the absolute host path of the bind-mounted engine // state directory. StatePath string // DockerNetwork stores the Docker network the container was attached // to at create time. DockerNetwork string // StartedAt stores the wall-clock at which the container became // running. Non-nil when status is running or stopped. StartedAt *time.Time // StoppedAt stores the wall-clock at which the container exited. // Non-nil when status is stopped or removed (when the record passed // through stopped before removal). StoppedAt *time.Time // RemovedAt stores the wall-clock at which the container was removed // from Docker. Non-nil when status is removed. RemovedAt *time.Time // LastOpAt stores the wall-clock of the most recent operation // affecting this record. Drives the cleanup TTL. LastOpAt time.Time // CreatedAt stores the wall-clock at which Runtime Manager first saw // this game. CreatedAt time.Time } // Validate reports whether record satisfies the runtime-record invariants // implied by README §Lifecycles and the SQL CHECK on `runtime_records`. func (record RuntimeRecord) Validate() error { if strings.TrimSpace(record.GameID) == "" { return fmt.Errorf("game id must not be empty") } if !record.Status.IsKnown() { return fmt.Errorf("status %q is unsupported", record.Status) } if strings.TrimSpace(record.EngineEndpoint) == "" { return fmt.Errorf("engine endpoint must not be empty") } if strings.TrimSpace(record.StatePath) == "" { return fmt.Errorf("state path must not be empty") } if strings.TrimSpace(record.DockerNetwork) == "" { return fmt.Errorf("docker network must not be empty") } if record.LastOpAt.IsZero() { return fmt.Errorf("last op at must not be zero") } if record.CreatedAt.IsZero() { return fmt.Errorf("created at must not be zero") } if record.LastOpAt.Before(record.CreatedAt) { return fmt.Errorf("last op at must not be before created at") } switch record.Status { case StatusRunning: if strings.TrimSpace(record.CurrentContainerID) == "" { return fmt.Errorf("current container id must not be empty for running records") } if strings.TrimSpace(record.CurrentImageRef) == "" { return fmt.Errorf("current image ref must not be empty for running records") } if record.StartedAt == nil { return fmt.Errorf("started at must not be nil for running records") } if record.StartedAt.IsZero() { return fmt.Errorf("started at must not be zero when present") } case StatusStopped: if strings.TrimSpace(record.CurrentImageRef) == "" { return fmt.Errorf("current image ref must not be empty for stopped records") } if record.StoppedAt == nil { return fmt.Errorf("stopped at must not be nil for stopped records") } if record.StoppedAt.IsZero() { return fmt.Errorf("stopped at must not be zero when present") } case StatusRemoved: if record.RemovedAt == nil { return fmt.Errorf("removed at must not be nil for removed records") } if record.RemovedAt.IsZero() { return fmt.Errorf("removed at must not be zero when present") } } if record.StartedAt != nil && record.StartedAt.Before(record.CreatedAt) { return fmt.Errorf("started at must not be before created at") } if record.StoppedAt != nil && record.StartedAt != nil && record.StoppedAt.Before(*record.StartedAt) { return fmt.Errorf("stopped at must not be before started at") } if record.RemovedAt != nil && record.RemovedAt.Before(record.CreatedAt) { return fmt.Errorf("removed at must not be before created at") } return nil }