Spaces:
Build error
Build error
| // 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) | |
| } | |