Files
MyClub/internal/services/cache_service.go
T
Tomáš Dvořák 12cba639b9 upload
2025-10-16 13:32:05 +02:00

264 lines
5.2 KiB
Go

package services
import (
"context"
"encoding/json"
"fmt"
"sync"
"time"
)
// CacheService provides a simple in-memory cache with TTL
type CacheService struct {
mu sync.RWMutex
items map[string]*cacheItem
}
type cacheItem struct {
Value interface{}
Expiration time.Time
}
var (
defaultCache *CacheService
cacheInitOnce sync.Once
)
// GetCacheService returns singleton cache instance
func GetCacheService() *CacheService {
cacheInitOnce.Do(func() {
defaultCache = &CacheService{
items: make(map[string]*cacheItem),
}
// Start cleanup goroutine
go defaultCache.startCleanup()
})
return defaultCache
}
// Get retrieves value from cache
func (cs *CacheService) Get(key string, dest interface{}) error {
cs.mu.RLock()
item, exists := cs.items[key]
cs.mu.RUnlock()
if !exists {
return fmt.Errorf("key not found")
}
if time.Now().After(item.Expiration) {
cs.Delete(key)
return fmt.Errorf("key expired")
}
// Type assertion or JSON marshal/unmarshal
switch v := item.Value.(type) {
case []byte:
return json.Unmarshal(v, dest)
default:
// Marshal and unmarshal for type conversion
data, err := json.Marshal(item.Value)
if err != nil {
return err
}
return json.Unmarshal(data, dest)
}
}
// Set stores value in cache with TTL
func (cs *CacheService) Set(key string, value interface{}, ttl time.Duration) error {
cs.mu.Lock()
defer cs.mu.Unlock()
cs.items[key] = &cacheItem{
Value: value,
Expiration: time.Now().Add(ttl),
}
return nil
}
// Delete removes item from cache
func (cs *CacheService) Delete(key string) {
cs.mu.Lock()
defer cs.mu.Unlock()
delete(cs.items, key)
}
// Clear removes all items from cache
func (cs *CacheService) Clear() {
cs.mu.Lock()
defer cs.mu.Unlock()
cs.items = make(map[string]*cacheItem)
}
// Has checks if key exists and is not expired
func (cs *CacheService) Has(key string) bool {
cs.mu.RLock()
item, exists := cs.items[key]
cs.mu.RUnlock()
if !exists {
return false
}
if time.Now().After(item.Expiration) {
cs.Delete(key)
return false
}
return true
}
// GetOrSet retrieves from cache or executes function and caches result
func (cs *CacheService) GetOrSet(key string, dest interface{}, ttl time.Duration, fn func() (interface{}, error)) error {
// Try to get from cache
err := cs.Get(key, dest)
if err == nil {
return nil
}
// Execute function
value, err := fn()
if err != nil {
return err
}
// Store in cache
if err := cs.Set(key, value, ttl); err != nil {
return err
}
// Marshal and unmarshal for type conversion
data, err := json.Marshal(value)
if err != nil {
return err
}
return json.Unmarshal(data, dest)
}
// startCleanup periodically removes expired items
func (cs *CacheService) startCleanup() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
cs.cleanup()
}
}
func (cs *CacheService) cleanup() {
cs.mu.Lock()
defer cs.mu.Unlock()
now := time.Now()
for key, item := range cs.items {
if now.After(item.Expiration) {
delete(cs.items, key)
}
}
}
// Stats returns cache statistics
func (cs *CacheService) Stats() map[string]interface{} {
cs.mu.RLock()
defer cs.mu.RUnlock()
expired := 0
now := time.Now()
for _, item := range cs.items {
if now.After(item.Expiration) {
expired++
}
}
return map[string]interface{}{
"total_items": len(cs.items),
"expired_items": expired,
"active_items": len(cs.items) - expired,
}
}
// CacheKey generates consistent cache keys
func CacheKey(parts ...string) string {
return "cache:" + joinStrings(parts, ":")
}
func joinStrings(parts []string, sep string) string {
if len(parts) == 0 {
return ""
}
result := parts[0]
for i := 1; i < len(parts); i++ {
result += sep + parts[i]
}
return result
}
// WithCache is a decorator for caching function results
func WithCache(key string, ttl time.Duration, fn func() (interface{}, error)) (interface{}, error) {
cache := GetCacheService()
// Try cache first
var result interface{}
if err := cache.Get(key, &result); err == nil {
return result, nil
}
// Execute function
result, err := fn()
if err != nil {
return nil, err
}
// Cache result
cache.Set(key, result, ttl)
return result, nil
}
// InvalidateCachePattern removes all keys matching pattern
func (cs *CacheService) InvalidateCachePattern(pattern string) {
cs.mu.Lock()
defer cs.mu.Unlock()
for key := range cs.items {
if matchesPattern(key, pattern) {
delete(cs.items, key)
}
}
}
func matchesPattern(key, pattern string) bool {
// Simple pattern matching - supports * wildcard
if pattern == "*" {
return true
}
// Check if pattern contains wildcard
if len(pattern) > 0 && pattern[len(pattern)-1] == '*' {
prefix := pattern[:len(pattern)-1]
return len(key) >= len(prefix) && key[:len(prefix)] == prefix
}
return key == pattern
}
// WarmupCache preloads frequently accessed data
func WarmupCache(ctx context.Context) error {
_ = GetCacheService() // Available for warmup logic
// Example: preload settings
// This should be called on application startup
// Add your warmup logic here
// Example:
// cache := GetCacheService()
// settings, err := fetchSettings()
// if err == nil {
// cache.Set("settings:main", settings, 1*time.Hour)
// }
return nil
}