Files
cv-site/internal/cache/cv_cache.go
T
2025-11-11 13:53:14 +00:00

190 lines
3.6 KiB
Go

package cache
import (
"fmt"
"log"
"sync"
"time"
)
// CacheEntry represents a cached item with expiration
type cacheEntry struct {
data interface{}
expiration time.Time
}
// CVCache provides thread-safe caching for CV and UI data
type CVCache struct {
mu sync.RWMutex
entries map[string]*cacheEntry
ttl time.Duration
stats CacheStats
}
// CacheStats tracks cache performance metrics
type CacheStats struct {
mu sync.RWMutex
Hits int64
Misses int64
Size int
}
// New creates a new CVCache with the specified TTL
func New(ttl time.Duration) *CVCache {
if ttl <= 0 {
ttl = 1 * time.Hour // Default TTL
}
cache := &CVCache{
entries: make(map[string]*cacheEntry),
ttl: ttl,
stats: CacheStats{},
}
// Start background cleanup goroutine
go cache.cleanupExpired()
return cache
}
// Get retrieves an item from the cache
func (c *CVCache) Get(key string) (interface{}, bool) {
c.mu.RLock()
entry, exists := c.entries[key]
c.mu.RUnlock()
if !exists {
c.recordMiss()
return nil, false
}
// Check if expired
if time.Now().After(entry.expiration) {
c.mu.Lock()
delete(c.entries, key)
c.mu.Unlock()
c.recordMiss()
return nil, false
}
c.recordHit()
return entry.data, true
}
// Set stores an item in the cache with TTL
func (c *CVCache) Set(key string, data interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
c.entries[key] = &cacheEntry{
data: data,
expiration: time.Now().Add(c.ttl),
}
c.updateSize()
}
// Invalidate removes a specific key from cache
func (c *CVCache) Invalidate(key string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.entries, key)
c.updateSize()
}
// InvalidateAll clears the entire cache
func (c *CVCache) InvalidateAll() {
c.mu.Lock()
defer c.mu.Unlock()
c.entries = make(map[string]*cacheEntry)
c.updateSize()
log.Println("🗑️ Cache invalidated")
}
// Warm preloads cache with specific keys
func (c *CVCache) Warm(key string, loader func() (interface{}, error)) error {
data, err := loader()
if err != nil {
return fmt.Errorf("cache warm failed for key %s: %w", key, err)
}
c.Set(key, data)
log.Printf("🔥 Cache warmed: %s", key)
return nil
}
// GetStats returns current cache statistics
func (c *CVCache) GetStats() CacheStats {
c.stats.mu.RLock()
defer c.stats.mu.RUnlock()
return CacheStats{
Hits: c.stats.Hits,
Misses: c.stats.Misses,
Size: c.stats.Size,
}
}
// HitRate returns the cache hit rate as a percentage
func (c *CVCache) HitRate() float64 {
stats := c.GetStats()
total := stats.Hits + stats.Misses
if total == 0 {
return 0
}
return (float64(stats.Hits) / float64(total)) * 100
}
// recordHit increments the hit counter
func (c *CVCache) recordHit() {
c.stats.mu.Lock()
c.stats.Hits++
c.stats.mu.Unlock()
}
// recordMiss increments the miss counter
func (c *CVCache) recordMiss() {
c.stats.mu.Lock()
c.stats.Misses++
c.stats.mu.Unlock()
}
// updateSize updates the cached size count
func (c *CVCache) updateSize() {
c.stats.mu.Lock()
c.stats.Size = len(c.entries)
c.stats.mu.Unlock()
}
// cleanupExpired periodically removes expired entries
func (c *CVCache) cleanupExpired() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
c.mu.Lock()
now := time.Now()
removed := 0
for key, entry := range c.entries {
if now.After(entry.expiration) {
delete(c.entries, key)
removed++
}
}
if removed > 0 {
c.updateSize()
log.Printf("🧹 Cache cleanup: removed %d expired entries", removed)
}
c.mu.Unlock()
}
}
// BuildKey creates a consistent cache key
func BuildKey(prefix, lang string) string {
return fmt.Sprintf("%s:%s", prefix, lang)
}