Files
CLIProxyAPI/internal/runtime/executor/helps/cache_helpers.go
Luis Pater 2a050dc95d feat: enhance fault tolerance for kv-based caching and introduce additional tests
- Updated Antigravity Credits fallback to handle KV store unavailability as a service error.
- Enhanced signature caching mechanisms with request-time KV access and sliding expiration.
- Added and improved tests for KV client interactions, including error handling and expiration behaviors.
- Introduced `CacheSignatureBestEffort` for non-critical signature caching and clarified function flows with required context.
- Ensured consistent error reporting for missing or unavailable KV stores in various scenarios.
- Replaced direct `homekv` calls with injectable KV client interfaces for `antigravity` and `codex_reasoning_replay` modules.
- Improved error reporting and handling for KV operations, including `KVGet`, `KVSet`, `KVDel`, and `KVExpire`.
- Introduced dedicated fake KV clients for expanded and granular test coverage.
- Added new unit tests to validate KV client behaviors and error scenarios, ensuring robustness and sliding expiration functionality.
2026-06-14 21:11:35 +08:00

129 lines
3.6 KiB
Go

package helps
import (
"context"
"sync"
"time"
homekv "github.com/router-for-me/CLIProxyAPI/v7/internal/home"
)
type CodexCache struct {
ID string
Expire time.Time
}
// codexCacheMap stores prompt cache IDs keyed by model+user_id.
// Protected by codexCacheMu. Entries expire after 1 hour.
var (
codexCacheMap = make(map[string]CodexCache)
codexCacheMu sync.RWMutex
)
// codexCacheCleanupInterval controls how often expired entries are purged.
const codexCacheCleanupInterval = 15 * time.Minute
// codexCacheCleanupOnce ensures the background cleanup goroutine starts only once.
var codexCacheCleanupOnce sync.Once
// startCodexCacheCleanup launches a background goroutine that periodically
// removes expired entries from codexCacheMap to prevent memory leaks.
func startCodexCacheCleanup() {
go func() {
ticker := time.NewTicker(codexCacheCleanupInterval)
defer ticker.Stop()
for range ticker.C {
purgeExpiredCodexCache()
}
}()
}
// purgeExpiredCodexCache removes entries that have expired.
func purgeExpiredCodexCache() {
now := time.Now()
codexCacheMu.Lock()
defer codexCacheMu.Unlock()
for key, cache := range codexCacheMap {
if cache.Expire.Before(now) {
delete(codexCacheMap, key)
}
}
}
// GetCodexCache retrieves a cached entry, returning ok=false if not found or expired.
func GetCodexCache(key string) (CodexCache, bool) {
cache, ok, err := GetCodexCacheRequired(context.Background(), key)
if err == nil {
return cache, ok
}
return CodexCache{}, false
}
// GetCodexCacheRequired retrieves a cached entry for request-time paths.
func GetCodexCacheRequired(ctx context.Context, key string) (CodexCache, bool, error) {
var homeCache CodexCache
homeMode, found, errGet := homekv.KVGetJSONRequired(ctx, key, &homeCache)
if homeMode {
if errGet != nil || !found {
return CodexCache{}, false, errGet
}
if homeCache.Expire.Before(time.Now()) {
_, _, _ = homekv.KVDelRequired(ctx, key)
return CodexCache{}, false, nil
}
return homeCache, true, nil
}
codexCacheCleanupOnce.Do(startCodexCacheCleanup)
codexCacheMu.RLock()
cache, ok := codexCacheMap[key]
codexCacheMu.RUnlock()
if !ok || cache.Expire.Before(time.Now()) {
return CodexCache{}, false, nil
}
return cache, true, nil
}
// SetCodexCache stores a cache entry.
func SetCodexCache(key string, cache CodexCache) {
SetCodexCacheBestEffort(context.Background(), key, cache)
}
// SetCodexCacheRequired stores a cache entry for request-time paths.
func SetCodexCacheRequired(ctx context.Context, key string, cache CodexCache) error {
ttl := time.Until(cache.Expire)
if ttl <= 0 {
return nil
}
if _, homeMode, _ := homekv.CurrentKVClient(); homeMode {
_, errSet := homekv.KVSetJSONRequired(ctx, key, cache, ttl)
return errSet
}
codexCacheCleanupOnce.Do(startCodexCacheCleanup)
codexCacheMu.Lock()
codexCacheMap[key] = cache
codexCacheMu.Unlock()
return nil
}
// SetCodexCacheBestEffort stores a cache entry without failing completed responses.
func SetCodexCacheBestEffort(ctx context.Context, key string, cache CodexCache) bool {
ttl := time.Until(cache.Expire)
if ttl <= 0 {
return false
}
if _, homeMode, _ := homekv.CurrentKVClient(); homeMode {
return homekv.KVSetJSONBestEffort(ctx, key, cache, ttl)
}
codexCacheCleanupOnce.Do(startCodexCacheCleanup)
codexCacheMu.Lock()
codexCacheMap[key] = cache
codexCacheMu.Unlock()
return true
}
// CodexPromptCacheKey builds the Home KV key for a model/user prompt cache.
func CodexPromptCacheKey(modelName string, userScope string) string {
return "cpa:codex:prompt-cache:" + homekv.HashKeyPart(modelName) + ":" + homekv.HashKeyPart(userScope)
}