mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +00:00
413e4a1b7d
* LRU + cache conflicts prevention. * Bugfix universalCache flooding ( issue #105 ) 1. Traefik cancels the context for old plugin instances 2. Each plugin's Close() method is called 3. The CacheInterfaceWrapper.Close() was calling cache.Close() on the shared singleton caches 4. Each Close() triggered Clear() which logged "Cleared all items" at INFO level
291 lines
5.9 KiB
Go
291 lines
5.9 KiB
Go
package backends
|
|
|
|
import (
|
|
"container/list"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// cacheShard represents a single shard of the sharded cache
|
|
// Each shard has its own lock for reduced contention
|
|
type cacheShard struct {
|
|
items map[string]*memoryCacheItem
|
|
lruList *list.List
|
|
mu sync.RWMutex
|
|
maxSize int64
|
|
maxMemory int64
|
|
size int64
|
|
memoryUsed int64
|
|
}
|
|
|
|
// newCacheShard creates a new cache shard
|
|
func newCacheShard(maxSize, maxMemory int64) *cacheShard {
|
|
return &cacheShard{
|
|
items: make(map[string]*memoryCacheItem),
|
|
lruList: list.New(),
|
|
maxSize: maxSize,
|
|
maxMemory: maxMemory,
|
|
}
|
|
}
|
|
|
|
// get retrieves a value from this shard
|
|
// Returns: value, exists, expired
|
|
func (s *cacheShard) get(key string) (interface{}, bool, bool) {
|
|
s.mu.RLock()
|
|
item, exists := s.items[key]
|
|
s.mu.RUnlock()
|
|
|
|
if !exists {
|
|
return nil, false, false
|
|
}
|
|
|
|
if item.isExpired() {
|
|
return nil, true, true // exists but expired
|
|
}
|
|
|
|
// Update access time and LRU position under write lock
|
|
s.mu.Lock()
|
|
// Re-check item exists (could have been deleted)
|
|
item, exists = s.items[key]
|
|
if exists && !item.isExpired() {
|
|
item.accessedAt = time.Now()
|
|
item.accessCount++
|
|
if elem, ok := item.element.(*list.Element); ok && elem != nil {
|
|
s.lruList.MoveToFront(elem)
|
|
}
|
|
}
|
|
s.mu.Unlock()
|
|
|
|
if !exists || item.isExpired() {
|
|
return nil, false, false
|
|
}
|
|
|
|
return item.value, true, false
|
|
}
|
|
|
|
// set stores a value in this shard
|
|
func (s *cacheShard) set(key string, value interface{}, expiresAt time.Time, size int64) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
// Check if we need to evict items
|
|
if s.maxSize > 0 && s.size >= s.maxSize {
|
|
s.evictLRULocked()
|
|
}
|
|
if s.maxMemory > 0 && s.memoryUsed+size > s.maxMemory {
|
|
s.evictLRULocked()
|
|
}
|
|
|
|
// Remove old item if exists
|
|
if oldItem, exists := s.items[key]; exists {
|
|
s.memoryUsed -= oldItem.size
|
|
if elem, ok := oldItem.element.(*list.Element); ok && elem != nil {
|
|
s.lruList.Remove(elem)
|
|
}
|
|
s.size--
|
|
}
|
|
|
|
now := time.Now()
|
|
item := &memoryCacheItem{
|
|
key: key,
|
|
value: value,
|
|
expiresAt: expiresAt,
|
|
createdAt: now,
|
|
accessedAt: now,
|
|
accessCount: 0,
|
|
size: size,
|
|
}
|
|
|
|
item.element = s.lruList.PushFront(item)
|
|
s.items[key] = item
|
|
s.size++
|
|
s.memoryUsed += size
|
|
}
|
|
|
|
// delete removes a key from this shard
|
|
// Returns true if the key was deleted
|
|
func (s *cacheShard) delete(key string) bool {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
item, exists := s.items[key]
|
|
if !exists {
|
|
return false
|
|
}
|
|
|
|
s.deleteItemLocked(item)
|
|
return true
|
|
}
|
|
|
|
// exists checks if a key exists (and is not expired)
|
|
func (s *cacheShard) exists(key string) bool {
|
|
s.mu.RLock()
|
|
item, exists := s.items[key]
|
|
s.mu.RUnlock()
|
|
|
|
if !exists {
|
|
return false
|
|
}
|
|
|
|
return !item.isExpired()
|
|
}
|
|
|
|
// ttl returns the remaining TTL for a key
|
|
func (s *cacheShard) ttl(key string) (time.Duration, bool) {
|
|
s.mu.RLock()
|
|
item, exists := s.items[key]
|
|
s.mu.RUnlock()
|
|
|
|
if !exists || item.isExpired() {
|
|
return 0, false
|
|
}
|
|
|
|
if item.expiresAt.IsZero() {
|
|
return 0, true // No expiration
|
|
}
|
|
|
|
remaining := time.Until(item.expiresAt)
|
|
if remaining < 0 {
|
|
return 0, false
|
|
}
|
|
|
|
return remaining, true
|
|
}
|
|
|
|
// expire updates the TTL for an existing key
|
|
func (s *cacheShard) expire(key string, ttl time.Duration) bool {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
item, exists := s.items[key]
|
|
if !exists || item.isExpired() {
|
|
return false
|
|
}
|
|
|
|
if ttl > 0 {
|
|
item.expiresAt = time.Now().Add(ttl)
|
|
} else {
|
|
item.expiresAt = time.Time{} // Remove expiration
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// keys returns all non-expired keys matching the pattern
|
|
func (s *cacheShard) keys(pattern string) []string {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
var keys []string
|
|
for key, item := range s.items {
|
|
if !item.isExpired() && matchPattern(pattern, key) {
|
|
keys = append(keys, key)
|
|
}
|
|
}
|
|
return keys
|
|
}
|
|
|
|
// clear removes all items from this shard
|
|
func (s *cacheShard) clear() {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
s.items = make(map[string]*memoryCacheItem)
|
|
s.lruList.Init()
|
|
s.size = 0
|
|
s.memoryUsed = 0
|
|
}
|
|
|
|
// cleanup removes expired items
|
|
// Returns the number of items removed
|
|
func (s *cacheShard) cleanup() int {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
var toRemove []*memoryCacheItem
|
|
for _, item := range s.items {
|
|
if item.isExpired() {
|
|
toRemove = append(toRemove, item)
|
|
}
|
|
}
|
|
|
|
for _, item := range toRemove {
|
|
s.deleteItemLocked(item)
|
|
}
|
|
|
|
return len(toRemove)
|
|
}
|
|
|
|
// stats returns statistics for this shard
|
|
func (s *cacheShard) stats() (size, memory int64) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
return s.size, s.memoryUsed
|
|
}
|
|
|
|
// deleteItemLocked removes an item (must be called with lock held)
|
|
func (s *cacheShard) deleteItemLocked(item *memoryCacheItem) {
|
|
if elem, ok := item.element.(*list.Element); ok && elem != nil {
|
|
s.lruList.Remove(elem)
|
|
}
|
|
delete(s.items, item.key)
|
|
s.size--
|
|
s.memoryUsed -= item.size
|
|
}
|
|
|
|
// evictLRULocked evicts the least recently used item (must be called with lock held)
|
|
func (s *cacheShard) evictLRULocked() bool {
|
|
if s.lruList.Len() == 0 {
|
|
return false
|
|
}
|
|
|
|
element := s.lruList.Back()
|
|
if element != nil {
|
|
item := element.Value.(*memoryCacheItem)
|
|
s.deleteItemLocked(item)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// evictOne evicts one item from this shard (for global limit enforcement)
|
|
func (s *cacheShard) evictOne() bool {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.evictLRULocked()
|
|
}
|
|
|
|
// getOldestAccessTime returns the access time of the LRU item (oldest) in this shard
|
|
// Returns zero time if shard is empty
|
|
func (s *cacheShard) getOldestAccessTime() time.Time {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
if s.lruList.Len() == 0 {
|
|
return time.Time{}
|
|
}
|
|
|
|
element := s.lruList.Back()
|
|
if element != nil {
|
|
item := element.Value.(*memoryCacheItem)
|
|
return item.accessedAt
|
|
}
|
|
return time.Time{}
|
|
}
|
|
|
|
// fnv32 computes FNV-1a hash of a string
|
|
// This is a fast, well-distributed hash function
|
|
func fnv32(key string) uint32 {
|
|
const (
|
|
offset32 = uint32(2166136261)
|
|
prime32 = uint32(16777619)
|
|
)
|
|
|
|
hash := offset32
|
|
for i := 0; i < len(key); i++ {
|
|
hash ^= uint32(key[i])
|
|
hash *= prime32
|
|
}
|
|
return hash
|
|
}
|