#!/usr/bin/env python3
"""retry_with_backoff.py — 重试与指数退避决策器（无需 API key，本地可跑）

演示：
1. 哪些错误可重试（429 限速 / 500 / 529 / 连接错误）哪些不可（400/401/403/404/413、spend-cap 429）
2. 指数退避 + 抖动 + 尊重 retry-after
3. 类型化错误分支（对应 SDK 的 anthropic.RateLimitError 等类，而不是字符串匹配）

官方依据（截至 2026-08-30）:
- SDK 默认自动重试瞬时故障 2 次（可配 max_retries）
- 429 spend-cap（enforced_spend_limit_reached）无 retry-after，重试无效
来源: https://platform.claude.com/docs/en/api/errors（访问于 2026-08-30）

运行: python3 retry_with_backoff.py
"""

import random

random.seed(8)


class ApiError(Exception):
    """模拟 API 错误对象（真实 SDK 里是类型化异常类）。"""

    def __init__(self, status: int, error_type: str, retry_after: float | None = None,
                 error_code: str | None = None):
        super().__init__(f"{status} {error_type}")
        self.status = status
        self.error_type = error_type
        self.retry_after = retry_after
        self.error_code = error_code  # 如 enforced_spend_limit_reached


# 重试决策表：生产代码里这就是你的 catch 分支
def is_retryable(err: ApiError) -> bool:
    if err.status in (500, 529):                    # 服务端错误/过载：退避重试
        return True
    if err.status == 429:
        # spend-cap 429（无 retry-after 且 error_code=enforced_spend_limit_reached）不可重试
        if err.error_code == "enforced_spend_limit_reached":
            return False
        return True                                  # 普通限速 429：可重试
    if err.status == 409:                            # 冲突：解决冲突后可重试（视为可）
        return True
    return False                                     # 400/401/402/403/404/413：修请求，重试无用


def backoff_delay(attempt: int, base_s: float = 1.0, cap_s: float = 60.0,
                  retry_after: float | None = None) -> float:
    """指数退避 + 全抖动。若服务端给了 retry-after，优先尊重。"""
    if retry_after is not None:
        return retry_after
    return min(cap_s, base_s * (2 ** attempt)) * random.uniform(0.5, 1.0)


def call_with_retry(transport, max_retries: int = 3):
    """transport: 可调用，可能抛 ApiError。返回其结果或抛出最后的错误。"""
    last_err = None
    for attempt in range(max_retries + 1):
        try:
            return transport()
        except ApiError as e:
            last_err = e
            if not is_retryable(e):
                raise RuntimeError(
                    f"不可重试错误 {e.status} {e.error_type}"
                    + (f" code={e.error_code}" if e.error_code else "")
                    + " -> 修请求/凭证/配额，勿重试"
                ) from e
            if attempt == max_retries:
                break
            delay = backoff_delay(attempt, retry_after=e.retry_after)
            print(f"    第{attempt+1}次失败({e.status} {e.error_type})"
                  + (f" retry-after={e.retry_after}s" if e.retry_after else "")
                  + f" -> 退避 {delay:.2f}s 后重试")
    raise RuntimeError(f"重试 {max_retries} 次后仍失败: {last_err}")


if __name__ == "__main__":
    print("== 场景 1：普通限速 429（带 retry-after=3）==")
    state = {"n": 0}

    def flaky_429():
        state["n"] += 1
        if state["n"] < 3:
            raise ApiError(429, "rate_limit_error", retry_after=3.0)
        return {"ok": True, "attempt": state["n"]}

    print("   结果:", call_with_retry(flaky_429))

    print("\n== 场景 2：spend-cap 429（无 retry-after，重试无效）==")
    try:
        call_with_retry(lambda: (_ for _ in ()).throw(
            ApiError(429, "rate_limit_error", error_code="enforced_spend_limit_reached")))
    except RuntimeError as e:
        print("   正确熔断:", e)

    print("\n== 场景 3：500 连续失败 -> 退避后放弃 ==")
    try:
        call_with_retry(lambda: (_ for _ in ()).throw(ApiError(500, "api_error")), max_retries=2)
    except RuntimeError as e:
        print("   ", e)

    print("\n== 场景 4：401 认证错误 -> 立即失败不重试 ==")
    try:
        call_with_retry(lambda: (_ for _ in ()).throw(ApiError(401, "authentication_error")))
    except RuntimeError as e:
        print("   ", e)

    print("\n== 退避曲线（无 retry-after 时：base*2^n + 全抖动，cap 60s）==")
    for n in range(5):
        ds = [backoff_delay(n) for _ in range(3)]
        print(f"   attempt {n}: {[f'{d:.1f}s' for d in ds]}")
