refactor(proxy): remove is_proxy_target in favor of failover_queue

- Remove `is_proxy_target` field from Provider struct (Rust & TypeScript)
- Remove related DAO methods: get_proxy_target_provider, set_proxy_target
- Remove deprecated Tauri commands: get_proxy_targets, set_proxy_target
- Add `is_available()` method to CircuitBreaker for availability checks
  without consuming HalfOpen probe permits (used in select_providers)
- Keep `allow_request()` for actual request gating with permit tracking
- Update stream_check to use failover_queue instead of is_proxy_target
- Clean up commented-out reset circuit breaker button in ProviderActions
- Remove unused useProxyTargets and useSetProxyTarget hooks
This commit is contained in:
Jason
2025-12-16 15:45:15 +08:00
parent d4f33224c6
commit e6654bd7f9
37 changed files with 348 additions and 564 deletions
+30
View File
@@ -99,6 +99,36 @@ impl CircuitBreaker {
log::debug!("Circuit breaker config updated");
}
/// 判断当前 Provider 是否“可被纳入候选链路”
///
/// 这个方法不会占用 HalfOpen 探测名额,仅用于路由选择阶段的“可用性判断”:
/// - Closed / HalfOpen:可用(返回 true
/// - Open:若超时到达则切到 HalfOpen 并返回 true,否则返回 false
///
/// 注意:真正发起请求前仍需调用 `allow_request()` 来获取 HalfOpen 探测名额,
/// 并在请求结束后通过 `record_success()` / `record_failure()` 释放。
pub async fn is_available(&self) -> bool {
let state = *self.state.read().await;
let config = self.config.read().await;
match state {
CircuitState::Closed | CircuitState::HalfOpen => true,
CircuitState::Open => {
if let Some(opened_at) = *self.last_opened_at.read().await {
if opened_at.elapsed().as_secs() >= config.timeout_seconds {
drop(config); // 释放读锁再转换状态
log::info!(
"Circuit breaker transitioning from Open to HalfOpen (timeout reached)"
);
self.transition_to_half_open().await;
return true;
}
}
false
}
}
}
/// 检查是否允许请求通过
pub async fn allow_request(&self) -> bool {
let state = *self.state.read().await;
+37 -5
View File
@@ -148,13 +148,33 @@ impl RequestForwarder {
let mut last_error = None;
let mut failover_happened = false;
let mut attempted_providers = 0usize;
// 依次尝试每个供应商
for (attempt, provider) in providers.iter().enumerate() {
for provider in providers.iter() {
// 发起请求前先获取熔断器放行许可(HalfOpen 会占用探测名额)
if !self
.router
.allow_provider_request(&provider.id, app_type_str)
.await
{
log::debug!(
"[{}] Provider {} 熔断器拒绝本次请求,跳过",
app_type_str,
provider.name
);
continue;
}
attempted_providers += 1;
if attempted_providers > 1 {
failover_happened = true;
}
log::info!(
"[{}] 尝试 {}/{} - 使用Provider: {} (sort_index: {})",
app_type_str,
attempt + 1,
attempted_providers,
providers.len(),
provider.name,
provider.sort_index.unwrap_or(999999)
@@ -167,9 +187,6 @@ impl RequestForwarder {
status.current_provider_id = Some(provider.id.clone());
status.total_requests += 1;
status.last_request_at = Some(chrono::Utc::now().to_rfc3339());
if attempt > 0 {
failover_happened = true;
}
}
let start = Instant::now();
@@ -306,6 +323,21 @@ impl RequestForwarder {
}
}
if attempted_providers == 0 {
// providers 列表非空,但全部被熔断器拒绝(典型:HalfOpen 探测名额被占用)
{
let mut status = self.status.write().await;
status.failed_requests += 1;
status.last_error = Some("所有供应商暂时不可用(熔断器限制)".to_string());
if status.total_requests > 0 {
status.success_rate = (status.success_requests as f32
/ status.total_requests as f32)
* 100.0;
}
}
return Err(ProxyError::NoAvailableProvider);
}
// 所有供应商都失败了
{
let mut status = self.status.write().await;
+57 -20
View File
@@ -43,7 +43,7 @@ impl ProviderRouter {
let circuit_key = format!("{}:{}", app_type, current.id);
let breaker = self.get_or_create_circuit_breaker(&circuit_key).await;
if breaker.allow_request().await {
if breaker.is_available().await {
log::info!(
"[{}] Current provider available: {} ({})",
app_type,
@@ -81,7 +81,7 @@ impl ProviderRouter {
let circuit_key = format!("{}:{}", app_type, provider.id);
let breaker = self.get_or_create_circuit_breaker(&circuit_key).await;
if breaker.allow_request().await {
if breaker.is_available().await {
log::info!(
"[{}] Failover provider available: {} ({}) at queue position {}",
app_type,
@@ -116,6 +116,20 @@ impl ProviderRouter {
Ok(result)
}
/// 请求执行前获取熔断器“放行许可”
///
/// - Closed:直接放行
/// - Open:超时到达后切到 HalfOpen 并放行一次探测
/// - HalfOpen:按限流规则放行探测
///
/// 注意:调用方必须在请求结束后通过 `record_result()` 释放 HalfOpen 名额,
/// 否则会导致该 Provider 长时间无法进入探测状态。
pub async fn allow_provider_request(&self, provider_id: &str, app_type: &str) -> bool {
let circuit_key = format!("{app_type}:{provider_id}");
let breaker = self.get_or_create_circuit_breaker(&circuit_key).await;
breaker.allow_request().await
}
/// 记录供应商请求结果
pub async fn record_result(
&self,
@@ -155,24 +169,6 @@ impl ProviderRouter {
)
.await?;
// 4. 如果连续失败达到熔断阈值,自动禁用代理目标
if !success {
let health = self.db.get_provider_health(provider_id, app_type).await?;
// 如果连续失败达到阈值,自动关闭该供应商的代理开关
if health.consecutive_failures >= failure_threshold {
log::warn!(
"Provider {} has failed {} times (threshold: {}), auto-disabling proxy target",
provider_id,
health.consecutive_failures,
failure_threshold
);
self.db
.set_proxy_target(provider_id, app_type, false)
.await?;
}
}
Ok(())
}
@@ -261,6 +257,7 @@ impl ProviderRouter {
mod tests {
use super::*;
use crate::database::Database;
use serde_json::json;
#[tokio::test]
async fn test_provider_router_creation() {
@@ -271,4 +268,44 @@ mod tests {
let breaker = router.get_or_create_circuit_breaker("claude:test").await;
assert!(breaker.allow_request().await);
}
#[tokio::test]
async fn select_providers_does_not_consume_half_open_permit() {
let db = Arc::new(Database::memory().unwrap());
// 配置:让熔断器 Open 后立刻进入 HalfOpentimeout_seconds=0),并用 1 次失败就打开熔断器
db.update_circuit_breaker_config(&CircuitBreakerConfig {
failure_threshold: 1,
timeout_seconds: 0,
..Default::default()
})
.await
.unwrap();
// 准备 2 个 ProviderA(当前)+ B(队列)
let provider_a =
Provider::with_id("a".to_string(), "Provider A".to_string(), json!({}), None);
let provider_b =
Provider::with_id("b".to_string(), "Provider B".to_string(), json!({}), None);
db.save_provider("claude", &provider_a).unwrap();
db.save_provider("claude", &provider_b).unwrap();
db.set_current_provider("claude", "a").unwrap();
db.add_to_failover_queue("claude", "b").unwrap();
let router = ProviderRouter::new(db.clone());
// 让 B 进入 Open 状态(failure_threshold=1
router
.record_result("b", "claude", false, Some("fail".to_string()))
.await
.unwrap();
// select_providers 只做“可用性判断”,不应占用 HalfOpen 探测名额
let providers = router.select_providers("claude").await.unwrap();
assert_eq!(providers.len(), 2);
// 如果 select_providers 错误地消耗了 HalfOpen 名额,这里会返回 false(被限流拒绝)
assert!(router.allow_provider_request("b", "claude").await);
}
}
-1
View File
@@ -253,7 +253,6 @@ mod tests {
meta: None,
icon: None,
icon_color: None,
is_proxy_target: None,
}
}
-1
View File
@@ -174,7 +174,6 @@ mod tests {
meta: None,
icon: None,
icon_color: None,
is_proxy_target: None,
}
}
-1
View File
@@ -250,7 +250,6 @@ mod tests {
meta: None,
icon: None,
icon_color: None,
is_proxy_target: None,
}
}
-1
View File
@@ -205,7 +205,6 @@ mod tests {
meta: None,
icon: None,
icon_color: None,
is_proxy_target: None,
}
}
@@ -394,7 +394,6 @@ mod tests {
meta: None,
icon: None,
icon_color: None,
is_proxy_target: None,
}
}