// Package backend provides cache backend implementations for the Traefik OIDC plugin. package backends import ( "container/list" "context" "sync" "sync/atomic" "time" ) // memoryCacheItem represents an item in the memory cache type memoryCacheItem struct { key string value interface{} expiresAt time.Time createdAt time.Time accessedAt time.Time accessCount int64 size int64 element *list.Element // for LRU tracking } // isExpired checks if the item is expired func (item *memoryCacheItem) isExpired() bool { if item.expiresAt.IsZero() { return false } return time.Now().After(item.expiresAt) } // MemoryCacheBackend implements the CacheBackend interface using in-memory storage type MemoryCacheBackend struct { mu sync.RWMutex items map[string]*memoryCacheItem lruList *list.List maxSize int64 maxMemory int64 currentSize int64 currentMemory int64 // Statistics hits atomic.Int64 misses atomic.Int64 sets atomic.Int64 deletes atomic.Int64 evictions atomic.Int64 errors atomic.Int64 // Latency tracking totalGetTime atomic.Int64 totalSetTime atomic.Int64 getCount atomic.Int64 setCount atomic.Int64 // Status startTime time.Time lastError string lastErrorTime time.Time cleanupTicker *time.Ticker cleanupDone chan bool closed atomic.Bool // Configuration cleanupInterval time.Duration evictionPolicy string // "lru", "lfu", "fifo" } // NewMemoryCacheBackend creates a new memory cache backend func NewMemoryCacheBackend(maxSize int64, maxMemory int64, cleanupInterval time.Duration) *MemoryCacheBackend { if maxSize <= 0 { maxSize = 10000 // Default to 10k items } if maxMemory <= 0 { maxMemory = 100 * 1024 * 1024 // Default to 100MB } if cleanupInterval <= 0 { cleanupInterval = 5 * time.Minute } m := &MemoryCacheBackend{ items: make(map[string]*memoryCacheItem), lruList: list.New(), maxSize: maxSize, maxMemory: maxMemory, startTime: time.Now(), cleanupInterval: cleanupInterval, evictionPolicy: "lru", cleanupDone: make(chan bool), } // Start cleanup goroutine m.cleanupTicker = time.NewTicker(cleanupInterval) go m.cleanupLoop() return m } // cleanupLoop runs periodic cleanup of expired items func (m *MemoryCacheBackend) cleanupLoop() { for { select { case <-m.cleanupTicker.C: m.cleanupExpired() case <-m.cleanupDone: return } } } // cleanupExpired removes all expired items from the cache func (m *MemoryCacheBackend) cleanupExpired() { m.mu.Lock() defer m.mu.Unlock() var keysToDelete []string for key, item := range m.items { if item.isExpired() { keysToDelete = append(keysToDelete, key) } } for _, key := range keysToDelete { m.deleteItemLocked(key) } } // Get retrieves a value from the cache func (m *MemoryCacheBackend) Get(ctx context.Context, key string) (interface{}, error) { if m.closed.Load() { return nil, ErrBackendUnavailable } start := time.Now() defer func() { duration := time.Since(start).Nanoseconds() m.totalGetTime.Add(duration) m.getCount.Add(1) }() m.mu.RLock() item, exists := m.items[key] m.mu.RUnlock() if !exists { m.misses.Add(1) return nil, ErrCacheMiss } if item.isExpired() { m.mu.Lock() m.deleteItemLocked(key) m.mu.Unlock() m.misses.Add(1) return nil, ErrCacheMiss } // Update access time and count m.mu.Lock() item.accessedAt = time.Now() item.accessCount++ // Move to front of LRU list if m.evictionPolicy == "lru" && item.element != nil { m.lruList.MoveToFront(item.element) } m.mu.Unlock() m.hits.Add(1) return item.value, nil } // Set stores a value in the cache with optional TTL func (m *MemoryCacheBackend) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error { if m.closed.Load() { return ErrBackendUnavailable } start := time.Now() defer func() { duration := time.Since(start).Nanoseconds() m.totalSetTime.Add(duration) m.setCount.Add(1) }() // Calculate item size (simplified estimation) itemSize := int64(len(key)) + estimateValueSize(value) m.mu.Lock() defer m.mu.Unlock() // Check if we need to evict items if m.currentSize >= m.maxSize || m.currentMemory+itemSize > m.maxMemory { m.evictLocked() } // Check if key exists if oldItem, exists := m.items[key]; exists { m.currentMemory -= oldItem.size if oldItem.element != nil { m.lruList.Remove(oldItem.element) } } else { m.currentSize++ } now := time.Now() var expiresAt time.Time if ttl > 0 { expiresAt = now.Add(ttl) } item := &memoryCacheItem{ key: key, value: value, expiresAt: expiresAt, createdAt: now, accessedAt: now, accessCount: 0, size: itemSize, } // Add to LRU list if m.evictionPolicy == "lru" { item.element = m.lruList.PushFront(item) } m.items[key] = item m.currentMemory += itemSize m.sets.Add(1) return nil } // Delete removes a key from the cache func (m *MemoryCacheBackend) Delete(ctx context.Context, key string) error { if m.closed.Load() { return ErrBackendUnavailable } m.mu.Lock() defer m.mu.Unlock() if _, exists := m.items[key]; !exists { return nil } m.deleteItemLocked(key) m.deletes.Add(1) return nil } // deleteItemLocked deletes an item without acquiring the lock (must be called with lock held) func (m *MemoryCacheBackend) deleteItemLocked(key string) { if item, exists := m.items[key]; exists { m.currentMemory -= item.size m.currentSize-- if item.element != nil { m.lruList.Remove(item.element) } delete(m.items, key) } } // evictLocked evicts items based on the eviction policy (must be called with lock held) func (m *MemoryCacheBackend) evictLocked() { if m.evictionPolicy == "lru" && m.lruList.Len() > 0 { // Evict least recently used item element := m.lruList.Back() if element != nil { item := element.Value.(*memoryCacheItem) m.deleteItemLocked(item.key) m.evictions.Add(1) } } } // Exists checks if a key exists in the cache func (m *MemoryCacheBackend) Exists(ctx context.Context, key string) (bool, error) { if m.closed.Load() { return false, ErrBackendUnavailable } m.mu.RLock() item, exists := m.items[key] m.mu.RUnlock() if !exists { return false, nil } return !item.isExpired(), nil } // Clear removes all items from the cache func (m *MemoryCacheBackend) Clear(ctx context.Context) error { if m.closed.Load() { return ErrBackendUnavailable } m.mu.Lock() defer m.mu.Unlock() m.items = make(map[string]*memoryCacheItem) m.lruList = list.New() m.currentSize = 0 m.currentMemory = 0 return nil } // Keys returns all keys matching the pattern (use "*" for all keys) func (m *MemoryCacheBackend) Keys(ctx context.Context, pattern string) ([]string, error) { if m.closed.Load() { return nil, ErrBackendUnavailable } m.mu.RLock() defer m.mu.RUnlock() var keys []string for key, item := range m.items { if !item.isExpired() && matchPattern(pattern, key) { keys = append(keys, key) } } return keys, nil } // Size returns the number of items in the cache func (m *MemoryCacheBackend) Size(ctx context.Context) (int64, error) { if m.closed.Load() { return 0, ErrBackendUnavailable } m.mu.RLock() defer m.mu.RUnlock() return m.currentSize, nil } // TTL returns the remaining time-to-live for a key func (m *MemoryCacheBackend) TTL(ctx context.Context, key string) (time.Duration, error) { if m.closed.Load() { return 0, ErrBackendUnavailable } m.mu.RLock() item, exists := m.items[key] m.mu.RUnlock() if !exists || item.isExpired() { return 0, ErrCacheMiss } if item.expiresAt.IsZero() { return 0, nil // No expiration } remaining := time.Until(item.expiresAt) if remaining < 0 { return 0, nil } return remaining, nil } // Expire updates the TTL for an existing key func (m *MemoryCacheBackend) Expire(ctx context.Context, key string, ttl time.Duration) error { if m.closed.Load() { return ErrBackendUnavailable } m.mu.Lock() defer m.mu.Unlock() item, exists := m.items[key] if !exists || item.isExpired() { return ErrCacheMiss } if ttl > 0 { item.expiresAt = time.Now().Add(ttl) } else { item.expiresAt = time.Time{} // Remove expiration } return nil } // GetStats returns statistics about the cache backend func (m *MemoryCacheBackend) GetStats(ctx context.Context) (*BackendStats, error) { if m.closed.Load() { return nil, ErrBackendUnavailable } m.mu.RLock() lastError := m.lastError lastErrorTime := m.lastErrorTime m.mu.RUnlock() avgGetLatency := time.Duration(0) if getCount := m.getCount.Load(); getCount > 0 { avgGetLatency = time.Duration(m.totalGetTime.Load() / getCount) } avgSetLatency := time.Duration(0) if setCount := m.setCount.Load(); setCount > 0 { avgSetLatency = time.Duration(m.totalSetTime.Load() / setCount) } return &BackendStats{ Type: TypeMemory, Hits: m.hits.Load(), Misses: m.misses.Load(), Sets: m.sets.Load(), Deletes: m.deletes.Load(), Errors: m.errors.Load(), Evictions: m.evictions.Load(), CurrentSize: m.currentSize, MaxSize: m.maxSize, MemoryUsage: m.currentMemory, AverageGetLatency: avgGetLatency, AverageSetLatency: avgSetLatency, LastError: lastError, LastErrorTime: lastErrorTime, Uptime: time.Since(m.startTime), StartTime: m.startTime, }, nil } // Ping checks if the backend is healthy func (m *MemoryCacheBackend) Ping(ctx context.Context) error { if m.closed.Load() { return ErrBackendUnavailable } return nil } // Close closes the backend and releases resources func (m *MemoryCacheBackend) Close() error { if m.closed.Swap(true) { return nil // Already closed } m.cleanupTicker.Stop() close(m.cleanupDone) m.mu.Lock() m.items = nil m.lruList = nil m.mu.Unlock() return nil } // IsHealthy returns true if the backend is healthy func (m *MemoryCacheBackend) IsHealthy() bool { return !m.closed.Load() } // Type returns the backend type func (m *MemoryCacheBackend) Type() BackendType { return TypeMemory } // Capabilities returns the backend capabilities func (m *MemoryCacheBackend) Capabilities() *BackendCapabilities { return &BackendCapabilities{ Distributed: false, Persistent: false, Eviction: true, TTL: true, MaxKeySize: 1024, // 1KB MaxValueSize: 10485760, // 10MB MaxKeys: m.maxSize, SupportsExpire: true, SupportsMultiGet: true, SupportsTransaction: false, SupportsCompression: false, RequiresSerialize: false, } } // Helper functions // estimateValueSize estimates the size of a value in bytes func estimateValueSize(value interface{}) int64 { // This is a simplified estimation // In production, you might want to use a more accurate method switch v := value.(type) { case string: return int64(len(v)) case []byte: return int64(len(v)) case int, int32, int64, uint, uint32, uint64: return 8 case float32, float64: return 8 case bool: return 1 default: // For complex types, use a default estimate return 256 } } // matchPattern checks if a key matches a pattern (simplified glob matching) func matchPattern(pattern, key string) bool { if pattern == "*" { return true } // Simplified pattern matching - in production, use a proper glob library return key == pattern || (len(pattern) > 0 && pattern[0] == '*' && len(key) >= len(pattern)-1 && key[len(key)-len(pattern)+1:] == pattern[1:]) }