mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +00:00
e64fc7f730
* Add redis support for distributed caching * Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * ... and another all nighter. * fixup! ... and another all nighter. * fixup! fixup! ... and another all nighter. * fixup! fixup! fixup! ... and another all nighter. * Resolve issue #85 by adding ability to set custom claims in JWT tokens * Remove redundant validation in auth middleware ( issue #89 ) * Add ability to set cookie prefix for session cookies ( #87 ) * fixup! Add ability to set cookie prefix for session cookies ( #87 ) * Add ability to set cookie max age - issue #91 * Potential fix for code scanning alert no. 10: Size computation for allocation may overflow Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fixup! Merge main into 0.8.0-redis: resolve conflicts --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
517 lines
11 KiB
Go
517 lines
11 KiB
Go
// 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:])
|
|
}
|