新增: 弱网韧性: send 网络感知重试与基准面探针

- connect 类失败重试: 10 分钟窗口+基准面感知(IP 直连与 DNS 双测判本机断网), 断网进长等待自适应探测(5s 快探到 30s 封顶省电), 恢复即重试; 单点故障指数短退避 1s→8s
- 两协议(anthropic/openai)同构, 隧道级分钟级弱网不再直接吐中断
This commit is contained in:
lxy
2026-10-07 18:33:41 +08:00
parent 76ace953be
commit c2382325cb
4 changed files with 230 additions and 72 deletions
+88 -41
View File
@@ -743,47 +743,94 @@ impl LlmProvider for AnthropicCompatProvider {
// 流式长生成任务会被误砍。改用 tokio::time::timeout
// 包裹 send().await,只管建连+首响应头,不管后续 body 读取(后续由 stream_llm idle timeout 兜底)。
// 60s 选型:正常 send(建连+收 200 headers)<5s,60s 足够宽容。
let send_future = self
.auth_headers(self.client.post(self.messages_url()))
.json(&body)
.version(reqwest::Version::HTTP_11)
.send();
let resp = match tokio::time::timeout(Duration::from_secs(60), send_future).await {
Ok(Ok(r)) => r,
Ok(Err(e)) => {
// 谓词结构化分类(对齐 openai 路径,共用 crate::send_error_class)。原 bail 串
// [timeout={} connect={}=...] 的字面词让一切错误(含 builder 错)侥幸可重试。
// 类词改由谓词决定置串首;谓词转储缩写为 t/c/b/r 防误触分类词表,source 链
// 保留真实传输词(reset/broken pipe 等)作串兜底。
let class = crate::send_error_class(crate::SendErrPreds::of(&e));
tracing::error!(
is_timeout = e.is_timeout(),
is_connect = e.is_connect(),
is_body = e.is_body(),
is_request = e.is_request(),
class = class,
url = %self.messages_url(),
source = ?std::error::Error::source(&e),
"Anthropic 流式 send 失败"
);
anyhow::bail!(
"send 失败({}): {} [t={} c={} b={} r={} src={:?}]",
class,
e,
e.is_timeout(),
e.is_connect(),
e.is_body(),
e.is_request(),
std::error::Error::source(&e)
);
}
Err(_elapsed) => {
// send 阶段超时(60s 未返回 HTTP 响应头):GLM 端点可能不可达或极慢
tracing::error!(
url = %self.messages_url(),
"Anthropic 流式 send 超时(60s 未返回响应头)"
);
anyhow::bail!("流式请求超时(60秒未收到 HTTP 响应,可能服务不可达或被防火墙拦截)");
// send 阶段弱网退避: connect 类失败(隧道断网/基站切换/DNS 抖动)请求未发出, 重试零副作用。
// 网络感知分速(net_probe 基准面): 本机断网进长等待按自适应间隔探测恢复(隧道级分钟级波动),
// 单点故障指数短退避 1s→8s; 10 分钟窗口耗尽才放弃。timeout/decode/HTTP 状态错不重试。
// 与 openai_compat 同构。
let resp = {
let start = std::time::Instant::now();
let mut offline_logged = false;
let mut offline_polls = 0usize;
let mut attempt = 0usize;
loop {
attempt += 1;
let send_future = self
.auth_headers(self.client.post(self.messages_url()))
.json(&body)
.version(reqwest::Version::HTTP_11)
.send();
let outcome = tokio::time::timeout(Duration::from_secs(60), send_future).await;
match outcome {
Ok(Ok(r)) => break r,
Ok(Err(e)) => {
// 谓词结构化分类(对齐 openai 路径,共用 crate::send_error_class)。原 bail 串
// [timeout={} connect={}=...] 的字面词让一切错误(含 builder 错)侥幸可重试。
// 类词改由谓词决定置串首;谓词转储缩写为 t/c/b/r 防误触分类词表,source 链
// 保留真实传输词(reset/broken pipe 等)作串兜底。
let preds = crate::SendErrPreds::of(&e);
let retryable = preds.is_connect || (preds.is_request && !preds.is_builder);
if retryable && start.elapsed() < crate::net_probe::SEND_RETRY_WINDOW {
// 本机断网: 等基准面恢复(自适应间隔), 不消耗退避档位
if !crate::net_probe::baseline_alive().await {
offline_polls += 1;
let wait = crate::net_probe::offline_probe_wait(offline_polls);
if !offline_logged {
warn!(
url = %self.messages_url(),
window_secs = crate::net_probe::SEND_RETRY_WINDOW.as_secs(),
"本机网络不可达(疑似隧道/电梯弱网), 按自适应间隔探测恢复, 恢复后立即重试"
);
offline_logged = true;
}
tokio::time::sleep(wait).await;
continue;
}
offline_polls = 0;
offline_logged = false;
let delay = Duration::from_millis(
1000u64.saturating_mul(1 << (attempt - 1).min(3)).min(8000),
);
warn!(
url = %self.messages_url(),
attempt,
delay_ms = delay.as_millis() as u64,
error = %e,
"流式 send 连接失败, 退避重试"
);
tokio::time::sleep(delay).await;
continue;
}
let class = crate::send_error_class(preds);
tracing::error!(
is_timeout = e.is_timeout(),
is_connect = e.is_connect(),
is_body = e.is_body(),
is_request = e.is_request(),
class = class,
url = %self.messages_url(),
source = ?std::error::Error::source(&e),
"Anthropic 流式 send 失败"
);
anyhow::bail!(
"send 失败({}): {} [t={} c={} b={} r={} src={:?}]",
class,
e,
e.is_timeout(),
e.is_connect(),
e.is_body(),
e.is_request(),
std::error::Error::source(&e)
);
}
Err(_elapsed) => {
// send 阶段超时(60s 未返回 HTTP 响应头):GLM 端点可能不可达或极慢
tracing::error!(
url = %self.messages_url(),
"Anthropic 流式 send 超时(60s 未返回响应头)"
);
anyhow::bail!("流式请求超时(60秒未收到 HTTP 响应,可能服务不可达或被防火墙拦截)");
}
}
}
};