package runtime import ( "context" "fmt" "sync" "sync/atomic" "github.com/google/uuid" ) // Cache is the in-memory write-through projection of the runtime // records and engine version registry. Mirrors the lobby/auth/admin // cache idiom: Postgres is the source of truth, the cache is updated // only after a successful commit. // // Reads (Get*) take RLocks; writes (Put*, Remove*) take Locks. The // cache only retains non-terminal runtime records so the active set // stays small and warm. type Cache struct { mu sync.RWMutex runtimes map[uuid.UUID]RuntimeRecord engineVersions map[string]EngineVersion ready atomic.Bool } // NewCache returns an empty Cache. func NewCache() *Cache { return &Cache{ runtimes: make(map[uuid.UUID]RuntimeRecord), engineVersions: make(map[string]EngineVersion), } } // Warm populates the cache from store. Must be called once at process // boot before the HTTP listener accepts traffic. func (c *Cache) Warm(ctx context.Context, store *Store) error { if c == nil { return nil } versions, err := store.ListEngineVersions(ctx) if err != nil { return fmt.Errorf("runtime cache warm: engine versions: %w", err) } records, err := store.ListAllRuntimeRecords(ctx) if err != nil { return fmt.Errorf("runtime cache warm: runtime records: %w", err) } c.mu.Lock() defer c.mu.Unlock() c.engineVersions = make(map[string]EngineVersion, len(versions)) for _, v := range versions { c.engineVersions[v.Version] = v } c.runtimes = make(map[uuid.UUID]RuntimeRecord) for _, r := range records { if r.IsTerminal() { continue } c.runtimes[r.GameID] = r } c.ready.Store(true) return nil } // Ready reports whether Warm completed at least once. func (c *Cache) Ready() bool { if c == nil { return false } return c.ready.Load() } // Sizes returns the cardinalities of the two projections; used by the // startup log line and tests. func (c *Cache) Sizes() (runtimes int, engineVersions int) { if c == nil { return 0, 0 } c.mu.RLock() defer c.mu.RUnlock() return len(c.runtimes), len(c.engineVersions) } // GetRuntime returns the cached runtime record for gameID together // with a presence flag. func (c *Cache) GetRuntime(gameID uuid.UUID) (RuntimeRecord, bool) { if c == nil { return RuntimeRecord{}, false } c.mu.RLock() defer c.mu.RUnlock() r, ok := c.runtimes[gameID] return r, ok } // PutRuntime stores or updates the runtime record. Terminal statuses // cause the entry to be evicted. func (c *Cache) PutRuntime(rec RuntimeRecord) { if c == nil { return } c.mu.Lock() defer c.mu.Unlock() if rec.IsTerminal() { delete(c.runtimes, rec.GameID) return } c.runtimes[rec.GameID] = rec } // RemoveRuntime evicts the entry for gameID. func (c *Cache) RemoveRuntime(gameID uuid.UUID) { if c == nil { return } c.mu.Lock() defer c.mu.Unlock() delete(c.runtimes, gameID) } // ActiveRuntimes returns a snapshot copy of every cached runtime // record. The reconciler and the scheduler both iterate this list. func (c *Cache) ActiveRuntimes() []RuntimeRecord { if c == nil { return nil } c.mu.RLock() defer c.mu.RUnlock() out := make([]RuntimeRecord, 0, len(c.runtimes)) for _, r := range c.runtimes { out = append(out, r) } return out } // GetEngineVersion returns the cached engine_versions row keyed by // version label, together with a presence flag. func (c *Cache) GetEngineVersion(version string) (EngineVersion, bool) { if c == nil { return EngineVersion{}, false } c.mu.RLock() defer c.mu.RUnlock() v, ok := c.engineVersions[version] return v, ok } // PutEngineVersion stores or updates the engine_versions cache entry. func (c *Cache) PutEngineVersion(v EngineVersion) { if c == nil { return } c.mu.Lock() defer c.mu.Unlock() c.engineVersions[v.Version] = v } // ListEngineVersions returns a snapshot of the cached engine_versions // rows ordered by created_at DESC. Falls back to a deterministic order // by version label when timestamps tie. func (c *Cache) ListEngineVersions() []EngineVersion { if c == nil { return nil } c.mu.RLock() defer c.mu.RUnlock() out := make([]EngineVersion, 0, len(c.engineVersions)) for _, v := range c.engineVersions { out = append(out, v) } return out }