mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-29 18:05:37 +08:00
fix(proxy): serialize per-app provider switches to prevent state corruption
Concurrent failover switches for the same app could cause is_current, local settings, and Live backup to point at different providers. - Add SwitchLockManager with per-app mutexes (different apps still parallel) - Unify scattered switch logic into ProxyService::hot_switch_provider - Fix TOCTOU in set_current_provider via mutate_settings - Add logical_target_changed to skip redundant UI refreshes - Add tests for serialization and restore-waits-for-switch scenarios
This commit is contained in:
@@ -2,15 +2,12 @@
|
||||
//!
|
||||
//! 处理故障转移成功后的供应商切换逻辑,包括:
|
||||
//! - 去重控制(避免多个请求同时触发)
|
||||
//! - 数据库更新
|
||||
//! - 托盘菜单更新
|
||||
//! - 前端事件发射
|
||||
//! - Live 备份更新
|
||||
|
||||
use crate::database::Database;
|
||||
use crate::error::AppError;
|
||||
use std::collections::HashSet;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use tauri::{Emitter, Manager};
|
||||
use tokio::sync::RwLock;
|
||||
@@ -98,30 +95,21 @@ impl FailoverSwitchManager {
|
||||
|
||||
log::info!("[FO-001] 切换: {app_type} → {provider_name}");
|
||||
|
||||
// 1. 更新数据库 is_current
|
||||
self.db.set_current_provider(app_type, provider_id)?;
|
||||
let mut switched = false;
|
||||
|
||||
// 2. 更新本地 settings(设备级)
|
||||
let app_type_enum = crate::app_config::AppType::from_str(app_type)
|
||||
.map_err(|_| AppError::Message(format!("无效的应用类型: {app_type}")))?;
|
||||
crate::settings::set_current_provider(&app_type_enum, Some(provider_id))?;
|
||||
|
||||
// 3. 更新托盘菜单和发射事件
|
||||
if let Some(app) = app_handle {
|
||||
// 更新托盘菜单
|
||||
if let Some(app_state) = app.try_state::<crate::store::AppState>() {
|
||||
// 更新 Live 备份(确保代理停止时恢复正确配置)
|
||||
if let Ok(Some(provider)) = self.db.get_provider_by_id(provider_id, app_type) {
|
||||
if let Err(e) = app_state
|
||||
.proxy_service
|
||||
.update_live_backup_from_provider(app_type, &provider)
|
||||
.await
|
||||
{
|
||||
log::warn!("[FO-003] Live 备份更新失败: {e}");
|
||||
}
|
||||
switched = app_state
|
||||
.proxy_service
|
||||
.hot_switch_provider(app_type, provider_id)
|
||||
.await
|
||||
.map_err(AppError::Message)?
|
||||
.logical_target_changed;
|
||||
|
||||
if !switched {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// 重建托盘菜单
|
||||
if let Ok(new_menu) = crate::tray::create_tray_menu(app, app_state.inner()) {
|
||||
if let Some(tray) = app.tray_by_id("main") {
|
||||
if let Err(e) = tray.set_menu(Some(new_menu)) {
|
||||
@@ -142,6 +130,6 @@ impl FailoverSwitchManager {
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
Ok(switched)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ pub mod response_processor;
|
||||
pub(crate) mod server;
|
||||
pub mod session;
|
||||
pub(crate) mod sse;
|
||||
pub(crate) mod switch_lock;
|
||||
pub mod thinking_budget_rectifier;
|
||||
pub mod thinking_optimizer;
|
||||
pub mod thinking_rectifier;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
//! Per-app switch lock
|
||||
//!
|
||||
//! 确保同一应用同时只有一个供应商切换操作在执行,
|
||||
//! 防止并发切换导致 is_current 与 Live 备份不一致。
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{Mutex, OwnedMutexGuard, RwLock};
|
||||
|
||||
/// 每个应用类型一把互斥锁,保证同一应用的切换操作串行执行。
|
||||
///
|
||||
/// 不同应用之间(如 Claude 和 Codex)可以并行切换。
|
||||
#[derive(Clone, Default)]
|
||||
pub struct SwitchLockManager {
|
||||
locks: Arc<RwLock<HashMap<String, Arc<Mutex<()>>>>>,
|
||||
}
|
||||
|
||||
impl SwitchLockManager {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// 获取指定应用的切换锁。
|
||||
///
|
||||
/// 返回 `OwnedMutexGuard`,持有期间同一 `app_type` 的其他切换会排队等待。
|
||||
pub async fn lock_for_app(&self, app_type: &str) -> OwnedMutexGuard<()> {
|
||||
let lock = {
|
||||
let locks = self.locks.read().await;
|
||||
if let Some(lock) = locks.get(app_type) {
|
||||
lock.clone()
|
||||
} else {
|
||||
drop(locks);
|
||||
let mut locks = self.locks.write().await;
|
||||
locks
|
||||
.entry(app_type.to_string())
|
||||
.or_insert_with(|| Arc::new(Mutex::new(())))
|
||||
.clone()
|
||||
}
|
||||
};
|
||||
lock.lock_owned().await
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user