Files
galaxy-game/backend/internal/runtime/cache.go
T
2026-05-06 10:14:55 +03:00

175 lines
4.2 KiB
Go

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
}