// 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 }