Add redis support for distributed caching (#83)

* 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>
This commit is contained in:
2025-11-30 02:18:46 +00:00
committed by GitHub
co-authored by Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
parent 5fcbd54955
commit e64fc7f730
318 changed files with 100989 additions and 948 deletions
+146
View File
@@ -3,10 +3,13 @@ package traefikoidc
import (
"container/list"
"context"
"encoding/json"
"fmt"
"sync"
"sync/atomic"
"time"
"github.com/lukaszraczylo/traefikoidc/internal/cache/backends"
)
// CacheType defines the type of cache for optimized behavior
@@ -94,6 +97,10 @@ type UniversalCache struct {
config UniversalCacheConfig
logger *Logger
// Backend for distributed caching (NEW)
backend backends.CacheBackend
ownsBackend bool // If true, cache should close backend on Close(); if false, backend is shared
// Memory management
currentSize int64
currentMemory int64
@@ -115,6 +122,14 @@ func NewUniversalCache(config UniversalCacheConfig) *UniversalCache {
return createUniversalCache(config)
}
// NewUniversalCacheWithBackend creates a new universal cache with a specific backend
func NewUniversalCacheWithBackend(config UniversalCacheConfig, cacheBackend backends.CacheBackend) *UniversalCache {
cache := createUniversalCache(config)
cache.backend = cacheBackend
cache.ownsBackend = false // Shared backend, managed externally
return cache
}
// createUniversalCache is the internal constructor
func createUniversalCache(config UniversalCacheConfig) *UniversalCache {
// Apply type-specific defaults first (including MaxSize)
@@ -232,6 +247,25 @@ func (c *UniversalCache) Set(key string, value interface{}, ttl time.Duration) e
ttl = c.config.DefaultTTL
}
// If we have a backend, use it for distributed caching
if c.backend != nil {
// Serialize the value
data, err := c.serialize(value)
if err != nil {
c.logger.Errorf("Failed to serialize value for key %s: %v", key, err)
return err
}
// Store in backend
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
if err := c.backend.Set(ctx, c.prefixKey(key), data, ttl); err != nil {
c.logger.Infof("Backend set error for key %s: %v", key, err)
// Continue with local cache even if backend fails
}
}
size := c.estimateSize(value)
c.mu.Lock()
@@ -294,6 +328,32 @@ func (c *UniversalCache) Set(key string, value interface{}, ttl time.Duration) e
// Get retrieves a value from the cache
func (c *UniversalCache) Get(key string) (interface{}, bool) {
// Try backend first if available (for distributed consistency)
if c.backend != nil {
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel()
data, _, exists, err := c.backend.Get(ctx, c.prefixKey(key))
if err != nil {
c.logger.Debugf("Backend get error for key %s: %v", key, err)
// Fall through to local cache
} else if exists {
// Deserialize the value
var value interface{}
if err := c.deserialize(data, &value); err != nil {
c.logger.Errorf("Failed to deserialize value for key %s: %v", key, err)
// Fall through to local cache
} else {
atomic.AddInt64(&c.hits, 1)
// Update local cache with backend value
go func() {
_ = c.updateLocalCache(key, value, c.config.DefaultTTL)
}()
return value, true
}
}
}
c.mu.Lock()
defer c.mu.Unlock()
@@ -350,6 +410,17 @@ func (c *UniversalCache) Get(key string) (interface{}, bool) {
// Delete removes a key from the cache
func (c *UniversalCache) Delete(key string) bool {
// Delete from backend if available
if c.backend != nil {
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel()
if _, err := c.backend.Delete(ctx, c.prefixKey(key)); err != nil {
c.logger.Debugf("Backend delete error for key %s: %v", key, err)
// Continue with local delete
}
}
c.mu.Lock()
defer c.mu.Unlock()
@@ -364,6 +435,17 @@ func (c *UniversalCache) Delete(key string) bool {
// Clear removes all items from the cache
func (c *UniversalCache) Clear() {
// Clear backend if available
if c.backend != nil {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
if err := c.backend.Clear(ctx); err != nil {
c.logger.Infof("Backend clear error: %v", err)
// Continue with local clear
}
}
c.mu.Lock()
defer c.mu.Unlock()
@@ -446,6 +528,13 @@ func (c *UniversalCache) Close() error {
// Clear all items
c.Clear()
// Close backend only if this cache owns it (not shared)
if c.backend != nil && c.ownsBackend {
if err := c.backend.Close(); err != nil {
c.logger.Infof("Failed to close cache backend: %v", err)
}
}
c.logger.Debugf("UniversalCache[%s]: Closed", c.config.Type)
return nil
}
@@ -710,3 +799,60 @@ func (c *UniversalCache) Mutex() *sync.RWMutex {
func (c *UniversalCache) Strategy() CacheStrategy {
return c.config.Strategy
}
// serialize converts a value to bytes for backend storage
func (c *UniversalCache) serialize(value interface{}) ([]byte, error) {
// Use JSON for serialization - simple and universal
return json.Marshal(value)
}
// deserialize converts bytes from backend storage to a value
func (c *UniversalCache) deserialize(data []byte, value interface{}) error {
// Use JSON for deserialization
return json.Unmarshal(data, value)
}
// prefixKey adds a cache type prefix to the key for backend storage
func (c *UniversalCache) prefixKey(key string) string {
return fmt.Sprintf("%s:%s", c.config.Type, key)
}
// updateLocalCache updates the local cache with a value from the backend
func (c *UniversalCache) updateLocalCache(key string, value interface{}, ttl time.Duration) error {
size := c.estimateSize(value)
c.mu.Lock()
defer c.mu.Unlock()
// Check memory limits
if c.config.MaxMemoryBytes > 0 {
for c.currentMemory+size > c.config.MaxMemoryBytes && c.lruList.Len() > 0 {
c.evictOldest()
}
}
// Check size limits
if c.lruList.Len() >= c.config.MaxSize {
c.evictOldest()
}
now := time.Now()
item := &CacheItem{
Key: key,
Value: value,
Size: size,
ExpiresAt: now.Add(ttl),
LastAccessed: now,
AccessCount: 1,
CacheType: c.config.Type,
Metadata: make(map[string]interface{}),
}
item.element = c.lruList.PushFront(key)
c.items[key] = item
c.currentSize++
c.currentMemory += size
return nil
}