197 lines
4.7 KiB
Go
197 lines
4.7 KiB
Go
package affinity
|
|
|
|
import (
|
|
"container/list"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
DefaultTTL = time.Hour
|
|
DefaultCapacity = 10_000
|
|
)
|
|
|
|
// Target is the exact Credential identity remembered as a soft preference.
|
|
type Target struct {
|
|
GroupID uint
|
|
CredentialID uint
|
|
IdentityGeneration uint64
|
|
}
|
|
|
|
func (target Target) Valid() bool {
|
|
return target.GroupID != 0 && target.CredentialID != 0 && target.IdentityGeneration != 0
|
|
}
|
|
|
|
// Observation is a versioned cache lookup used for conditional success updates.
|
|
type Observation struct {
|
|
Target Target
|
|
key Key
|
|
revision uint64
|
|
version uint64
|
|
found bool
|
|
}
|
|
|
|
func (observation Observation) Found() bool {
|
|
return observation.found
|
|
}
|
|
|
|
type cacheEntry struct {
|
|
key Key
|
|
target Target
|
|
version uint64
|
|
expiresAt time.Time
|
|
}
|
|
|
|
// Cache is a bounded, process-local soft-affinity cache.
|
|
type Cache struct {
|
|
mu sync.Mutex
|
|
entries map[Key]*list.Element
|
|
recent list.List
|
|
capacity int
|
|
ttl time.Duration
|
|
now func() time.Time
|
|
revision uint64
|
|
nextVersion uint64
|
|
}
|
|
|
|
func NewCache() *Cache {
|
|
return newCache(DefaultCapacity, DefaultTTL, time.Now)
|
|
}
|
|
|
|
func newCache(capacity int, ttl time.Duration, now func() time.Time) *Cache {
|
|
return &Cache{
|
|
entries: make(map[Key]*list.Element),
|
|
capacity: capacity,
|
|
ttl: ttl,
|
|
now: now,
|
|
}
|
|
}
|
|
|
|
// Configure applies one frozen runtime configuration revision. Moving to a
|
|
// newer revision clears entries so changed TTL, capacity, and group policy
|
|
// take effect atomically. Older requests cannot restore stale configuration.
|
|
func (cache *Cache) Configure(revision uint64, capacity int, ttl time.Duration) bool {
|
|
if cache == nil || revision == 0 || capacity <= 0 || ttl <= 0 || cache.now == nil {
|
|
return false
|
|
}
|
|
cache.mu.Lock()
|
|
defer cache.mu.Unlock()
|
|
if revision < cache.revision {
|
|
return false
|
|
}
|
|
if revision == cache.revision {
|
|
return cache.capacity == capacity && cache.ttl == ttl
|
|
}
|
|
cache.revision = revision
|
|
cache.capacity = capacity
|
|
cache.ttl = ttl
|
|
cache.entries = make(map[Key]*list.Element)
|
|
cache.recent.Init()
|
|
return true
|
|
}
|
|
|
|
func (cache *Cache) Lookup(key Key) Observation {
|
|
if cache == nil || !key.Valid() || cache.now == nil {
|
|
return Observation{}
|
|
}
|
|
cache.mu.Lock()
|
|
defer cache.mu.Unlock()
|
|
element := cache.currentElementLocked(key, cache.now())
|
|
if element == nil {
|
|
return Observation{key: key, revision: cache.revision}
|
|
}
|
|
cache.recent.MoveToFront(element)
|
|
entry := element.Value.(*cacheEntry)
|
|
return Observation{
|
|
Target: entry.target, key: key, revision: cache.revision,
|
|
version: entry.version, found: true,
|
|
}
|
|
}
|
|
|
|
// RecordSuccess conditionally learns the successful target. It returns true
|
|
// when this call inserted, refreshed, or changed the current mapping.
|
|
func (cache *Cache) RecordSuccess(key Key, observed Observation, target Target) bool {
|
|
if cache == nil || !key.Valid() || !target.Valid() || cache.now == nil {
|
|
return false
|
|
}
|
|
if observed.key.Valid() && observed.key != key {
|
|
return false
|
|
}
|
|
|
|
cache.mu.Lock()
|
|
defer cache.mu.Unlock()
|
|
if cache.capacity <= 0 || cache.ttl <= 0 || observed.revision != cache.revision {
|
|
return false
|
|
}
|
|
now := cache.now()
|
|
element := cache.currentElementLocked(key, now)
|
|
if observed.found {
|
|
if element != nil {
|
|
current := element.Value.(*cacheEntry)
|
|
if current.version != observed.version || current.target != observed.Target {
|
|
return false
|
|
}
|
|
} else {
|
|
return cache.insertLocked(key, target, now)
|
|
}
|
|
} else if element != nil {
|
|
return false
|
|
}
|
|
|
|
if element == nil {
|
|
return cache.insertLocked(key, target, now)
|
|
}
|
|
cache.nextVersion++
|
|
entry := element.Value.(*cacheEntry)
|
|
entry.target = target
|
|
entry.version = cache.nextVersion
|
|
entry.expiresAt = now.Add(cache.ttl)
|
|
cache.recent.MoveToFront(element)
|
|
return true
|
|
}
|
|
|
|
func (cache *Cache) entryCount() int {
|
|
if cache == nil {
|
|
return 0
|
|
}
|
|
cache.mu.Lock()
|
|
defer cache.mu.Unlock()
|
|
return len(cache.entries)
|
|
}
|
|
|
|
func (cache *Cache) currentElementLocked(key Key, now time.Time) *list.Element {
|
|
element := cache.entries[key]
|
|
if element == nil {
|
|
return nil
|
|
}
|
|
entry := element.Value.(*cacheEntry)
|
|
if !now.Before(entry.expiresAt) {
|
|
cache.removeLocked(element)
|
|
return nil
|
|
}
|
|
return element
|
|
}
|
|
|
|
func (cache *Cache) insertLocked(key Key, target Target, now time.Time) bool {
|
|
for len(cache.entries) >= cache.capacity {
|
|
oldest := cache.recent.Back()
|
|
if oldest == nil {
|
|
break
|
|
}
|
|
cache.removeLocked(oldest)
|
|
}
|
|
cache.nextVersion++
|
|
entry := &cacheEntry{
|
|
key: key, target: target, version: cache.nextVersion,
|
|
expiresAt: now.Add(cache.ttl),
|
|
}
|
|
cache.entries[key] = cache.recent.PushFront(entry)
|
|
return true
|
|
}
|
|
|
|
func (cache *Cache) removeLocked(element *list.Element) {
|
|
entry := element.Value.(*cacheEntry)
|
|
delete(cache.entries, entry.key)
|
|
cache.recent.Remove(element)
|
|
}
|