Current section
Files
Jump to
Current section
Files
lib/cmdc/agent/retry.ex
defmodule CMDC.Agent.Retry do
@moduledoc """
Provider 错误分类与重试延迟计算。
## 错误分类
- **Transient(暂时性)** — 限流、服务器过载、网络断开等,等待后重试可能成功。
- **Permanent(永久性)** — 认证失败、上下文溢出、无效请求,重试无意义。
永久性错误优先检测——若同一错误同时包含永久与暂时模式,归为永久性。
## 退避策略
指数退避:`delay = base_ms × 2^(attempt-1)`,上限 `max_ms`。
不引入 jitter——单 Agent 并发请求量不足以受益,且确定性延迟便于测试。
"""
alias CMDC.Agent.Overflow
@transient_patterns [
"overloaded",
"rate_limit",
"rate limit",
"too many requests",
"429",
"500",
"502",
"503",
"504",
"connection",
"econnreset",
"econnrefused",
"etimedout",
"fetch failed",
"socket hang up",
"request timeout",
"server_error",
"timeout",
"recv_timeout",
"connect_timeout",
"read_timeout",
"stream_timeout",
"closed",
"transport_error"
]
@permanent_patterns Overflow.overflow_patterns() ++
[
"unauthorized",
"invalid_api_key",
"authentication"
]
# ==========================================================================
# Public API
# ==========================================================================
@doc """
返回 `true` 如果该错误是暂时性的、可以重试。
## 示例
iex> CMDC.Agent.Retry.retryable?("429 Too Many Requests")
true
iex> CMDC.Agent.Retry.retryable?("context_length_exceeded")
false
"""
@spec retryable?(term()) :: boolean()
def retryable?(reason) do
text = stringify(reason) |> String.downcase()
not matches_any?(text, @permanent_patterns) and matches_any?(text, @transient_patterns)
end
@doc """
计算第 `attempt` 次重试的延迟毫秒数。
## 选项
- `:base_ms` — 初始延迟(默认 `2_000`)
- `:max_ms` — 最大延迟(默认 `60_000`)
## 示例
iex> CMDC.Agent.Retry.delay(1)
2_000
iex> CMDC.Agent.Retry.delay(3)
8_000
"""
@spec delay(pos_integer(), keyword()) :: pos_integer()
def delay(attempt, opts \\ []) do
base = Keyword.get(opts, :base_ms, 2_000)
max = Keyword.get(opts, :max_ms, 60_000)
min(base * Integer.pow(2, attempt - 1), max)
end
@doc "返回暂时性错误模式列表。"
@spec transient_patterns() :: [String.t()]
def transient_patterns, do: @transient_patterns
@doc "返回永久性错误模式列表。"
@spec permanent_patterns() :: [String.t()]
def permanent_patterns, do: @permanent_patterns
# ==========================================================================
# 私有辅助
# ==========================================================================
defp stringify(reason) when is_binary(reason), do: reason
defp stringify(reason) when is_atom(reason), do: Atom.to_string(reason)
defp stringify(reason), do: inspect(reason)
defp matches_any?(text, patterns) do
Enum.any?(patterns, &String.contains?(text, &1))
end
end