package session import ( "context" "sync" "sync/atomic" ) // Cache is the in-memory write-through projection of the active rows in // backend.sessions, keyed by token hash so Resolve avoids a database round-trip // on the hot path. Reads are RLocked; writes are Locked. Callers commit the // corresponding database write before invoking Add or Remove so the cache stays // consistent with the persisted state. type Cache struct { mu sync.RWMutex byHash map[string]Session ready atomic.Bool } // NewCache constructs an empty Cache. It reports Ready() == false until Warm // completes successfully. func NewCache() *Cache { return &Cache{byHash: make(map[string]Session)} } // Warm replaces the cache contents with every active session loaded from store. // It is intended to run once at process boot before the listener accepts // traffic; success flips Ready to true. Re-warming is supported (useful in // tests). func (c *Cache) Warm(ctx context.Context, store *Store) error { sessions, err := store.ListActive(ctx) if err != nil { return err } c.mu.Lock() defer c.mu.Unlock() c.byHash = make(map[string]Session, len(sessions)) for _, s := range sessions { c.byHash[s.TokenHash] = s } c.ready.Store(true) return nil } // Ready reports whether Warm has completed at least once. The /readyz probe // wires through this so the backend only reports ready once sessions are // hydrated. func (c *Cache) Ready() bool { if c == nil { return false } return c.ready.Load() } // Size returns the number of cached active sessions, for startup logs and tests. func (c *Cache) Size() int { if c == nil { return 0 } c.mu.RLock() defer c.mu.RUnlock() return len(c.byHash) } // Get returns the session for tokenHash and a presence flag. A miss returns the // zero Session and false. func (c *Cache) Get(tokenHash string) (Session, bool) { if c == nil { return Session{}, false } c.mu.RLock() defer c.mu.RUnlock() s, ok := c.byHash[tokenHash] return s, ok } // Add stores s under its token hash. It is safe to call on an existing entry. func (c *Cache) Add(s Session) { if c == nil { return } c.mu.Lock() defer c.mu.Unlock() c.byHash[s.TokenHash] = s } // Remove evicts the entry for tokenHash. Removing a missing entry is a no-op. func (c *Cache) Remove(tokenHash string) { if c == nil { return } c.mu.Lock() defer c.mu.Unlock() delete(c.byHash, tokenHash) }