#!/usr/bin/env python3
"""retry_backoff.py — 错误分类 + 指数退避重试模拟（无需 API key，本地可跑）

演示生产级重试策略的四个核心决策（模块 03 README §5）：
  1. 错误分类先行：哪些错误值得重试，哪些重试是浪费
  2. 指数退避 + 抖动（jitter）：delay = base × 2^n ± jitter
  3. 尊重 retry-after（429 带此头时优先于自身退避）
  4. 重试预算（最大次数），预算耗尽进入降级

官方依据（访问于 2026-08-30）:
  - SDK 默认自动重试 2 次、指数退避、尊重 retry-after:
    https://platform.claude.com/docs/en/api/errors
  - spend-cap 429 无 retry-after 且重试无效:
    https://platform.claude.com/docs/en/api/rate-limits
  - >10 分钟长请求应换 streaming / Batches 而非硬重试:
    https://platform.claude.com/docs/en/api/errors

运行: python3 retry_backoff.py
"""

from __future__ import annotations

import random

# 错误分类表（HTTP 状态码 -> (官方 error type, 是否可重试, 原因)）
ERROR_TABLE: dict[int, tuple[str, bool, str]] = {
    400: ("invalid_request_error", False, "请求有问题——修请求，不是重试"),
    401: ("authentication_error",   False, "凭据问题——换 key / 检查 WIF"),
    402: ("billing_error",          False, "账单问题——人工介入"),
    403: ("permission_error",       False, "无权限——检查 workspace/key 权限"),
    404: ("not_found_error",        False, "路径或资源 ID 错——修代码"),
    409: ("conflict_error",         True,  "状态冲突——官方建议解决后可重试"),
    413: ("request_too_large",      False, "超请求体上限——裁剪 payload"),
    429: ("rate_limit_error",       True,  "限流——按 retry-after 等；spend-cap 型除外"),
    500: ("api_error",              True,  "内部错误——官方明确建议指数退避重试"),
    504: ("timeout_error",          False, "处理超时——换 streaming/Batches，别原地重试"),
    529: ("overloaded_error",       True,  "过载——退避重试；流式场景可能以流内 error 事件出现"),
}


def backoff_delay(attempt: int, base_s: float = 1.0, cap_s: float = 60.0,
                  jitter: float = 0.3) -> float:
    """指数退避 + 全抖动近似：delay = min(base×2^n, cap) × uniform(1±jitter)。"""
    raw = min(base_s * (2 ** attempt), cap_s)
    return round(raw * random.uniform(1 - jitter, 1 + jitter), 2)


def request_with_retry(failures: list[tuple[int, dict | None]], max_retries: int = 2,
                        quiet: bool = False) -> str:
    """模拟带重试的调用。failures 是注入的错误序列((status, headers))，耗尽即"成功"。"""
    for attempt in range(max_retries + 1):
        if not failures:
            return "SUCCESS"
        status, headers = failures.pop(0)
        etype, retryable, why = ERROR_TABLE[status]

        spend_cap = status == 429 and (headers or {}).get("error_code") == "enforced_spend_limit_reached"
        retry_after = (headers or {}).get("retry-after")

        if not quiet:
            print(f"  第 {attempt} 次调用 -> {status} {etype} ({why})")
        if spend_cap:
            return "DEGRADE: spend-cap 429 无 retry-after,重试无效 -> 告警 + 升档/等次月"
        if not retryable:
            return f"DEGRADE: {etype} 不可重试 -> 修请求/换路径 + 告警"
        if attempt == max_retries:
            return "DEGRADE: 重试预算耗尽 -> fallback 模型 / 排队 / 人审"

        if retry_after is not None:
            wait = float(retry_after)  # 429 带 retry-after 时优先服从服务端
            if not quiet:
                print(f"    遵守 retry-after={wait}s（优先于自身退避）")
        else:
            wait = backoff_delay(attempt)
            if not quiet:
                print(f"    指数退避等待 {wait}s（base×2^{attempt} ± 30% jitter）")
    return "SUCCESS"


def main() -> None:
    rng = random.Random(42)
    random.seed(42)  # 让演示输出稳定

    print("=== 场景 1：瞬时 529 过载 -> 指数退避后成功 ===")
    print("结果:", request_with_retry([(529, None), (529, None)]))
    print("\n=== 场景 2：429 限流带 retry-after -> 服从服务端 ===")
    print("结果:", request_with_retry([(429, {"retry-after": "7"})]))
    print("\n=== 场景 3：spend-cap 429 -> 立即停止（重试无效）===")
    print("结果:", request_with_retry(
        [(429, {"error_code": "enforced_spend_limit_reached"})]))
    print("\n=== 场景 4：400 确定性错误 -> 不重试 ===")
    print("结果:", request_with_retry([(400, None)]))
    print("\n=== 场景 5：重试预算耗尽 -> 降级 ===")
    print("结果:", request_with_retry([(500, None), (500, None), (500, None)]))

    print("\n=== 退避曲线（base=1s, cap=60s）===")
    for n in range(7):
        print(f"  第 {n} 次重试: ~{backoff_delay(n)}s")

    # 自检：SDK 默认行为 = 最多 2 次自动重试（官方 errors 文档）
    assert request_with_retry([(529, None)], quiet=True) == "SUCCESS"
    assert request_with_retry([(404, None)], quiet=True).startswith("DEGRADE")
    assert backoff_delay(0) <= 1.3 and backoff_delay(6) <= 60 * 1.3
    print("\n自检通过：错误分类/预算/cap 行为符合官方文档描述")


if __name__ == "__main__":
    main()
