feat: Add in-memory cache with TTL, LRU eviction, JSON serialization, and statistics tracking

- Add Cache struct with entries map, mutex, maxSize, defaultTTL, strategy, and stats fields
- Add CacheStrategy type with LRU, TTL, write-through, write-back, and read-through constants
- Add CacheEntry struct with Key, Value, ExpiresAt, CreatedAt, and HitCount fields
- Add NewCache constructor with CacheConfig parameter and automatic cleanup goroutine
- Add Get, Set, Delete, and Clear methods with hit
This commit is contained in:
nutcas3
2026-05-03 10:41:33 +03:00
parent d9c2026cb9
commit 625bfb5671
@@ -0,0 +1,215 @@
package infra
import (
"context"
"encoding/json"
"fmt"
"sync"
"time"
)
// CacheStrategy defines caching strategies
type CacheStrategy string
const (
CacheStrategyLRU CacheStrategy = "lru"
CacheStrategyTTL CacheStrategy = "ttl"
CacheStrategyWriteThru CacheStrategy = "write_through"
CacheStrategyWriteBack CacheStrategy = "write_back"
CacheStrategyReadThru CacheStrategy = "read_through"
)
// CacheEntry represents a cached item
type CacheEntry struct {
Key string
Value []byte
ExpiresAt time.Time
CreatedAt time.Time
HitCount int64
}
// Cache provides in-memory caching with multiple strategies
type Cache struct {
entries map[string]*CacheEntry
mu sync.RWMutex
maxSize int
defaultTTL time.Duration
strategy CacheStrategy
stats CacheStats
}
// CacheConfig configures the cache
type CacheConfig struct {
MaxSize int
DefaultTTL time.Duration
Strategy CacheStrategy
}
// DefaultCacheConfig returns default cache configuration
func DefaultCacheConfig() CacheConfig {
return CacheConfig{
MaxSize: 10000,
DefaultTTL: 5 * time.Minute,
Strategy: CacheStrategyTTL,
}
}
// NewCache creates a new cache instance
func NewCache(config CacheConfig) *Cache {
c := &Cache{
entries: make(map[string]*CacheEntry),
maxSize: config.MaxSize,
defaultTTL: config.DefaultTTL,
strategy: config.Strategy,
}
go c.cleanupLoop()
return c
}
// Get retrieves a value from cache
func (c *Cache) Get(ctx context.Context, key string) ([]byte, bool) {
c.mu.RLock()
entry, exists := c.entries[key]
c.mu.RUnlock()
if !exists {
c.stats.Misses++
return nil, false
}
if time.Now().After(entry.ExpiresAt) {
c.Delete(ctx, key)
c.stats.Misses++
return nil, false
}
c.mu.Lock()
entry.HitCount++
c.stats.Hits++
c.mu.Unlock()
return entry.Value, true
}
// Set stores a value in cache
func (c *Cache) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error {
if ttl == 0 {
ttl = c.defaultTTL
}
c.mu.Lock()
defer c.mu.Unlock()
if len(c.entries) >= c.maxSize {
c.evict()
}
c.entries[key] = &CacheEntry{
Key: key,
Value: value,
ExpiresAt: time.Now().Add(ttl),
CreatedAt: time.Now(),
}
c.stats.Sets++
return nil
}
// SetJSON stores a JSON-serializable value
func (c *Cache) SetJSON(ctx context.Context, key string, value any, ttl time.Duration) error {
data, err := json.Marshal(value)
if err != nil {
return fmt.Errorf("failed to marshal value: %w", err)
}
return c.Set(ctx, key, data, ttl)
}
// GetJSON retrieves and unmarshals a JSON value
func (c *Cache) GetJSON(ctx context.Context, key string, dest any) (bool, error) {
data, exists := c.Get(ctx, key)
if !exists {
return false, nil
}
if err := json.Unmarshal(data, dest); err != nil {
return false, fmt.Errorf("failed to unmarshal value: %w", err)
}
return true, nil
}
// Delete removes a value from cache
func (c *Cache) Delete(_ context.Context, key string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.entries, key)
c.stats.Deletes++
}
// Clear removes all entries
func (c *Cache) Clear(_ context.Context) {
c.mu.Lock()
defer c.mu.Unlock()
c.entries = make(map[string]*CacheEntry)
}
// evict removes the least recently used or oldest entry
func (c *Cache) evict() {
var oldest *CacheEntry
var oldestKey string
for key, entry := range c.entries {
if oldest == nil || entry.CreatedAt.Before(oldest.CreatedAt) {
oldest = entry
oldestKey = key
}
}
if oldestKey != "" {
delete(c.entries, oldestKey)
c.stats.Evictions++
}
}
func (c *Cache) cleanupLoop() {
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
for range ticker.C {
c.cleanup()
}
}
func (c *Cache) cleanup() {
c.mu.Lock()
defer c.mu.Unlock()
now := time.Now()
for key, entry := range c.entries {
if now.After(entry.ExpiresAt) {
delete(c.entries, key)
c.stats.Evictions++
}
}
}
// Stats returns cache statistics
func (c *Cache) Stats() CacheStats {
c.mu.RLock()
defer c.mu.RUnlock()
stats := c.stats
stats.Size = len(c.entries)
if stats.Hits+stats.Misses > 0 {
stats.HitRate = float64(stats.Hits) / float64(stats.Hits+stats.Misses) * 100
}
return stats
}
// CacheStats contains cache statistics
type CacheStats struct {
Size int `json:"size"`
Hits int64 `json:"hits"`
Misses int64 `json:"misses"`
Sets int64 `json:"sets"`
Deletes int64 `json:"deletes"`
Evictions int64 `json:"evictions"`
HitRate float64 `json:"hit_rate_pct"`
}