Files
CLIProxyAPI/internal/runtime/executor/helps/session_id_cache.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

149 lines
3.7 KiB
Go

package helps
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
"sync"
"time"
"github.com/google/uuid"
homekv "github.com/router-for-me/CLIProxyAPI/v7/internal/home"
)
type sessionIDCacheEntry struct {
value string
expire time.Time
}
var (
sessionIDCache = make(map[string]sessionIDCacheEntry)
sessionIDCacheMu sync.RWMutex
sessionIDCacheCleanupOnce sync.Once
)
type claudeIDKVClient interface {
KVGet(ctx context.Context, key string) ([]byte, bool, error)
KVSetNX(ctx context.Context, key string, value []byte, ttl time.Duration) (bool, error)
KVExpire(ctx context.Context, key string, ttl time.Duration) (bool, error)
}
var currentClaudeIDKVClient = func() (claudeIDKVClient, bool, error) {
return homekv.CurrentKVClient()
}
const (
sessionIDTTL = time.Hour
sessionIDCacheCleanupPeriod = 15 * time.Minute
)
func startSessionIDCacheCleanup() {
go func() {
ticker := time.NewTicker(sessionIDCacheCleanupPeriod)
defer ticker.Stop()
for range ticker.C {
purgeExpiredSessionIDs()
}
}()
}
func purgeExpiredSessionIDs() {
now := time.Now()
sessionIDCacheMu.Lock()
for key, entry := range sessionIDCache {
if !entry.expire.After(now) {
delete(sessionIDCache, key)
}
}
sessionIDCacheMu.Unlock()
}
func sessionIDCacheKey(apiKey string) string {
sum := sha256.Sum256([]byte(apiKey))
return hex.EncodeToString(sum[:])
}
// CachedSessionID returns a stable session UUID per apiKey, refreshing the TTL on each access.
func CachedSessionID(apiKey string) string {
value, errValue := CachedSessionIDRequired(context.Background(), apiKey)
if errValue == nil && value != "" {
return value
}
return uuid.New().String()
}
// CachedSessionIDRequired returns a stable session UUID per apiKey for request-time paths.
func CachedSessionIDRequired(ctx context.Context, apiKey string) (string, error) {
if apiKey == "" {
return uuid.New().String(), nil
}
client, homeMode, errClient := currentClaudeIDKVClient()
if homeMode {
if errClient != nil {
return "", errClient
}
key := claudeSessionIDKVKey(apiKey)
raw, found, errGet := client.KVGet(ctx, key)
if errGet != nil {
return "", errGet
}
if found && strings.TrimSpace(string(raw)) != "" {
if _, errExpire := client.KVExpire(ctx, key, sessionIDTTL); errExpire != nil {
return "", errExpire
}
return strings.TrimSpace(string(raw)), nil
}
newID := uuid.New().String()
if _, errSet := client.KVSetNX(ctx, key, []byte(newID), sessionIDTTL); errSet != nil {
return "", errSet
}
raw, found, errGet = client.KVGet(ctx, key)
if errGet != nil {
return "", errGet
}
if found && strings.TrimSpace(string(raw)) != "" {
return strings.TrimSpace(string(raw)), nil
}
return "", fmt.Errorf("home kv session id missing after set")
}
sessionIDCacheCleanupOnce.Do(startSessionIDCacheCleanup)
key := sessionIDCacheKey(apiKey)
now := time.Now()
sessionIDCacheMu.RLock()
entry, ok := sessionIDCache[key]
valid := ok && entry.value != "" && entry.expire.After(now)
sessionIDCacheMu.RUnlock()
if valid {
sessionIDCacheMu.Lock()
entry = sessionIDCache[key]
if entry.value != "" && entry.expire.After(now) {
entry.expire = now.Add(sessionIDTTL)
sessionIDCache[key] = entry
sessionIDCacheMu.Unlock()
return entry.value, nil
}
sessionIDCacheMu.Unlock()
}
newID := uuid.New().String()
sessionIDCacheMu.Lock()
entry, ok = sessionIDCache[key]
if !ok || entry.value == "" || !entry.expire.After(now) {
entry.value = newID
}
entry.expire = now.Add(sessionIDTTL)
sessionIDCache[key] = entry
sessionIDCacheMu.Unlock()
return entry.value, nil
}
func claudeSessionIDKVKey(apiKey string) string {
return "cpa:claude:session-id:" + homekv.HashKeyPart(apiKey)
}