// 通用重试策略 -- 跨行业可用的重试策略实现. // // 包含: // - ExponentialBackoff: 指数退避 + 抖动(防 thundering herd) // - ForegroundOnly: 后台请求不重试 529/过载(防容量级联放大) // - ConsecutiveLimit: 连续 N 次同类错误后放弃 // - ServerDirective: 尊重服务端的 x-should-retry 指令 // - MaxAttemptsLimit: 最大重试次数限制 // // 这些策略不依赖任何特定供应商,仓储/金融/编程场景通用. // Anthropic 特有策略在 anthropic.go 中. package retry import ( "crypto/rand" "encoding/binary" "math" "time" ) // ============================================================ // ExponentialBackoff - 指数退避 + 抖动 // ============================================================ // ExponentialBackoff 实现指数退避策略. // // 精妙之处(CLEVER): 公式 baseDelay * 2^(attempt-1) + random * jitter% * base // 经典的退避算法,被所有主流 SDK 采用(AWS/GCP/Azure 都用类似实现). // 抖动防止分布式环境下多个客户端同时重试导致 thundering herd. // // 替代方案: // - 固定间隔重试:简单但不适应负载波动 // - 线性退避:退避太慢,高负载时效果差 // - 全随机退避:退避不可预测,调试困难 type ExponentialBackoff struct { // BaseDelay 初始退避时间(默认 500ms) BaseDelay time.Duration // MaxDelay 最大退避时间上限(默认 32s) MaxDelay time.Duration // MaxRetries 最大重试次数(默认 10) MaxRetries int // JitterPct 抖动比例(默认 0.25 = 25%) JitterPct float64 } // DefaultExponentialBackoff 返回默认配置的指数退避策略. func DefaultExponentialBackoff() *ExponentialBackoff { return &ExponentialBackoff{ BaseDelay: 500 * time.Millisecond, MaxDelay: 32 * time.Second, MaxRetries: 10, JitterPct: 0.25, } } // ShouldRetry 如果错误本身可重试且未超过最大次数,返回带退避的重试决策. func (b *ExponentialBackoff) ShouldRetry(err RetryError, attempt int, ctx *RetryContext) *RetryDecision { maxRetries := b.MaxRetries if maxRetries <= 0 { maxRetries = 10 } if attempt > maxRetries { return &RetryDecision{Retry: false, Reason: "max retries exceeded"} } // 检查 APIError 自身是否可重试 if !err.IsRetryable() { return &RetryDecision{Retry: false, Reason: "error not retryable: " + err.Category()} } // 检查总时长限制 if ctx.MaxDuration > 0 && !ctx.StartTime.IsZero() { elapsed := time.Since(ctx.StartTime) if elapsed >= ctx.MaxDuration { return &RetryDecision{Retry: false, Reason: "max duration exceeded"} } } // 优先用 APIError.RetryInfo 中服务端建议的延迟 delay := err.RetryDelay() if delay == 0 { delay = b.calculateDelay(attempt) } return &RetryDecision{ Retry: true, Delay: delay, Reason: "exponential backoff", } } // calculateDelay 计算第 attempt 次的退避时间. func (b *ExponentialBackoff) calculateDelay(attempt int) time.Duration { baseDelay := b.BaseDelay if baseDelay <= 0 { baseDelay = 500 * time.Millisecond } maxDelay := b.MaxDelay if maxDelay <= 0 { maxDelay = 32 * time.Second } jitterPct := b.JitterPct if jitterPct <= 0 { jitterPct = 0.25 } // base * 2^(attempt-1),上限 maxDelay base := float64(baseDelay) * math.Pow(2, float64(attempt-1)) if base > float64(maxDelay) { base = float64(maxDelay) } // + random * jitter% * base // 升华改进(ELEVATED): 早期方案用 math/rand(伪随机),对于 jitter 的目的(防 thundering herd) // 密码学随机更彻底--多进程同时初始化重试时,伪随机序列可能相同(相同 seed), // 导致抖动形同虚设,多客户端依然同时重试. // LEGACY: crypto/rand.Float64() 不存在,需手动实现.失败时 fallback 到 0(无 jitter,安全降级). // 原方案:rand.Float64() - 否决:同进程多 goroutine 的伪随机序列足够分散, // 但跨进程启动时若未 seed 则相同(Go 1.20+ 已自动 seed,此风险已弱化为 P3). jitter := cryptoFloat64() * jitterPct * base return time.Duration(base + jitter) } // ============================================================ // ForegroundOnly - 后台请求不重试过载 // ============================================================ // ForegroundOnly 限制只有前景请求才重试 529/过载错误. // // 精妙之处(CLEVER): 容量级联时,每次重试是 3-10× 网关放大. // 后台请求(标题生成,摘要,分类器)失败了用户看不到, // 重试只会让过载更严重. // // 升华改进(ELEVATED): 早期方案用 FOREGROUND_529_RETRY_SOURCES Set 硬编码 15+ 来源, // 每加一种查询源要改重试模块.我们让调用方声明 IsForeground=true/false. // // 替代方案:<原方案在重试模块维护前景来源白名单 Set> type ForegroundOnly struct{} // ShouldRetry 后台请求遇到过载直接放弃. func (f *ForegroundOnly) ShouldRetry(err RetryError, attempt int, ctx *RetryContext) *RetryDecision { // 只拦截过载类错误 if err.Category() != "server_overload" { return nil // 不表态,交给下一个策略 } // 前景请求 → 不拦截,让下游策略决定 if ctx.IsForeground { return nil } // 后台请求遇到过载 → 直接放弃 return &RetryDecision{ Retry: false, Reason: "background request dropped during overload", } } // ============================================================ // ConsecutiveLimit - 连续同类错误上限 // ============================================================ // ConsecutiveLimit 在连续 N 次同类错误后放弃重试. // // 精妙之处(CLEVER): 早期方案硬编码 MAX_529_RETRIES=3,然后触发模型降级. // 我们泛化为任意错误类别 + 可配置上限,模型降级由上层或另一个策略负责. // // 用法示例:连续 3 次 ErrOverloaded → 放弃(上层可以据此触发降级) type ConsecutiveLimit struct { // Category 要追踪的错误类别字符串(如 "server_overload") Category string // Limit 连续出现次数上限 Limit int } // ShouldRetry 检查连续同类错误计数. func (c *ConsecutiveLimit) ShouldRetry(err RetryError, attempt int, ctx *RetryContext) *RetryDecision { if err.Category() != c.Category { return nil // 不是目标类别,不表态 } count := ctx.ConsecutiveCounts[c.Category] if count >= c.Limit { return &RetryDecision{ Retry: false, Reason: "consecutive " + c.Category + " limit reached", } } return nil // 还没到上限,让下游策略决定 } // ============================================================ // ServerDirective - 尊重服务端重试指令 // ============================================================ // ServerDirective 尊重 APIError.RetryInfo.ServerSaid 字段 // (来自 x-should-retry header,在 8.1 的 Classifier 中已解析). // // 精妙之处(CLEVER): 服务端最清楚"这个错误重试有没有意义". // 比如 429 限流,服务端说 should-retry: false 可能是因为 // 窗口级限制要等几小时--自动重试毫无意义. type ServerDirective struct{} // ShouldRetry 服务端明确表态时遵从. func (s *ServerDirective) ShouldRetry(err RetryError, attempt int, ctx *RetryContext) *RetryDecision { info := err.RetryInfo() if info == nil || info.ServerSaid == nil { return nil // 服务端没表态,不拦截 } if *info.ServerSaid { return nil // 服务端说可以重试--不拦截,让退避策略决定延迟 } // 服务端明确说不要重试 return &RetryDecision{ Retry: false, Reason: "server said do not retry", } } // ============================================================ // MaxAttemptsLimit - 最大重试次数 // ============================================================ // MaxAttemptsLimit 是独立的最大重试次数策略. // 与 ExponentialBackoff.MaxRetries 不同,这是一个独立策略, // 可以放在 Composite 的最前面作为硬上限. type MaxAttemptsLimit struct { Max int } // ShouldRetry 超过最大次数直接放弃. func (m *MaxAttemptsLimit) ShouldRetry(err RetryError, attempt int, ctx *RetryContext) *RetryDecision { if attempt > m.Max { return &RetryDecision{Retry: false, Reason: "hard attempt limit reached"} } return nil } // cryptoFloat64 使用 crypto/rand 返回 [0.0, 1.0) 的随机浮点数. // // 精妙之处(CLEVER): crypto/rand 无 Float64 接口,需手动读取 8 字节并按 IEEE 754 归一化. // 右移 11 位取 53 位有效位(float64 尾数位宽),再除以 2^53 得到 [0, 1) 范围. // 这与 math/rand 内部实现一致,只是随机源换成了 CSPRNG. // // 失败时返回 0(无 jitter,而非 panic/error),因为 jitter 是优化而非正确性要求. func cryptoFloat64() float64 { var buf [8]byte if _, err := rand.Read(buf[:]); err != nil { // 降级:无 jitter(base delay 无抖动),比 panic 更安全 return 0 } // 取 53 位有效位(float64 尾数)归一化到 [0.0, 1.0) u := binary.LittleEndian.Uint64(buf[:]) >> 11 return float64(u) / (1 << 53) }