package data import ( "context" "strconv" "sync" "time" "kra/internal/biz" "github.com/redis/go-redis/v9" ) type memoryCacheEntry struct { value string expiresAt time.Time } type cacheStore struct { data *Data mu sync.Mutex memory map[string]memoryCacheEntry } const maxMemoryCacheEntries = 10000 func NewCache(data *Data) biz.Cache { return &cacheStore{data: data, memory: make(map[string]memoryCacheEntry)} } func (s *cacheStore) client() redis.UniversalClient { return s.data.redis.load() } func (s *cacheStore) Get(ctx context.Context, key string) (string, bool, error) { if client := s.client(); client != nil { value, err := client.Get(ctx, key).Result() if err == nil { return value, true, nil } if err != redis.Nil { return "", false, err } return "", false, nil } s.mu.Lock() defer s.mu.Unlock() entry, ok := s.memory[key] if !ok { return "", false, nil } if !entry.expiresAt.IsZero() && time.Now().After(entry.expiresAt) { delete(s.memory, key) return "", false, nil } return entry.value, true, nil } func (s *cacheStore) Set(ctx context.Context, key, value string, expiration time.Duration) error { if client := s.client(); client != nil { return client.Set(ctx, key, value, expiration).Err() } s.mu.Lock() defer s.mu.Unlock() s.makeMemoryRoom(key) entry := memoryCacheEntry{value: value} if expiration > 0 { entry.expiresAt = time.Now().Add(expiration) } s.memory[key] = entry return nil } func (s *cacheStore) Delete(ctx context.Context, key string) error { if client := s.client(); client != nil { return client.Del(ctx, key).Err() } s.mu.Lock() delete(s.memory, key) s.mu.Unlock() return nil } func (s *cacheStore) Increment(ctx context.Context, key string, expiration time.Duration) (int64, error) { if client := s.client(); client != nil { value, err := client.Incr(ctx, key).Result() if err != nil { return 0, err } if value == 1 && expiration > 0 { if err := client.Expire(ctx, key, expiration).Err(); err != nil { return 0, err } } return value, nil } s.mu.Lock() defer s.mu.Unlock() s.makeMemoryRoom(key) entry, ok := s.memory[key] if ok && !entry.expiresAt.IsZero() && time.Now().After(entry.expiresAt) { ok = false } value := int64(0) if ok { value, _ = strconv.ParseInt(entry.value, 10, 64) } value++ entry.value = strconv.FormatInt(value, 10) if !ok && expiration > 0 { entry.expiresAt = time.Now().Add(expiration) } s.memory[key] = entry return value, nil } func (s *cacheStore) makeMemoryRoom(incoming string) { if len(s.memory) < maxMemoryCacheEntries { return } now := time.Now() for key, entry := range s.memory { if !entry.expiresAt.IsZero() && now.After(entry.expiresAt) { delete(s.memory, key) } } if len(s.memory) < maxMemoryCacheEntries { return } if _, exists := s.memory[incoming]; exists { return } // The in-memory store is only a Redis fallback. A bounded arbitrary eviction // is preferable to unbounded growth under IP churn. for key := range s.memory { delete(s.memory, key) break } }