package executor import ( "context" "strings" "sync" "time" "github.com/google/uuid" "github.com/redis/go-redis/v9" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" log "github.com/sirupsen/logrus" ) type codexPromptCacheRedisState struct { client *redis.Client url string timeout time.Duration } var ( codexPromptCacheRedisMu sync.Mutex codexPromptCacheRedis codexPromptCacheRedisState codexPromptCacheRedisWarns sync.Map ) func redisConfigFrom(cfg *config.Config) (url string, keyPrefix string, ttl time.Duration, timeout time.Duration) { url = "" keyPrefix = config.DefaultCodexPromptCacheKeyPrefix ttl = time.Duration(config.DefaultCodexPromptCacheTTLSeconds) * time.Second timeout = time.Duration(config.DefaultCodexPromptCacheTimeoutMS) * time.Millisecond if cfg == nil { return } url = strings.TrimSpace(cfg.CodexPromptCache.RedisURL) if v := strings.TrimSpace(cfg.CodexPromptCache.KeyPrefix); v != "" { keyPrefix = v } if cfg.CodexPromptCache.TTLSeconds > 0 { ttl = time.Duration(cfg.CodexPromptCache.TTLSeconds) * time.Second } if cfg.CodexPromptCache.TimeoutMS > 0 { timeout = time.Duration(cfg.CodexPromptCache.TimeoutMS) * time.Millisecond } return } func getOrCreateCodexPromptCacheRedis(cfg *config.Config, key string, requestTTL time.Duration) (string, bool) { redisURL, keyPrefix, ttl, timeout := redisConfigFrom(cfg) if requestTTL > 0 { ttl = clampCodexPromptCacheTTL(requestTTL) } if ttl <= 0 { ttl = clampCodexPromptCacheTTL(time.Duration(config.DefaultCodexPromptCacheTTLSeconds) * time.Second) } if redisURL == "" { return "", false } if timeout <= 0 { timeout = time.Duration(config.DefaultCodexPromptCacheTimeoutMS) * time.Millisecond } client, err := codexPromptCacheRedisClient(redisURL, timeout) if err != nil { warnCodexPromptCacheRedis("redis_init", err) return "", false } redisKey := keyPrefix + key ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() if existing, errGet := client.Get(ctx, redisKey).Result(); errGet == nil { trimmed := strings.TrimSpace(existing) if trimmed != "" { return trimmed, true } } else if errGet != redis.Nil { warnCodexPromptCacheRedis("redis_get", errGet) return "", false } candidate := uuid.NewString() setOK, errSet := client.SetNX(ctx, redisKey, candidate, ttl).Result() if errSet != nil { warnCodexPromptCacheRedis("redis_setnx", errSet) return "", false } if setOK { return candidate, true } winner, errWinner := client.Get(ctx, redisKey).Result() if errWinner != nil { warnCodexPromptCacheRedis("redis_get_after_setnx", errWinner) return "", false } winner = strings.TrimSpace(winner) if winner == "" { return "", false } return winner, true } func codexPromptCacheRedisClient(redisURL string, timeout time.Duration) (*redis.Client, error) { codexPromptCacheRedisMu.Lock() defer codexPromptCacheRedisMu.Unlock() if codexPromptCacheRedis.client != nil && codexPromptCacheRedis.url == redisURL && codexPromptCacheRedis.timeout == timeout { return codexPromptCacheRedis.client, nil } if codexPromptCacheRedis.client != nil { _ = codexPromptCacheRedis.client.Close() codexPromptCacheRedis.client = nil } opts, err := redis.ParseURL(redisURL) if err != nil { return nil, err } opts.DialTimeout = timeout opts.ReadTimeout = timeout opts.WriteTimeout = timeout client := redis.NewClient(opts) ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() if errPing := client.Ping(ctx).Err(); errPing != nil { _ = client.Close() return nil, errPing } codexPromptCacheRedis.client = client codexPromptCacheRedis.url = redisURL codexPromptCacheRedis.timeout = timeout codexPromptCacheRedisWarns = sync.Map{} return client, nil } func warnCodexPromptCacheRedis(kind string, err error) { if err == nil { return } if _, loaded := codexPromptCacheRedisWarns.LoadOrStore(kind+":"+err.Error(), struct{}{}); loaded { return } log.WithError(err).Warn("codex prompt cache redis unavailable, falling back to local cache") } func resetCodexPromptCacheRedisForTest() { codexPromptCacheRedisMu.Lock() defer codexPromptCacheRedisMu.Unlock() if codexPromptCacheRedis.client != nil { _ = codexPromptCacheRedis.client.Close() } codexPromptCacheRedis = codexPromptCacheRedisState{} codexPromptCacheRedisWarns = sync.Map{} }