105 lines
2.9 KiB
Go
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
|
|
}
|