fix(proxy): move client-request counters out of per-attempt loop

Three statistics-shape issues fixed together so the dashboard reflects
client requests, not provider attempts:

1. active_connections never moved off zero — the field had no caller in
   the entire crate. Wrap forward_with_retry into a thin entry point
   that saturating_add(1) on enter and saturating_sub(1) on exit; every
   inner return path is covered automatically.

2. total_requests counted attempts, not requests. A single client call
   that failed over P1 -> P2 -> success was recorded as
   total=2 / success=1 -> 50% success rate. Move the increment and the
   last_request_at refresh into the wrapper so they fire once per
   client request regardless of how many providers were tried.

3. current_provider / current_provider_id stay inside the inner loop
   because they are intentionally per-attempt ("what am I trying right
   now?") — moving them would break the live-failover indicator.

Refactor: split forward_with_retry into a public wrapper + private
forward_with_retry_inner. Every existing `return Err(...)` inside inner
remains correct because the wrapper always runs the decrement on its
return.
This commit is contained in:
Jason
2026-05-13 23:28:16 +08:00
parent b06e0fa538
commit f2ae9823cb
+37 -4
View File
@@ -156,13 +156,44 @@ impl RequestForwarder {
/// 转发请求(带故障转移)
///
/// 这是 thin wrapper:在客户端请求维度记一次 `total_requests` / 调整
/// `active_connections` / 刷新 `last_request_at`,无论 inner 走哪条出口路径,
/// 出口处都会把 `active_connections` 回收。Per-attempt 维度(成功/失败/熔断
/// 等)仍由 inner 内自行更新 `success_requests` / `failed_requests`。
pub async fn forward_with_retry(
&self,
app_type: &AppType,
endpoint: &str,
body: Value,
headers: axum::http::HeaderMap,
extensions: Extensions,
providers: Vec<Provider>,
) -> Result<ForwardResult, ForwardError> {
{
let mut s = self.status.write().await;
s.total_requests = s.total_requests.saturating_add(1);
s.active_connections = s.active_connections.saturating_add(1);
s.last_request_at = Some(chrono::Utc::now().to_rfc3339());
}
let result = self
.forward_with_retry_inner(app_type, endpoint, body, headers, extensions, providers)
.await;
{
let mut s = self.status.write().await;
s.active_connections = s.active_connections.saturating_sub(1);
}
result
}
/// 实际转发逻辑(不包含客户端维度的入口/出口计数)
///
/// # Arguments
/// * `app_type` - 应用类型
/// * `endpoint` - API 端点
/// * `body` - 请求体
/// * `headers` - 请求头
/// * `providers` - 已选择的 Provider 列表(由 RequestContext 提供,避免重复调用 select_providers
pub async fn forward_with_retry(
async fn forward_with_retry_inner(
&self,
app_type: &AppType,
endpoint: &str,
@@ -240,13 +271,15 @@ impl RequestForwarder {
attempted_providers += 1;
// 更新状态中的当前Provider信息
// 更新状态中的当前 Provider 信息per-attempt 维度的标识)
//
// total_requests / last_request_at / active_connections 已由
// forward_with_retry wrapper 在客户端请求维度统一处理,这里只刷
// 新「正在尝试哪个 provider」的展示字段。
{
let mut status = self.status.write().await;
status.current_provider = Some(provider.name.clone());
status.current_provider_id = Some(provider.id.clone());
status.total_requests += 1;
status.last_request_at = Some(chrono::Utc::now().to_rfc3339());
}
// 转发请求(每个 Provider 只尝试一次,重试由客户端控制)