Files
2026-05-06 10:14:55 +03:00

105 lines
2.9 KiB
Go

package user
import (
"context"
"sync"
"sync/atomic"
"github.com/google/uuid"
)
// Cache is the in-memory write-through projection of the active rows in
// `backend.entitlement_snapshots`. Reads (Get) are RLocked; writes (Add)
// are Locked.
//
// The cache keys snapshots by user_id. Soft-delete does not evict
// entries — read paths gate visibility through the
// `accounts.deleted_at IS NULL` predicate, so a cached snapshot for a
// soft-deleted user is harmless and is reaped on the next process
// reboot.
//
// The caller is expected to commit the corresponding database write
// *before* invoking Add so that the cache stays consistent under crash:
// a Postgres commit failure leaves the cache untouched, matching the
// previous DB state. This mirrors the post-commit write-through pattern
// established in `backend/internal/auth.Cache`.
type Cache struct {
mu sync.RWMutex
byID map[uuid.UUID]EntitlementSnapshot
ready atomic.Bool
}
// NewCache constructs an empty Cache. The cache reports Ready() == false
// until Warm completes successfully.
func NewCache() *Cache {
return &Cache{
byID: make(map[uuid.UUID]EntitlementSnapshot),
}
}
// Warm replaces the cache contents with every row loaded from store. It
// is intended to be called exactly once at process boot before the HTTP
// listener accepts traffic; successful completion flips Ready to true.
// Subsequent calls re-warm the cache (useful in tests).
func (c *Cache) Warm(ctx context.Context, store *Store) error {
if c == nil {
return nil
}
snapshots, err := store.ListEntitlementSnapshots(ctx)
if err != nil {
return err
}
c.mu.Lock()
defer c.mu.Unlock()
c.byID = make(map[uuid.UUID]EntitlementSnapshot, len(snapshots))
for _, snap := range snapshots {
c.byID[snap.UserID] = snap
}
c.ready.Store(true)
return nil
}
// Ready reports whether Warm has completed at least once. The HTTP
// readiness probe wires through this method together with the auth
// cache so `/readyz` only flips to 200 after every cache is hydrated.
func (c *Cache) Ready() bool {
if c == nil {
return false
}
return c.ready.Load()
}
// Size returns the number of cached entitlement snapshots. Useful for
// the startup log line and tests.
func (c *Cache) Size() int {
if c == nil {
return 0
}
c.mu.RLock()
defer c.mu.RUnlock()
return len(c.byID)
}
// Get returns the snapshot for userID and a presence flag. Misses
// always return the zero EntitlementSnapshot and false.
func (c *Cache) Get(userID uuid.UUID) (EntitlementSnapshot, bool) {
if c == nil {
return EntitlementSnapshot{}, false
}
c.mu.RLock()
defer c.mu.RUnlock()
s, ok := c.byID[userID]
return s, ok
}
// Add stores snap in the cache. It is safe to call on an existing
// entry — the value is overwritten with the latest snapshot.
func (c *Cache) Add(snap EntitlementSnapshot) {
if c == nil {
return
}
c.mu.Lock()
defer c.mu.Unlock()
c.byID[snap.UserID] = snap
}