Files
user-system/internal/cache/l1.go

238 lines
6.0 KiB
Go
Raw Normal View History

// Package cache provides in-memory L1 cache with true O(1) LRU eviction.
//
// Implementation uses a doubly-linked list + hash-map, giving O(1) for Get, Set,
// Delete and eviction — compared to the previous O(n) slice-scan approach which
// became a bottleneck under high concurrency (10 000-item cache, 1 000+ QPS).
//
// Thread-safety: a single sync.RWMutex guards the whole structure.
// Reads (Get) promote the entry to MRU and therefore must take a write lock.
// If read-heavy workloads dominate, consider a sharded variant.
package cache
import (
"container/list"
"sync"
"time"
)
const (
// defaultMaxItems is the maximum number of entries held in L1Cache.
// Entries beyond this limit are evicted using LRU policy (O(1)).
defaultMaxItems = 10000
)
// CacheItem holds a cached value together with its expiry timestamp.
type CacheItem struct {
Value interface{}
Expiration int64 // UnixNano; 0 means no expiration
}
// Expired reports whether this item has passed its TTL.
func (item *CacheItem) Expired() bool {
return item.Expiration > 0 && time.Now().UnixNano() > item.Expiration
}
// lruEntry is the value stored inside the doubly-linked list element.
type lruEntry struct {
key string
item *CacheItem
}
// L1Cache is an in-process LRU cache backed by a hash-map and a doubly-linked
// list. All exported methods are safe for concurrent use.
type L1Cache struct {
mu sync.RWMutex
items map[string]*list.Element // key → list element
lruList *list.List // front = MRU, back = LRU
maxItems int
}
// NewL1Cache creates a new L1Cache with the default capacity (10 000 items).
func NewL1Cache() *L1Cache {
return &L1Cache{
items: make(map[string]*list.Element, defaultMaxItems),
lruList: list.New(),
maxItems: defaultMaxItems,
}
}
// NewL1CacheWithSize creates a new L1Cache with a custom capacity.
func NewL1CacheWithSize(maxItems int) *L1Cache {
if maxItems <= 0 {
maxItems = defaultMaxItems
}
return &L1Cache{
items: make(map[string]*list.Element, maxItems),
lruList: list.New(),
maxItems: maxItems,
}
}
// Set inserts or updates key with the given value and TTL.
// A zero or negative TTL means the entry never expires.
// O(1) amortised.
func (c *L1Cache) Set(key string, value interface{}, ttl time.Duration) {
var expiration int64
if ttl > 0 {
expiration = time.Now().Add(ttl).UnixNano()
}
c.mu.Lock()
defer c.mu.Unlock()
if elem, ok := c.items[key]; ok {
// Update existing entry and move to front (MRU).
c.lruList.MoveToFront(elem)
entry := elem.Value.(*lruEntry)
entry.item = &CacheItem{Value: value, Expiration: expiration}
return
}
// Evict LRU entry if at capacity.
if c.lruList.Len() >= c.maxItems {
c.evictLRU()
}
// Insert new entry at front.
entry := &lruEntry{
key: key,
item: &CacheItem{Value: value, Expiration: expiration},
}
elem := c.lruList.PushFront(entry)
c.items[key] = elem
}
// evictLRU removes the least-recently-used entry. Must be called with c.mu held.
func (c *L1Cache) evictLRU() {
back := c.lruList.Back()
if back == nil {
return
}
entry := back.Value.(*lruEntry)
delete(c.items, entry.key)
c.lruList.Remove(back)
}
// Get retrieves a value from the cache.
// On a hit the entry is promoted to MRU (requires write lock).
// On expiry the entry is removed and (nil, false) is returned.
// O(1).
func (c *L1Cache) Get(key string) (interface{}, bool) {
c.mu.Lock()
defer c.mu.Unlock()
elem, ok := c.items[key]
if !ok {
return nil, false
}
entry := elem.Value.(*lruEntry)
if entry.item.Expired() {
c.lruList.Remove(elem)
delete(c.items, key)
return nil, false
}
// Promote to MRU.
c.lruList.MoveToFront(elem)
return entry.item.Value, true
}
// Delete removes a key from the cache. No-op if the key is absent.
// O(1).
func (c *L1Cache) Delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()
if elem, ok := c.items[key]; ok {
c.lruList.Remove(elem)
delete(c.items, key)
}
}
// Clear removes all entries from the cache.
func (c *L1Cache) Clear() {
c.mu.Lock()
defer c.mu.Unlock()
c.items = make(map[string]*list.Element, c.maxItems)
c.lruList.Init()
}
// Size returns the number of entries currently held (including potentially
// expired ones that have not yet been evicted).
func (c *L1Cache) Size() int {
c.mu.RLock()
defer c.mu.RUnlock()
return len(c.items)
}
// Cleanup scans all entries and removes those that have expired.
// This is a background maintenance operation; normal eviction is lazy (on Get).
func (c *L1Cache) Cleanup() {
c.mu.Lock()
defer c.mu.Unlock()
now := time.Now().UnixNano()
var toRemove []*list.Element
for elem := c.lruList.Back(); elem != nil; elem = elem.Prev() {
entry := elem.Value.(*lruEntry)
if entry.item.Expiration > 0 && now > entry.item.Expiration {
toRemove = append(toRemove, elem)
}
}
for _, elem := range toRemove {
entry := elem.Value.(*lruEntry)
delete(c.items, entry.key)
c.lruList.Remove(elem)
}
}
// Increment atomically adds delta to the int64 counter stored at key,
// creating it with value delta if it does not exist.
// Used for rate-limit counters, login-failure counters, etc.
// O(1).
func (c *L1Cache) Increment(key string, delta int64, ttl time.Duration) int64 {
var expiration int64
if ttl > 0 {
expiration = time.Now().Add(ttl).UnixNano()
}
c.mu.Lock()
defer c.mu.Unlock()
if elem, ok := c.items[key]; ok {
entry := elem.Value.(*lruEntry)
if !entry.item.Expired() {
current := int64(0)
switch v := entry.item.Value.(type) {
case int64:
current = v
case int:
current = int64(v)
case float64:
current = int64(v)
}
newVal := current + delta
entry.item = &CacheItem{Value: newVal, Expiration: expiration}
c.lruList.MoveToFront(elem)
return newVal
}
// Expired: remove and recreate below.
c.lruList.Remove(elem)
delete(c.items, key)
}
// Key absent or expired: insert fresh counter.
if c.lruList.Len() >= c.maxItems {
c.evictLRU()
}
entry := &lruEntry{
key: key,
item: &CacheItem{Value: delta, Expiration: expiration},
}
elem := c.lruList.PushFront(entry)
c.items[key] = elem
return delta
}