320 lines
6.9 KiB
Go
320 lines
6.9 KiB
Go
package cache
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"crypto/md5"
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"sync"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
// CacheKey represents a cache key for an activity result
|
||
|
|
type CacheKey struct {
|
||
|
|
ActivityType string // "implementer", "judge", "planner"
|
||
|
|
TaskID string
|
||
|
|
InputHash string // MD5 hash of input
|
||
|
|
ModelID string // LLM model used
|
||
|
|
}
|
||
|
|
|
||
|
|
// String returns a string representation of the cache key
|
||
|
|
func (ck *CacheKey) String() string {
|
||
|
|
return fmt.Sprintf("%s:%s:%s:%s", ck.ActivityType, ck.TaskID, ck.InputHash, ck.ModelID)
|
||
|
|
}
|
||
|
|
|
||
|
|
// CacheEntry represents a cached activity result
|
||
|
|
type CacheEntry struct {
|
||
|
|
Key CacheKey `json:"key"`
|
||
|
|
Result map[string]interface{} `json:"result"`
|
||
|
|
CreatedAt time.Time `json:"created_at"`
|
||
|
|
HitCount int `json:"hit_count"`
|
||
|
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// ResultCache caches activity results to avoid redundant computations
|
||
|
|
type ResultCache struct {
|
||
|
|
mu sync.RWMutex
|
||
|
|
basePath string
|
||
|
|
cache map[string]*CacheEntry
|
||
|
|
maxSize int
|
||
|
|
ttl time.Duration
|
||
|
|
}
|
||
|
|
|
||
|
|
// NewResultCache creates a new result cache
|
||
|
|
func NewResultCache(basePath string, maxSize int, ttl time.Duration) *ResultCache {
|
||
|
|
return &ResultCache{
|
||
|
|
basePath: basePath,
|
||
|
|
cache: make(map[string]*CacheEntry),
|
||
|
|
maxSize: maxSize,
|
||
|
|
ttl: ttl,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ComputeHash computes a hash of the input data
|
||
|
|
func ComputeHash(data interface{}) (string, error) {
|
||
|
|
jsonData, err := json.Marshal(data)
|
||
|
|
if err != nil {
|
||
|
|
return "", err
|
||
|
|
}
|
||
|
|
|
||
|
|
hash := md5.Sum(jsonData)
|
||
|
|
return fmt.Sprintf("%x", hash), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Set stores a result in the cache
|
||
|
|
func (rc *ResultCache) Set(key *CacheKey, result map[string]interface{}) error {
|
||
|
|
if key == nil {
|
||
|
|
return fmt.Errorf("cache key cannot be nil")
|
||
|
|
}
|
||
|
|
|
||
|
|
rc.mu.Lock()
|
||
|
|
defer rc.mu.Unlock()
|
||
|
|
|
||
|
|
keyStr := key.String()
|
||
|
|
|
||
|
|
entry := &CacheEntry{
|
||
|
|
Key: *key,
|
||
|
|
Result: result,
|
||
|
|
CreatedAt: time.Now(),
|
||
|
|
Metadata: make(map[string]interface{}),
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check size limit
|
||
|
|
if len(rc.cache) >= rc.maxSize && rc.cache[keyStr] == nil {
|
||
|
|
// Evict oldest entry (simple FIFO)
|
||
|
|
var oldestKey string
|
||
|
|
var oldestTime time.Time
|
||
|
|
|
||
|
|
for k, v := range rc.cache {
|
||
|
|
if oldestTime.IsZero() || v.CreatedAt.Before(oldestTime) {
|
||
|
|
oldestKey = k
|
||
|
|
oldestTime = v.CreatedAt
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if oldestKey != "" {
|
||
|
|
delete(rc.cache, oldestKey)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
rc.cache[keyStr] = entry
|
||
|
|
return rc.persistLocked(keyStr, entry)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get retrieves a result from the cache
|
||
|
|
func (rc *ResultCache) Get(key *CacheKey) (map[string]interface{}, bool, error) {
|
||
|
|
if key == nil {
|
||
|
|
return nil, false, fmt.Errorf("cache key cannot be nil")
|
||
|
|
}
|
||
|
|
|
||
|
|
rc.mu.Lock()
|
||
|
|
defer rc.mu.Unlock()
|
||
|
|
|
||
|
|
keyStr := key.String()
|
||
|
|
entry, exists := rc.cache[keyStr]
|
||
|
|
|
||
|
|
if !exists {
|
||
|
|
return nil, false, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check TTL
|
||
|
|
if rc.ttl > 0 && time.Since(entry.CreatedAt) > rc.ttl {
|
||
|
|
delete(rc.cache, keyStr)
|
||
|
|
return nil, false, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Increment hit count
|
||
|
|
entry.HitCount++
|
||
|
|
_ = rc.persistLocked(keyStr, entry)
|
||
|
|
|
||
|
|
return entry.Result, true, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Invalidate removes a cache entry
|
||
|
|
func (rc *ResultCache) Invalidate(key *CacheKey) error {
|
||
|
|
if key == nil {
|
||
|
|
return fmt.Errorf("cache key cannot be nil")
|
||
|
|
}
|
||
|
|
|
||
|
|
rc.mu.Lock()
|
||
|
|
defer rc.mu.Unlock()
|
||
|
|
|
||
|
|
keyStr := key.String()
|
||
|
|
delete(rc.cache, keyStr)
|
||
|
|
|
||
|
|
// Delete from disk
|
||
|
|
cacheFile := filepath.Join(rc.basePath, "cache", fmt.Sprintf("%s.json", keyStr))
|
||
|
|
_ = os.Remove(cacheFile)
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Clear clears all cache entries
|
||
|
|
func (rc *ResultCache) Clear() error {
|
||
|
|
rc.mu.Lock()
|
||
|
|
defer rc.mu.Unlock()
|
||
|
|
|
||
|
|
rc.cache = make(map[string]*CacheEntry)
|
||
|
|
|
||
|
|
// Clear disk cache
|
||
|
|
cacheDir := filepath.Join(rc.basePath, "cache")
|
||
|
|
_ = os.RemoveAll(cacheDir)
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetStats returns cache statistics
|
||
|
|
func (rc *ResultCache) GetStats() map[string]interface{} {
|
||
|
|
rc.mu.RLock()
|
||
|
|
defer rc.mu.RUnlock()
|
||
|
|
|
||
|
|
totalHits := 0
|
||
|
|
for _, entry := range rc.cache {
|
||
|
|
totalHits += entry.HitCount
|
||
|
|
}
|
||
|
|
|
||
|
|
return map[string]interface{}{
|
||
|
|
"size": len(rc.cache),
|
||
|
|
"max_size": rc.maxSize,
|
||
|
|
"total_hits": totalHits,
|
||
|
|
"usage_ratio": float64(len(rc.cache)) / float64(rc.maxSize),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetSize returns the current cache size
|
||
|
|
func (rc *ResultCache) GetSize() int {
|
||
|
|
rc.mu.RLock()
|
||
|
|
defer rc.mu.RUnlock()
|
||
|
|
|
||
|
|
return len(rc.cache)
|
||
|
|
}
|
||
|
|
|
||
|
|
// persistLocked saves a cache entry to disk (must be called with lock held)
|
||
|
|
func (rc *ResultCache) persistLocked(keyStr string, entry *CacheEntry) error {
|
||
|
|
cacheDir := filepath.Join(rc.basePath, "cache")
|
||
|
|
|
||
|
|
// Create directory if it doesn't exist
|
||
|
|
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
cacheFile := filepath.Join(cacheDir, fmt.Sprintf("%s.json", keyStr))
|
||
|
|
|
||
|
|
data, err := json.MarshalIndent(entry, "", " ")
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
return os.WriteFile(cacheFile, data, 0644)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Load loads cache from disk
|
||
|
|
func (rc *ResultCache) Load() error {
|
||
|
|
rc.mu.Lock()
|
||
|
|
defer rc.mu.Unlock()
|
||
|
|
|
||
|
|
cacheDir := filepath.Join(rc.basePath, "cache")
|
||
|
|
entries, err := os.ReadDir(cacheDir)
|
||
|
|
if err != nil {
|
||
|
|
if os.IsNotExist(err) {
|
||
|
|
return nil // Cache doesn't exist yet
|
||
|
|
}
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, entry := range entries {
|
||
|
|
if entry.IsDir() {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
filePath := filepath.Join(cacheDir, entry.Name())
|
||
|
|
data, err := os.ReadFile(filePath)
|
||
|
|
if err != nil {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
var cacheEntry CacheEntry
|
||
|
|
if err := json.Unmarshal(data, &cacheEntry); err != nil {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
// Skip expired entries
|
||
|
|
if rc.ttl > 0 && time.Since(cacheEntry.CreatedAt) > rc.ttl {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
keyStr := cacheEntry.Key.String()
|
||
|
|
rc.cache[keyStr] = &cacheEntry
|
||
|
|
}
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// InvalidateByActivity invalidates all cache entries for an activity type
|
||
|
|
func (rc *ResultCache) InvalidateByActivity(activityType string) error {
|
||
|
|
rc.mu.Lock()
|
||
|
|
defer rc.mu.Unlock()
|
||
|
|
|
||
|
|
keysToDelete := make([]string, 0)
|
||
|
|
for keyStr, entry := range rc.cache {
|
||
|
|
if entry.Key.ActivityType == activityType {
|
||
|
|
keysToDelete = append(keysToDelete, keyStr)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, keyStr := range keysToDelete {
|
||
|
|
delete(rc.cache, keyStr)
|
||
|
|
|
||
|
|
// Delete from disk
|
||
|
|
cacheFile := filepath.Join(rc.basePath, "cache", fmt.Sprintf("%s.json", keyStr))
|
||
|
|
_ = os.Remove(cacheFile)
|
||
|
|
}
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// InvalidateByTask invalidates all cache entries for a task
|
||
|
|
func (rc *ResultCache) InvalidateByTask(taskID string) error {
|
||
|
|
rc.mu.Lock()
|
||
|
|
defer rc.mu.Unlock()
|
||
|
|
|
||
|
|
keysToDelete := make([]string, 0)
|
||
|
|
for keyStr, entry := range rc.cache {
|
||
|
|
if entry.Key.TaskID == taskID {
|
||
|
|
keysToDelete = append(keysToDelete, keyStr)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, keyStr := range keysToDelete {
|
||
|
|
delete(rc.cache, keyStr)
|
||
|
|
|
||
|
|
// Delete from disk
|
||
|
|
cacheFile := filepath.Join(rc.basePath, "cache", fmt.Sprintf("%s.json", keyStr))
|
||
|
|
_ = os.Remove(cacheFile)
|
||
|
|
}
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetHitRate returns the cache hit rate
|
||
|
|
func (rc *ResultCache) GetHitRate() (float64, int) {
|
||
|
|
rc.mu.RLock()
|
||
|
|
defer rc.mu.RUnlock()
|
||
|
|
|
||
|
|
if len(rc.cache) == 0 {
|
||
|
|
return 0, 0
|
||
|
|
}
|
||
|
|
|
||
|
|
totalHits := 0
|
||
|
|
for _, entry := range rc.cache {
|
||
|
|
totalHits += entry.HitCount
|
||
|
|
}
|
||
|
|
|
||
|
|
if totalHits == 0 {
|
||
|
|
return 0, len(rc.cache)
|
||
|
|
}
|
||
|
|
|
||
|
|
return float64(totalHits) / float64(len(rc.cache)), len(rc.cache)
|
||
|
|
}
|