feat: backend service
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user