perf(usage): coalesce session-sync notifications and serialize sync execution

Replace per-insert usage notifications with a single notification per
sync pass, guard all sync entry points (startup backfill, 60s loop,
manual sync) behind a process-wide single-flight tokio mutex, and move
blocking file/DB work onto spawn_blocking with MissedTickBehavior::Skip.

Extend SessionSyncResult with deferredFiles/suspectedDuplicates
observability fields (saturating aggregation) and add a test seam that
counts notifications even without an injected AppHandle.
This commit is contained in:
Jason
2026-07-20 12:15:32 +08:00
parent 01fca69641
commit eb105eae76
8 changed files with 148 additions and 96 deletions
+10 -44
View File
@@ -275,52 +275,18 @@ pub fn delete_model_pricing(state: State<'_, AppState>, model_id: String) -> Res
/// 手动触发会话日志同步
#[tauri::command]
pub fn sync_session_usage(
pub async fn sync_session_usage(
state: State<'_, AppState>,
) -> Result<crate::services::session_usage::SessionSyncResult, AppError> {
// 同步 Claude 会话日志
let mut result = crate::services::session_usage::sync_claude_session_logs(&state.db)?;
// 同步 Codex 使用数据
match crate::services::session_usage_codex::sync_codex_usage(&state.db) {
Ok(codex_result) => {
result.imported += codex_result.imported;
result.skipped += codex_result.skipped;
result.files_scanned += codex_result.files_scanned;
result.errors.extend(codex_result.errors);
}
Err(e) => {
result.errors.push(format!("Codex 同步失败: {e}"));
}
}
// 同步 Gemini 使用数据
match crate::services::session_usage_gemini::sync_gemini_usage(&state.db) {
Ok(gemini_result) => {
result.imported += gemini_result.imported;
result.skipped += gemini_result.skipped;
result.files_scanned += gemini_result.files_scanned;
result.errors.extend(gemini_result.errors);
}
Err(e) => {
result.errors.push(format!("Gemini 同步失败: {e}"));
}
}
// 同步 OpenCode 使用数据
match crate::services::session_usage_opencode::sync_opencode_usage(&state.db) {
Ok(opencode_result) => {
result.imported += opencode_result.imported;
result.skipped += opencode_result.skipped;
result.files_scanned += opencode_result.files_scanned;
result.errors.extend(opencode_result.errors);
}
Err(e) => {
result.errors.push(format!("OpenCode 同步失败: {e}"));
}
}
Ok(result)
let db = state.db.clone();
let _guard = crate::services::session_usage::session_sync_mutex()
.lock()
.await;
tauri::async_runtime::spawn_blocking(move || {
crate::services::session_usage::sync_all_unlocked(&db)
})
.await
.map_err(|error| AppError::Message(format!("会话用量同步任务失败: {error}")))
}
/// 获取数据来源分布