refactor(backend): phase 5 - optimize concurrency with RwLock and async IO
Replace Mutex with RwLock for AppState.config to enable concurrent reads, improving performance for tray menu building and query operations that previously blocked each other unnecessarily. Key changes: - Migrate AppState.config from Mutex<MultiAppConfig> to RwLock<MultiAppConfig> - Distinguish read-only operations (read()) from mutations (write()) across all command handlers and service layers - Offload blocking file I/O in import/export commands to spawn_blocking threads, minimizing lock hold time and preventing main thread blocking - Extract load_config_for_import() to separate I/O logic from state updates - Update all integration tests to use RwLock semantics Performance impact: - Concurrent reads: Multiple threads can now query config simultaneously (tray menu, provider list, MCP config) - Reduced contention: Write locks only acquired during actual mutations - Non-blocking I/O: Config import/export no longer freezes UI thread All existing tests pass with new locking semantics.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
use serde_json::json;
|
||||
use std::{fs, path::Path, sync::Mutex};
|
||||
use std::{fs, path::Path, sync::RwLock};
|
||||
use tauri::async_runtime;
|
||||
|
||||
use cc_switch_lib::{
|
||||
@@ -728,7 +728,7 @@ fn import_config_from_path_overwrites_state_and_creates_backup() {
|
||||
.expect("write import file");
|
||||
|
||||
let app_state = AppState {
|
||||
config: Mutex::new(MultiAppConfig::default()),
|
||||
config: RwLock::new(MultiAppConfig::default()),
|
||||
};
|
||||
|
||||
let backup_id =
|
||||
@@ -757,7 +757,7 @@ fn import_config_from_path_overwrites_state_and_creates_backup() {
|
||||
"saved config should record new current provider"
|
||||
);
|
||||
|
||||
let guard = app_state.config.lock().expect("lock state after import");
|
||||
let guard = app_state.config.read().expect("lock state after import");
|
||||
let claude_manager = guard
|
||||
.get_manager(&AppType::Claude)
|
||||
.expect("claude manager in state");
|
||||
@@ -784,7 +784,7 @@ fn import_config_from_path_invalid_json_returns_error() {
|
||||
fs::write(&invalid_path, "{ not-json ").expect("write invalid json");
|
||||
|
||||
let app_state = AppState {
|
||||
config: Mutex::new(MultiAppConfig::default()),
|
||||
config: RwLock::new(MultiAppConfig::default()),
|
||||
};
|
||||
|
||||
let err = import_config_from_path(&invalid_path, &app_state).expect_err("import should fail");
|
||||
@@ -802,7 +802,7 @@ fn import_config_from_path_missing_file_produces_io_error() {
|
||||
|
||||
let missing_path = Path::new("/nonexistent/import.json");
|
||||
let app_state = AppState {
|
||||
config: Mutex::new(MultiAppConfig::default()),
|
||||
config: RwLock::new(MultiAppConfig::default()),
|
||||
};
|
||||
|
||||
let err = import_config_from_path(missing_path, &app_state)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::fs;
|
||||
use std::{fs, sync::RwLock};
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
@@ -37,14 +37,14 @@ fn import_default_config_claude_persists_provider() {
|
||||
let mut config = MultiAppConfig::default();
|
||||
config.ensure_app(&AppType::Claude);
|
||||
let state = AppState {
|
||||
config: std::sync::Mutex::new(config),
|
||||
config: RwLock::new(config),
|
||||
};
|
||||
|
||||
import_default_config_test_hook(&state, AppType::Claude)
|
||||
.expect("import default config succeeds");
|
||||
|
||||
// 验证内存状态
|
||||
let guard = state.config.lock().expect("lock config");
|
||||
let guard = state.config.read().expect("lock config");
|
||||
let manager = guard
|
||||
.get_manager(&AppType::Claude)
|
||||
.expect("claude manager present");
|
||||
@@ -71,7 +71,7 @@ fn import_default_config_without_live_file_returns_error() {
|
||||
let home = ensure_test_home();
|
||||
|
||||
let state = AppState {
|
||||
config: std::sync::Mutex::new(MultiAppConfig::default()),
|
||||
config: RwLock::new(MultiAppConfig::default()),
|
||||
};
|
||||
|
||||
let err = import_default_config_test_hook(&state, AppType::Claude)
|
||||
@@ -113,7 +113,7 @@ fn import_mcp_from_claude_creates_config_and_enables_servers() {
|
||||
.expect("seed ~/.claude.json");
|
||||
|
||||
let state = AppState {
|
||||
config: std::sync::Mutex::new(MultiAppConfig::default()),
|
||||
config: RwLock::new(MultiAppConfig::default()),
|
||||
};
|
||||
|
||||
let changed =
|
||||
@@ -123,7 +123,7 @@ fn import_mcp_from_claude_creates_config_and_enables_servers() {
|
||||
"import should report inserted or normalized entries"
|
||||
);
|
||||
|
||||
let guard = state.config.lock().expect("lock config");
|
||||
let guard = state.config.read().expect("lock config");
|
||||
let claude_servers = &guard.mcp.claude.servers;
|
||||
let entry = claude_servers
|
||||
.get("echo")
|
||||
@@ -155,7 +155,7 @@ fn import_mcp_from_claude_invalid_json_preserves_state() {
|
||||
.expect("seed invalid ~/.claude.json");
|
||||
|
||||
let state = AppState {
|
||||
config: std::sync::Mutex::new(MultiAppConfig::default()),
|
||||
config: RwLock::new(MultiAppConfig::default()),
|
||||
};
|
||||
|
||||
let err =
|
||||
@@ -197,13 +197,13 @@ fn set_mcp_enabled_for_codex_writes_live_config() {
|
||||
);
|
||||
|
||||
let state = AppState {
|
||||
config: std::sync::Mutex::new(config),
|
||||
config: RwLock::new(config),
|
||||
};
|
||||
|
||||
set_mcp_enabled_test_hook(&state, AppType::Codex, "codex-server", true)
|
||||
.expect("set enabled should succeed");
|
||||
|
||||
let guard = state.config.lock().expect("lock config");
|
||||
let guard = state.config.read().expect("lock config");
|
||||
let entry = guard
|
||||
.mcp
|
||||
.codex
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use serde_json::json;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use cc_switch_lib::{
|
||||
get_codex_auth_path, get_codex_config_path, read_json_file, switch_provider_test_hook,
|
||||
@@ -71,7 +72,7 @@ command = "say"
|
||||
);
|
||||
|
||||
let app_state = AppState {
|
||||
config: std::sync::Mutex::new(config),
|
||||
config: RwLock::new(config),
|
||||
};
|
||||
|
||||
switch_provider_test_hook(&app_state, AppType::Codex, "new-provider")
|
||||
@@ -94,7 +95,7 @@ command = "say"
|
||||
"config.toml should contain synced MCP servers"
|
||||
);
|
||||
|
||||
let locked = app_state.config.lock().expect("lock config after switch");
|
||||
let locked = app_state.config.read().expect("lock config after switch");
|
||||
let manager = locked
|
||||
.get_manager(&AppType::Codex)
|
||||
.expect("codex manager after switch");
|
||||
@@ -142,7 +143,7 @@ fn switch_provider_missing_provider_returns_error() {
|
||||
.current = "does-not-exist".to_string();
|
||||
|
||||
let app_state = AppState {
|
||||
config: std::sync::Mutex::new(config),
|
||||
config: RwLock::new(config),
|
||||
};
|
||||
|
||||
let err = switch_provider_test_hook(&app_state, AppType::Claude, "missing-provider")
|
||||
@@ -210,7 +211,7 @@ fn switch_provider_updates_claude_live_and_state() {
|
||||
}
|
||||
|
||||
let app_state = AppState {
|
||||
config: std::sync::Mutex::new(config),
|
||||
config: RwLock::new(config),
|
||||
};
|
||||
|
||||
switch_provider_test_hook(&app_state, AppType::Claude, "new-provider")
|
||||
@@ -227,7 +228,7 @@ fn switch_provider_updates_claude_live_and_state() {
|
||||
"live settings.json should reflect new provider auth"
|
||||
);
|
||||
|
||||
let locked = app_state.config.lock().expect("lock config after switch");
|
||||
let locked = app_state.config.read().expect("lock config after switch");
|
||||
let manager = locked
|
||||
.get_manager(&AppType::Claude)
|
||||
.expect("claude manager after switch");
|
||||
@@ -304,7 +305,7 @@ fn switch_provider_codex_missing_auth_returns_error_and_keeps_state() {
|
||||
}
|
||||
|
||||
let app_state = AppState {
|
||||
config: std::sync::Mutex::new(config),
|
||||
config: RwLock::new(config),
|
||||
};
|
||||
|
||||
let err = switch_provider_test_hook(&app_state, AppType::Codex, "invalid")
|
||||
@@ -317,7 +318,7 @@ fn switch_provider_codex_missing_auth_returns_error_and_keeps_state() {
|
||||
other => panic!("expected config error, got {other:?}"),
|
||||
}
|
||||
|
||||
let locked = app_state.config.lock().expect("lock config after failure");
|
||||
let locked = app_state.config.read().expect("lock config after failure");
|
||||
let manager = locked.get_manager(&AppType::Codex).expect("codex manager");
|
||||
assert!(
|
||||
manager.current.is_empty(),
|
||||
|
||||
Reference in New Issue
Block a user