diff --git a/src-tauri/src/commands/database.rs b/src-tauri/src/commands/database.rs index ec92403..a502e07 100644 --- a/src-tauri/src/commands/database.rs +++ b/src-tauri/src/commands/database.rs @@ -749,13 +749,38 @@ async fn current_remote_and_local_from_state( return Ok(None); } - let local_path = sqlite_db_path(app_handle)?; - let local_conn = create_sqlite_connection(&local_path) - .await - .map_err(|e| e.to_string())?; - run_migration(&local_conn, DatabaseType::SQLite) - .await - .map_err(|e| e.to_string())?; + // 优先复用启动时缓存的本地 SQLite 连接;缓存未命中时再新建并回填。 + // 这样 realtime_dual_write_sync 不会在每次写命令后都新建一个 sqlx 连接池, + // 避免“pool timed out while waiting for an open connection”式池耗尽。 + let local_conn = { + // 先把 Arc 克隆出来,让 state_guard 尽早释放,再 read 缓存。 + let local_sqlite = { + let state_guard = app_state.read(); + state_guard.local_sqlite.clone() + }; + let cloned = local_sqlite.read().clone(); + cloned + }; + + let local_conn = match local_conn { + Some(conn) => conn, + None => { + let local_path = sqlite_db_path(app_handle)?; + let conn = create_sqlite_connection(&local_path) + .await + .map_err(|e| e.to_string())?; + run_migration(&conn, DatabaseType::SQLite) + .await + .map_err(|e| e.to_string())?; + // 回填缓存(best-effort,竞争时仅最后写入者生效,两者都有效)。 + let state_guard = app_state.read(); + let mut local_guard = state_guard.local_sqlite.write(); + if local_guard.is_none() { + *local_guard = Some(conn.clone()); + } + conn + } + }; Ok(Some((local_conn, remote_conn))) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0856ff6..ba210d6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -225,6 +225,9 @@ fn setup_database(app: &mut App) -> Result<(), Box> { let state = handle.state::(); let state_guard = state.write(); let mut active_conn = sqlite_conn.clone(); + // 缓存本地 SQLite 连接,供 realtime_dual_write_sync 复用, + // 避免每次写命令都新建连接池。 + *state_guard.local_sqlite.write() = Some(sqlite_conn.clone()); { let mut settings = state_guard.settings.write(); diff --git a/src-tauri/src/services/auto_score.rs b/src-tauri/src/services/auto_score.rs index cecd4af..6c6b51c 100644 --- a/src-tauri/src/services/auto_score.rs +++ b/src-tauri/src/services/auto_score.rs @@ -1858,7 +1858,7 @@ async fn execute_rule( for mut student in target_students { if mode == ExecutionMode::Normal { if !is_student_pass_cooldown( - conn, + &txn, execution_batches, rule, student.id, @@ -2368,8 +2368,8 @@ async fn resolve_target_students( Ok(matched) } -async fn is_student_pass_cooldown( - conn: &DatabaseConnection, +async fn is_student_pass_cooldown( + conn: &C, execution_batches: &[AutoScoreExecutionBatch], rule: &AutoScoreRule, student_id: i32, diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 76e29db..34fda73 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -12,6 +12,9 @@ use crate::services::{ pub struct AppState { pub db: Arc>>, + /// 缓存的本地 SQLite 连接(启动时建立),供 `realtime_dual_write_sync` 等复用, + /// 避免每次写操作都新建 sqlx 连接池导致池耗尽 / 文件句柄残留。 + pub local_sqlite: Arc>>, pub settings: Arc>, pub security: Arc>, pub permissions: Arc>, @@ -37,6 +40,7 @@ impl AppState { let data = Arc::new(RwLock::new(DataService::new())); let plugins = Arc::new(RwLock::new(PluginService::new())); let db = Arc::new(RwLock::new(None)); + let local_sqlite = Arc::new(RwLock::new(None)); let http_client = Client::builder() .timeout(std::time::Duration::from_secs(30)) @@ -45,6 +49,7 @@ impl AppState { Self { db, + local_sqlite, settings, security, permissions,