Deployment
Automated deployment update
4b1daed
Raw
History Blame Contribute Delete
6.61 kB
// 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)
}