190 lines
3.6 KiB
Go
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)
|
|
}
|