Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 58 additions & 1 deletion src-tauri/src/proxy/provider_router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ use crate::app_config::AppType;
use crate::database::Database;
use crate::error::AppError;
use crate::provider::Provider;
use crate::proxy::circuit_breaker::{AllowResult, CircuitBreaker, CircuitBreakerConfig};
use crate::proxy::circuit_breaker::{AllowResult, CircuitBreaker, CircuitBreakerConfig, CircuitState};
use crate::proxy::failover_switch::FailoverSwitchManager;
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::Arc;
Expand All @@ -18,6 +19,8 @@ pub struct ProviderRouter {
db: Arc<Database>,
/// 熔断器管理器 - key 格式: "app_type:provider_id"
circuit_breakers: Arc<RwLock<HashMap<String, Arc<CircuitBreaker>>>>,
/// 自动回切管理器(由 ProxyServer 构造时注入)
failover_switch_manager: Arc<RwLock<Option<Arc<FailoverSwitchManager>>>>,
}

impl ProviderRouter {
Expand All @@ -26,9 +29,19 @@ impl ProviderRouter {
Self {
db,
circuit_breakers: Arc::new(RwLock::new(HashMap::new())),
failover_switch_manager: Arc::new(RwLock::new(None)),
}
}

/// 设置自动回切管理器
pub fn set_failover_switch_manager(&self, mgr: Arc<FailoverSwitchManager>) {
// fire-and-forget,settle 后才正式启用
let mgr_ref = self.failover_switch_manager.clone();
tokio::spawn(async move {
*mgr_ref.write().await = Some(mgr);
});
}

/// 选择可用的供应商(支持故障转移)
///
/// 返回按优先级排序的可用供应商列表:
Expand Down Expand Up @@ -143,6 +156,50 @@ impl ProviderRouter {

if success {
breaker.record_success(used_half_open_permit).await;

// [FO-BACK] 自动回切:当前用的是 P2+,且 P1 已从熔断恢复
'failback: {
if let Some(ref mgr) = *self.failover_switch_manager.read().await {
let ordered_ids: Vec<String> = match self.db.get_failover_queue(app_type) {
Ok(ids) => ids.into_iter().map(|item| item.provider_id).collect(),
Err(_) => Vec::new(),
};

if let Some(p1_id) = ordered_ids.first() {
if provider_id != *p1_id {
// 当前不在 P1,检查 P1 是否已恢复为 Closed
let p1_circuit_key = format!("{app_type}:{}", p1_id);
let p1_breaker = self.get_or_create_circuit_breaker(&p1_circuit_key).await;
if p1_breaker.get_state().await == CircuitState::Closed {
let all = match self.db.get_all_providers(app_type) {
Ok(p) => p,
Err(e) => {
log::warn!(
"[FO-BACK] get_all_providers 失败,跳过本次回切: {}",
e
);
break 'failback;
}
};
if let Some(p1) = all.get(p1_id) {
log::info!(
"[FO-BACK] P1 {} 恢复为 Closed(当前: {}),触发自动回切",
p1.name,
provider_id
);
let fm = mgr.clone();
let pid = p1.id.clone();
let pname = p1.name.clone();
let at = app_type.to_string();
tokio::spawn(async move {
let _ = fm.try_switch(None, &at, &pid, &pname).await;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pass AppHandle when triggering failback switch

The new failback path calls try_switch with None, so the switch manager never executes hot_switch_provider or emits UI updates (those only run inside if let Some(app) in failover_switch.rs). In the recovery scenario this commit targets (traffic currently on P2, P1 circuit recovered), the spawned task becomes a no-op and the provider/UI state does not actually fail back to P1.

Useful? React with 👍 / 👎.

});
}
}
}
}
}
}
} else {
breaker.record_failure(used_half_open_permit).await;
}
Expand Down
2 changes: 2 additions & 0 deletions src-tauri/src/proxy/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ impl ProxyServer {
let provider_router = Arc::new(ProviderRouter::new(db.clone()));
// 创建故障转移切换管理器
let failover_manager = Arc::new(FailoverSwitchManager::new(db.clone()));
// [FO-BACK] 注入回切管理器,使 record_result 能触发自动回切
provider_router.set_failover_switch_manager(failover_manager.clone());

let state = ProxyState {
db,
Expand Down
Loading