Current section

Files

Jump to
cmdc lib cmdc agent retry.ex
Raw

lib/cmdc/agent/retry.ex

defmodule CMDC.Agent.Retry do
# 内部模块 — Provider 错误分类与重试延迟计算。API 不保证稳定。
@moduledoc false
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