feat(api): add grant liveness cache

This commit is contained in:
Deluan 2026-09-26 01:07:13 -04:00
commit 7dd8dba12b
2 changed files with 171 additions and 0 deletions

87
core/apiauth/cache.go Normal file
View file

@ -0,0 +1,87 @@
package apiauth
import (
"maps"
"sync"
"time"
)
const maxLivenessEntries = 1024
type livenessEntry struct {
userID string
epoch int
lastUsedAt time.Time
expires time.Time
}
// livenessCache bounds how long a node trusts "this grant exists" without asking the DB.
type livenessCache struct {
mu sync.Mutex
ttl time.Duration
gen uint64
entries map[string]livenessEntry
evicted map[string]uint64 // grant id -> generation of its last eviction
floor uint64 // fills started before the last trim of evicted are dropped
}
func newLivenessCache(ttl time.Duration) *livenessCache {
return &livenessCache{ttl: ttl, entries: map[string]livenessEntry{}, evicted: map[string]uint64{}}
}
func (c *livenessCache) begin() uint64 {
c.mu.Lock()
defer c.mu.Unlock()
return c.gen
}
func (c *livenessCache) get(id string, now time.Time) (livenessEntry, bool) {
c.mu.Lock()
defer c.mu.Unlock()
e, ok := c.entries[id]
if !ok || !now.Before(e.expires) {
return livenessEntry{}, false
}
return e, true
}
// put ignores a fill whose DB read started before the grant was last evicted.
func (c *livenessCache) put(id string, e livenessEntry, now time.Time, started uint64) {
c.mu.Lock()
defer c.mu.Unlock()
if started < c.floor || c.evicted[id] > started {
return
}
if len(c.entries) >= maxLivenessEntries {
maps.DeleteFunc(c.entries, func(_ string, v livenessEntry) bool { return !now.Before(v.expires) })
}
if len(c.evicted) >= maxLivenessEntries {
clear(c.evicted)
c.floor = c.gen
}
e.expires = now.Add(c.ttl)
c.entries[id] = e
}
func (c *livenessCache) evict(id string) {
c.mu.Lock()
defer c.mu.Unlock()
c.gen++
c.evicted[id] = c.gen
delete(c.entries, id)
}
func (c *livenessCache) markUsed(id string, at time.Time) {
c.mu.Lock()
defer c.mu.Unlock()
if e, ok := c.entries[id]; ok {
e.lastUsedAt = at
c.entries[id] = e
}
}
func (c *livenessCache) len() int {
c.mu.Lock()
defer c.mu.Unlock()
return len(c.entries)
}

View file

@ -0,0 +1,84 @@
package apiauth
import (
"fmt"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("livenessCache", func() {
var c *livenessCache
var t0 time.Time
BeforeEach(func() {
c = newLivenessCache(30 * time.Second)
t0 = time.Date(2026, 9, 25, 12, 0, 0, 0, time.UTC)
})
It("returns an entry until its TTL passes", func() {
c.put("g1", livenessEntry{userID: "u1", epoch: 2}, t0, c.begin())
e, ok := c.get("g1", t0.Add(29*time.Second))
Expect(ok).To(BeTrue())
Expect(e.userID).To(Equal("u1"))
Expect(e.epoch).To(Equal(2))
_, ok = c.get("g1", t0.Add(30*time.Second))
Expect(ok).To(BeFalse())
})
It("forgets evicted entries", func() {
c.put("g1", livenessEntry{userID: "u1"}, t0, c.begin())
c.evict("g1")
_, ok := c.get("g1", t0)
Expect(ok).To(BeFalse())
})
It("drops a fill that started before an eviction of the same grant", func() {
started := c.begin() // a request reads the grant from the DB...
c.evict("g1") // ...a logout deletes and evicts it...
c.put("g1", livenessEntry{userID: "u1"}, t0, started) // ...then the slow request tries to cache it
_, ok := c.get("g1", t0)
Expect(ok).To(BeFalse())
})
It("still accepts fills of other grants and later fills of the same grant", func() {
started := c.begin()
c.evict("g1")
c.put("g2", livenessEntry{userID: "u2"}, t0, started)
_, ok := c.get("g2", t0)
Expect(ok).To(BeTrue())
c.put("g1", livenessEntry{userID: "u1"}, t0, c.begin())
_, ok = c.get("g1", t0)
Expect(ok).To(BeTrue())
})
It("still drops a racing fill after the eviction log is trimmed", func() {
started := c.begin()
c.evict("g1")
for i := range maxLivenessEntries {
c.evict(fmt.Sprint("other", i))
}
c.put("fresh", livenessEntry{}, t0, c.begin())
c.put("g1", livenessEntry{userID: "u1"}, t0, started)
_, ok := c.get("g1", t0)
Expect(ok).To(BeFalse())
})
It("records the last use without extending the TTL", func() {
c.put("g1", livenessEntry{userID: "u1"}, t0, c.begin())
c.markUsed("g1", t0.Add(10*time.Second))
e, _ := c.get("g1", t0.Add(11*time.Second))
Expect(e.lastUsedAt).To(Equal(t0.Add(10 * time.Second)))
_, ok := c.get("g1", t0.Add(30*time.Second))
Expect(ok).To(BeFalse())
})
It("drops expired entries when it grows", func() {
for i := range maxLivenessEntries {
c.put(fmt.Sprint(i), livenessEntry{}, t0, c.begin())
}
c.put("fresh", livenessEntry{}, t0.Add(time.Minute), c.begin())
Expect(c.len()).To(Equal(1))
})
})