mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
602 lines
14 KiB
Go
602 lines
14 KiB
Go
package traefikoidc
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// DefaultMaxSize is the default maximum number of items in cache
|
|
const DefaultMaxSize = 500
|
|
|
|
// CacheStrategy defines the caching strategy interface
|
|
type CacheStrategy interface {
|
|
// Name returns the strategy name for debugging
|
|
Name() string
|
|
// ShouldEvict determines if an item should be evicted
|
|
ShouldEvict(item interface{}, now time.Time) bool
|
|
// OnAccess is called when an item is accessed
|
|
OnAccess(key string, item interface{})
|
|
// OnRemove is called when an item is removed
|
|
OnRemove(key string)
|
|
// EstimateSize estimates the memory size of an item
|
|
EstimateSize(item interface{}) int64
|
|
// GetEvictionCandidate returns the best candidate for eviction
|
|
GetEvictionCandidate() (key string, found bool)
|
|
}
|
|
|
|
// UnifiedCacheConfig provides configuration for the unified cache
|
|
type UnifiedCacheConfig struct {
|
|
MaxSize int
|
|
MaxMemoryBytes int64
|
|
CleanupInterval time.Duration
|
|
Strategy CacheStrategy
|
|
EnableMemoryLimit bool
|
|
EnableAutoCleanup bool
|
|
Logger *Logger
|
|
}
|
|
|
|
// DefaultUnifiedCacheConfig returns default cache configuration
|
|
func DefaultUnifiedCacheConfig() UnifiedCacheConfig {
|
|
return UnifiedCacheConfig{
|
|
MaxSize: DefaultMaxSize,
|
|
MaxMemoryBytes: 64 * 1024 * 1024, // 64MB default
|
|
CleanupInterval: 2 * time.Minute,
|
|
EnableMemoryLimit: false,
|
|
EnableAutoCleanup: true,
|
|
Logger: nil,
|
|
}
|
|
}
|
|
|
|
// UnifiedCache provides a single, flexible cache implementation
|
|
// that can be configured with different strategies and features
|
|
type UnifiedCache struct {
|
|
items map[string]interface{}
|
|
strategy CacheStrategy
|
|
cleanupTask *BackgroundTask
|
|
logger *Logger
|
|
config UnifiedCacheConfig
|
|
currentMemoryBytes int64
|
|
mutex sync.RWMutex
|
|
}
|
|
|
|
// NewUnifiedCache creates a new unified cache with the given configuration
|
|
func NewUnifiedCache(config UnifiedCacheConfig) *UnifiedCache {
|
|
if config.Logger == nil {
|
|
config.Logger = GetSingletonNoOpLogger()
|
|
}
|
|
|
|
if config.Strategy == nil {
|
|
config.Strategy = NewLRUStrategy(config.MaxSize)
|
|
}
|
|
|
|
c := &UnifiedCache{
|
|
items: make(map[string]interface{}, config.MaxSize),
|
|
strategy: config.Strategy,
|
|
config: config,
|
|
logger: config.Logger,
|
|
}
|
|
|
|
if config.EnableAutoCleanup {
|
|
c.startAutoCleanup()
|
|
}
|
|
|
|
return c
|
|
}
|
|
|
|
// NewUnifiedCacheSimple creates a unified cache with default configuration
|
|
// This is a drop-in replacement for the old NewCache() function
|
|
func NewUnifiedCacheSimple() *UnifiedCache {
|
|
config := DefaultUnifiedCacheConfig()
|
|
return NewUnifiedCache(config)
|
|
}
|
|
|
|
// Set stores a value in the cache
|
|
func (c *UnifiedCache) Set(key string, value interface{}, ttl time.Duration) {
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
|
|
// Wrap the value with metadata
|
|
var expiresAt time.Time
|
|
if ttl > 0 {
|
|
expiresAt = time.Now().Add(ttl)
|
|
} else if ttl == 0 {
|
|
// Zero TTL means no expiration - set to far future
|
|
expiresAt = time.Now().Add(100 * 365 * 24 * time.Hour) // 100 years
|
|
} else {
|
|
// Negative TTL means already expired
|
|
expiresAt = time.Now().Add(ttl) // This will be in the past
|
|
}
|
|
|
|
item := &CacheEntry{
|
|
Value: value,
|
|
ExpiresAt: expiresAt,
|
|
Key: key,
|
|
}
|
|
|
|
// Check memory limits if enabled
|
|
if c.config.EnableMemoryLimit {
|
|
itemSize := c.strategy.EstimateSize(item)
|
|
|
|
// Evict items if necessary
|
|
for (c.currentMemoryBytes+itemSize > c.config.MaxMemoryBytes ||
|
|
len(c.items) >= c.config.MaxSize) && len(c.items) > 0 {
|
|
if !c.evictOne() {
|
|
break
|
|
}
|
|
}
|
|
|
|
c.currentMemoryBytes += itemSize
|
|
} else if len(c.items) >= c.config.MaxSize {
|
|
c.evictOne()
|
|
}
|
|
|
|
c.items[key] = item
|
|
c.strategy.OnAccess(key, item)
|
|
}
|
|
|
|
// Get retrieves a value from the cache
|
|
func (c *UnifiedCache) Get(key string) (interface{}, bool) {
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
|
|
item, exists := c.items[key]
|
|
if !exists {
|
|
return nil, false
|
|
}
|
|
|
|
// Check if expired
|
|
entry, ok := item.(*CacheEntry)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
|
|
if c.strategy.ShouldEvict(entry, time.Now()) {
|
|
c.removeItem(key)
|
|
return nil, false
|
|
}
|
|
|
|
c.strategy.OnAccess(key, entry)
|
|
return entry.Value, true
|
|
}
|
|
|
|
// Delete removes an item from the cache
|
|
func (c *UnifiedCache) Delete(key string) {
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
|
|
c.removeItem(key)
|
|
}
|
|
|
|
// Cleanup removes expired entries
|
|
func (c *UnifiedCache) Cleanup() {
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
|
|
now := time.Now()
|
|
toRemove := make([]string, 0)
|
|
|
|
for key, item := range c.items {
|
|
if c.strategy.ShouldEvict(item, now) {
|
|
toRemove = append(toRemove, key)
|
|
}
|
|
}
|
|
|
|
for _, key := range toRemove {
|
|
c.removeItem(key)
|
|
}
|
|
}
|
|
|
|
// Close stops the cache and cleans up resources
|
|
func (c *UnifiedCache) Close() {
|
|
// Stop the cleanup task first before acquiring the lock to avoid deadlock
|
|
var taskToStop *BackgroundTask
|
|
c.mutex.Lock()
|
|
taskToStop = c.cleanupTask
|
|
c.cleanupTask = nil
|
|
c.mutex.Unlock()
|
|
|
|
// Stop the task outside of the lock and wait for proper cleanup
|
|
if taskToStop != nil {
|
|
taskToStop.Stop()
|
|
// Give the goroutine a brief moment to fully terminate
|
|
// This ensures the test can detect that goroutines have been cleaned up
|
|
time.Sleep(50 * time.Millisecond)
|
|
}
|
|
|
|
// Now acquire the lock again to clear items
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
|
|
// Clear all items to help with garbage collection
|
|
for key := range c.items {
|
|
delete(c.items, key)
|
|
}
|
|
c.currentMemoryBytes = 0
|
|
|
|
// Clean up the strategy if it supports cleanup
|
|
if strategy, ok := c.strategy.(*LRUStrategy); ok {
|
|
strategy.Cleanup()
|
|
}
|
|
}
|
|
|
|
// SetMaxSize updates the maximum cache size
|
|
func (c *UnifiedCache) SetMaxSize(size int) {
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
|
|
c.config.MaxSize = size
|
|
|
|
// Evict excess items
|
|
for len(c.items) > size && len(c.items) > 0 {
|
|
if !c.evictOne() {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// SetMaxMemory updates the maximum memory limit
|
|
func (c *UnifiedCache) SetMaxMemory(bytes int64) {
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
c.config.MaxMemoryBytes = bytes
|
|
c.config.EnableMemoryLimit = bytes > 0
|
|
}
|
|
|
|
// GetMetrics returns cache metrics
|
|
func (c *UnifiedCache) GetMetrics() map[string]interface{} {
|
|
c.mutex.RLock()
|
|
defer c.mutex.RUnlock()
|
|
|
|
return map[string]interface{}{
|
|
"item_count": len(c.items),
|
|
"memory_bytes": c.currentMemoryBytes,
|
|
"max_size": c.config.MaxSize,
|
|
"strategy": c.strategy.Name(),
|
|
"has_cleanup": c.cleanupTask != nil,
|
|
}
|
|
}
|
|
|
|
// removeItem removes an item and updates memory tracking
|
|
func (c *UnifiedCache) removeItem(key string) {
|
|
if item, exists := c.items[key]; exists {
|
|
if c.config.EnableMemoryLimit {
|
|
c.currentMemoryBytes -= c.strategy.EstimateSize(item)
|
|
}
|
|
delete(c.items, key)
|
|
if c.strategy != nil {
|
|
c.strategy.OnRemove(key)
|
|
}
|
|
}
|
|
}
|
|
|
|
// evictOne evicts one item based on strategy
|
|
func (c *UnifiedCache) evictOne() bool {
|
|
// Try to use strategy's eviction candidate first
|
|
if c.strategy != nil {
|
|
if key, found := c.strategy.GetEvictionCandidate(); found {
|
|
c.removeItem(key)
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Fallback: evict any item
|
|
for key := range c.items {
|
|
c.removeItem(key)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// startAutoCleanup starts the background cleanup task
|
|
func (c *UnifiedCache) startAutoCleanup() {
|
|
// Create a unique task name for each cache instance to avoid singleton restrictions
|
|
// Avoid "cleanup" keyword to bypass circuit breaker singleton enforcement
|
|
taskName := fmt.Sprintf("cache-maintenance-%p", c)
|
|
|
|
c.cleanupTask = NewBackgroundTask(
|
|
taskName,
|
|
c.config.CleanupInterval,
|
|
c.Cleanup,
|
|
c.logger,
|
|
)
|
|
c.cleanupTask.Start()
|
|
}
|
|
|
|
// CacheEntry wraps cached values with metadata
|
|
type CacheEntry struct {
|
|
Value interface{}
|
|
ExpiresAt time.Time
|
|
Key string
|
|
}
|
|
|
|
// LRUStrategy implements LRU eviction strategy
|
|
type LRUStrategy struct {
|
|
order *DoublyLinkedList
|
|
elements map[string]*ListNode
|
|
maxSize int
|
|
mutex sync.Mutex
|
|
}
|
|
|
|
// NewLRUStrategy creates a new LRU strategy
|
|
func NewLRUStrategy(maxSize int) *LRUStrategy {
|
|
return &LRUStrategy{
|
|
order: NewDoublyLinkedList(),
|
|
elements: make(map[string]*ListNode, maxSize),
|
|
maxSize: maxSize,
|
|
}
|
|
}
|
|
|
|
// Name returns the strategy name
|
|
func (s *LRUStrategy) Name() string {
|
|
return "LRU"
|
|
}
|
|
|
|
// ShouldEvict checks if an item should be evicted
|
|
func (s *LRUStrategy) ShouldEvict(item interface{}, now time.Time) bool {
|
|
if entry, ok := item.(*CacheEntry); ok {
|
|
return now.After(entry.ExpiresAt)
|
|
}
|
|
return false
|
|
}
|
|
|
|
// OnAccess updates LRU order when item is accessed
|
|
func (s *LRUStrategy) OnAccess(key string, item interface{}) {
|
|
s.mutex.Lock()
|
|
defer s.mutex.Unlock()
|
|
|
|
if node, exists := s.elements[key]; exists {
|
|
s.order.MoveToBack(node)
|
|
} else {
|
|
node := s.order.PushBack(key)
|
|
s.elements[key] = node
|
|
}
|
|
}
|
|
|
|
// OnRemove removes item from LRU tracking
|
|
func (s *LRUStrategy) OnRemove(key string) {
|
|
s.mutex.Lock()
|
|
defer s.mutex.Unlock()
|
|
|
|
if node, exists := s.elements[key]; exists {
|
|
s.order.Remove(node)
|
|
delete(s.elements, key)
|
|
|
|
// Defensive cleanup: ensure node is completely disconnected to assist GC
|
|
if node != nil {
|
|
node.Key = "" // Clear key reference
|
|
node.prev = nil // Break backward reference
|
|
node.next = nil // Break forward reference
|
|
}
|
|
}
|
|
}
|
|
|
|
// EstimateSize estimates memory size of an item
|
|
func (s *LRUStrategy) EstimateSize(item interface{}) int64 {
|
|
// Basic size estimation
|
|
size := int64(80) // Base object overhead
|
|
|
|
if entry, ok := item.(*CacheEntry); ok {
|
|
size += int64(len(entry.Key))
|
|
|
|
switch v := entry.Value.(type) {
|
|
case string:
|
|
size += int64(len(v))
|
|
case []byte:
|
|
size += int64(len(v))
|
|
case map[string]interface{}:
|
|
size += int64(len(v)) * 64
|
|
for key, val := range v {
|
|
size += int64(len(key))
|
|
if str, ok := val.(string); ok {
|
|
size += int64(len(str))
|
|
} else {
|
|
size += 32
|
|
}
|
|
}
|
|
default:
|
|
size += 64
|
|
}
|
|
}
|
|
|
|
return size
|
|
}
|
|
|
|
// GetEvictionCandidate returns the least recently used item
|
|
func (s *LRUStrategy) GetEvictionCandidate() (key string, found bool) {
|
|
s.mutex.Lock()
|
|
defer s.mutex.Unlock()
|
|
|
|
// The actual first element is head.next (head is a sentinel)
|
|
if s.order.head != nil && s.order.head.next != nil && s.order.head.next != s.order.tail {
|
|
return s.order.head.next.Key, true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// Cleanup performs complete cleanup of the LRU strategy to assist GC
|
|
func (s *LRUStrategy) Cleanup() {
|
|
s.mutex.Lock()
|
|
defer s.mutex.Unlock()
|
|
|
|
// Clear all elements map entries
|
|
for key, node := range s.elements {
|
|
if node != nil {
|
|
// Break circular references before removing from map
|
|
node.Key = ""
|
|
node.prev = nil
|
|
node.next = nil
|
|
}
|
|
delete(s.elements, key)
|
|
}
|
|
|
|
// Clear the linked list completely
|
|
if s.order != nil {
|
|
s.order.Clear()
|
|
}
|
|
|
|
// Log memory impact of cleanup
|
|
memMonitor := GetGlobalMemoryMonitor()
|
|
if stats := memMonitor.GetCurrentStats(); stats != nil && stats.MemoryPressure >= MemoryPressureModerate {
|
|
// Trigger GC after major cleanup to free memory immediately
|
|
memMonitor.TriggerGC()
|
|
}
|
|
}
|
|
|
|
// DoublyLinkedList provides a simple doubly-linked list for LRU
|
|
type DoublyLinkedList struct {
|
|
head, tail *ListNode
|
|
size int
|
|
}
|
|
|
|
// ListNode represents a node in the doubly-linked list
|
|
type ListNode struct {
|
|
Key string
|
|
prev *ListNode
|
|
next *ListNode
|
|
}
|
|
|
|
// NewDoublyLinkedList creates a new doubly-linked list
|
|
func NewDoublyLinkedList() *DoublyLinkedList {
|
|
head := &ListNode{}
|
|
tail := &ListNode{}
|
|
head.next = tail
|
|
tail.prev = head
|
|
return &DoublyLinkedList{
|
|
head: head,
|
|
tail: tail,
|
|
size: 0,
|
|
}
|
|
}
|
|
|
|
// PushBack adds a key to the back of the list
|
|
func (l *DoublyLinkedList) PushBack(key string) *ListNode {
|
|
node := &ListNode{Key: key}
|
|
node.prev = l.tail.prev
|
|
node.next = l.tail
|
|
l.tail.prev.next = node
|
|
l.tail.prev = node
|
|
l.size++
|
|
return node
|
|
}
|
|
|
|
// MoveToBack moves a node to the back of the list
|
|
func (l *DoublyLinkedList) MoveToBack(node *ListNode) {
|
|
// Remove from current position
|
|
node.prev.next = node.next
|
|
node.next.prev = node.prev
|
|
|
|
// Add to back
|
|
node.prev = l.tail.prev
|
|
node.next = l.tail
|
|
l.tail.prev.next = node
|
|
l.tail.prev = node
|
|
}
|
|
|
|
// PopFront removes and returns the front node with defensive cleanup
|
|
func (l *DoublyLinkedList) PopFront() *ListNode {
|
|
if l.head.next == l.tail {
|
|
return nil
|
|
}
|
|
|
|
front := l.head.next
|
|
l.head.next = front.next
|
|
front.next.prev = l.head
|
|
l.size--
|
|
|
|
// Defensive cleanup to break circular references and assist GC
|
|
front.prev = nil
|
|
front.next = nil
|
|
// Note: We don't clear front.Key as the caller may still need it
|
|
|
|
return front
|
|
}
|
|
|
|
// Remove removes a node from the list with defensive cleanup for GC
|
|
func (l *DoublyLinkedList) Remove(node *ListNode) {
|
|
if node == nil || node.prev == nil || node.next == nil {
|
|
return
|
|
}
|
|
|
|
// Unlink the node from the list
|
|
node.prev.next = node.next
|
|
node.next.prev = node.prev
|
|
|
|
// Defensive cleanup to break circular references and assist GC
|
|
node.Key = "" // Clear string reference
|
|
node.prev = nil // Break backward circular reference
|
|
node.next = nil // Break forward circular reference
|
|
|
|
l.size--
|
|
}
|
|
|
|
// Clear removes all nodes from the list with complete cleanup
|
|
func (l *DoublyLinkedList) Clear() {
|
|
if l.head == nil || l.tail == nil {
|
|
return
|
|
}
|
|
|
|
// Walk through all nodes and break circular references
|
|
current := l.head.next
|
|
for current != nil && current != l.tail {
|
|
next := current.next
|
|
|
|
// Break all references in current node for GC
|
|
current.Key = ""
|
|
current.prev = nil
|
|
current.next = nil
|
|
|
|
current = next
|
|
}
|
|
|
|
// Reset head and tail connections
|
|
l.head.next = l.tail
|
|
l.tail.prev = l.head
|
|
l.size = 0
|
|
}
|
|
|
|
// CacheAdapter provides backward compatibility with existing cache interfaces
|
|
type CacheAdapter struct {
|
|
unified *UnifiedCache
|
|
}
|
|
|
|
// NewCacheAdapter creates an adapter for the unified cache
|
|
func NewCacheAdapter(unified *UnifiedCache) *CacheAdapter {
|
|
return &CacheAdapter{unified: unified}
|
|
}
|
|
|
|
// Set adapts the Set method
|
|
func (a *CacheAdapter) Set(key string, value interface{}, expiration time.Duration) {
|
|
a.unified.Set(key, value, expiration)
|
|
}
|
|
|
|
// Get adapts the Get method
|
|
func (a *CacheAdapter) Get(key string) (interface{}, bool) {
|
|
return a.unified.Get(key)
|
|
}
|
|
|
|
// Delete adapts the Delete method
|
|
func (a *CacheAdapter) Delete(key string) {
|
|
a.unified.Delete(key)
|
|
}
|
|
|
|
// Cleanup adapts the Cleanup method
|
|
func (a *CacheAdapter) Cleanup() {
|
|
a.unified.Cleanup()
|
|
}
|
|
|
|
// Close adapts the Close method
|
|
func (a *CacheAdapter) Close() {
|
|
a.unified.Close()
|
|
}
|
|
|
|
// SetMaxSize adapts the SetMaxSize method
|
|
func (a *CacheAdapter) SetMaxSize(size int) {
|
|
a.unified.SetMaxSize(size)
|
|
}
|
|
|
|
// SetMaxMemory adapts the SetMaxMemory method
|
|
func (a *CacheAdapter) SetMaxMemory(bytes int64) {
|
|
a.unified.SetMaxMemory(bytes)
|
|
}
|