// Package cache provides multi-tier caching implementation with local LRU and Redis package cache import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "sync" "time" "github.com/redis/go-redis/v9" ) // MultiTierCache implements a two-tier caching system with local LRU and Redis type MultiTierCache struct { local *LRUCache redis *redis.Client ttl time.Duration metrics *CacheMetrics } // CacheMetrics tracks cache performance type CacheMetrics struct { mu sync.RWMutex LocalHits int64 LocalMisses int64 RedisHits int64 RedisMisses int64 } // Config for cache initialization type Config struct { RedisURL string LocalSize int TTL time.Duration MaxRetries int PoolSize int } // New creates a new multi-tier cache func New(cfg Config) (*MultiTierCache, error) { // Parse Redis URL opt, err := redis.ParseURL(cfg.RedisURL) if err != nil { return nil, err } opt.MaxRetries = cfg.MaxRetries opt.PoolSize = cfg.PoolSize redisClient := redis.NewClient(opt) // Test Redis connection ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := redisClient.Ping(ctx).Err(); err != nil { // Redis not available, continue with local cache only redisClient = nil } return &MultiTierCache{ local: NewLRUCache(cfg.LocalSize), redis: redisClient, ttl: cfg.TTL, metrics: &CacheMetrics{}, }, nil } // Get retrieves a value from cache, checking local first then Redis func (c *MultiTierCache) Get(ctx context.Context, key string) ([]byte, error) { hashedKey := c.hashKey(key) // Check local cache first (sub-millisecond) if value, found := c.local.Get(hashedKey); found { c.metrics.mu.Lock() c.metrics.LocalHits++ c.metrics.mu.Unlock() return value, nil } c.metrics.mu.Lock() c.metrics.LocalMisses++ c.metrics.mu.Unlock() // Check Redis if available (1-5ms) if c.redis != nil { value, err := c.redis.Get(ctx, hashedKey).Bytes() if err == nil { c.metrics.mu.Lock() c.metrics.RedisHits++ c.metrics.mu.Unlock() // Backfill local cache c.local.Set(hashedKey, value) return value, nil } if err != redis.Nil { // Log error but continue } c.metrics.mu.Lock() c.metrics.RedisMisses++ c.metrics.mu.Unlock() } return nil, ErrCacheMiss } // Set stores a value in both cache tiers func (c *MultiTierCache) Set(ctx context.Context, key string, value []byte, ttlSeconds int) error { hashedKey := c.hashKey(key) ttl := time.Duration(ttlSeconds) * time.Second if ttlSeconds == 0 { ttl = c.ttl } // Set in local cache c.local.Set(hashedKey, value) // Set in Redis if available if c.redis != nil { if err := c.redis.Set(ctx, hashedKey, value, ttl).Err(); err != nil { // Log error but don't fail - local cache is still valid return nil } } return nil } // Delete removes a value from both cache tiers func (c *MultiTierCache) Delete(ctx context.Context, key string) error { hashedKey := c.hashKey(key) // Delete from local cache c.local.Delete(hashedKey) // Delete from Redis if available if c.redis != nil { if err := c.redis.Del(ctx, hashedKey).Err(); err != nil { return err } } return nil } // GetJSON retrieves and unmarshals a JSON value from cache func (c *MultiTierCache) GetJSON(ctx context.Context, key string, v interface{}) error { data, err := c.Get(ctx, key) if err != nil { return err } return json.Unmarshal(data, v) } // SetJSON marshals and stores a JSON value in cache func (c *MultiTierCache) SetJSON(ctx context.Context, key string, v interface{}, ttlSeconds int) error { data, err := json.Marshal(v) if err != nil { return err } return c.Set(ctx, key, data, ttlSeconds) } // GetMetrics returns cache performance metrics func (c *MultiTierCache) GetMetrics() CacheMetrics { c.metrics.mu.RLock() defer c.metrics.mu.RUnlock() return CacheMetrics{ LocalHits: c.metrics.LocalHits, LocalMisses: c.metrics.LocalMisses, RedisHits: c.metrics.RedisHits, RedisMisses: c.metrics.RedisMisses, } } // Close closes the cache connections func (c *MultiTierCache) Close() error { if c.redis != nil { return c.redis.Close() } return nil } func (c *MultiTierCache) hashKey(key string) string { hash := sha256.Sum256([]byte(key)) return "amani:" + hex.EncodeToString(hash[:16]) } // ErrCacheMiss indicates the key was not found in cache var ErrCacheMiss = &CacheMissError{} // CacheMissError represents a cache miss type CacheMissError struct{} func (e *CacheMissError) Error() string { return "cache miss" } // LRUCache implements a simple LRU cache type LRUCache struct { mu sync.RWMutex capacity int items map[string]*lruItem head *lruItem tail *lruItem } type lruItem struct { key string value []byte prev *lruItem next *lruItem } // NewLRUCache creates a new LRU cache with the given capacity func NewLRUCache(capacity int) *LRUCache { return &LRUCache{ capacity: capacity, items: make(map[string]*lruItem), } } // Get retrieves a value from the LRU cache func (c *LRUCache) Get(key string) ([]byte, bool) { c.mu.Lock() defer c.mu.Unlock() item, found := c.items[key] if !found { return nil, false } // Move to front (most recently used) c.moveToFront(item) return item.value, true } // Set stores a value in the LRU cache func (c *LRUCache) Set(key string, value []byte) { c.mu.Lock() defer c.mu.Unlock() // Check if item exists if item, found := c.items[key]; found { item.value = value c.moveToFront(item) return } // Create new item item := &lruItem{key: key, value: value} c.items[key] = item c.addToFront(item) // Evict if over capacity if len(c.items) > c.capacity { c.evictLRU() } } // Delete removes a value from the LRU cache func (c *LRUCache) Delete(key string) { c.mu.Lock() defer c.mu.Unlock() if item, found := c.items[key]; found { c.removeItem(item) delete(c.items, key) } } func (c *LRUCache) moveToFront(item *lruItem) { if item == c.head { return } c.removeItem(item) c.addToFront(item) } func (c *LRUCache) addToFront(item *lruItem) { item.prev = nil item.next = c.head if c.head != nil { c.head.prev = item } c.head = item if c.tail == nil { c.tail = item } } func (c *LRUCache) removeItem(item *lruItem) { if item.prev != nil { item.prev.next = item.next } else { c.head = item.next } if item.next != nil { item.next.prev = item.prev } else { c.tail = item.prev } } func (c *LRUCache) evictLRU() { if c.tail == nil { return } delete(c.items, c.tail.key) c.removeItem(c.tail) }