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
+25 -42
View File
@@ -1214,59 +1214,42 @@ pub fn run() {
tauri::async_runtime::spawn(async move {
const SESSION_SYNC_INTERVAL_SECS: u64 = 60;
fn run_step<T>(name: &str, result: Result<T, crate::error::AppError>) {
if let Err(e) = result {
log::warn!("{name} failed: {e}");
async fn run_session_sync(db: std::sync::Arc<crate::database::Database>, backfill: bool) {
let _guard = crate::services::session_usage::session_sync_mutex()
.lock()
.await;
let task = tauri::async_runtime::spawn_blocking(move || {
if backfill {
if let Err(error) = db.backfill_missing_usage_costs() {
log::warn!("Usage cost startup backfill failed: {error}");
}
}
crate::services::session_usage::sync_all_unlocked(&db)
});
match task.await {
Ok(result) if !result.errors.is_empty() => {
log::warn!(
"Session usage sync completed with {} error(s)",
result.errors.len()
);
}
Ok(_) => {}
Err(error) => log::warn!("Session usage blocking task failed: {error}"),
}
}
let db = &db_for_session_sync;
// 首次同步
run_step(
"Usage cost startup backfill",
db.backfill_missing_usage_costs(),
);
run_step(
"Session usage initial sync",
crate::services::session_usage::sync_claude_session_logs(db),
);
run_step(
"Codex usage initial sync",
crate::services::session_usage_codex::sync_codex_usage(db),
);
run_step(
"Gemini usage initial sync",
crate::services::session_usage_gemini::sync_gemini_usage(db),
);
run_step(
"OpenCode usage initial sync",
crate::services::session_usage_opencode::sync_opencode_usage(db),
);
// 首次同步(含费用回填)
run_session_sync(db_for_session_sync.clone(), true).await;
// 定期同步
let mut interval = tokio::time::interval(std::time::Duration::from_secs(
SESSION_SYNC_INTERVAL_SECS,
));
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
interval.tick().await; // skip immediate first tick
loop {
interval.tick().await;
run_step(
"Session usage periodic sync",
crate::services::session_usage::sync_claude_session_logs(db),
);
run_step(
"Codex usage periodic sync",
crate::services::session_usage_codex::sync_codex_usage(db),
);
run_step(
"Gemini usage periodic sync",
crate::services::session_usage_gemini::sync_gemini_usage(db),
);
run_step(
"OpenCode usage periodic sync",
crate::services::session_usage_opencode::sync_opencode_usage(db),
);
run_session_sync(db_for_session_sync.clone(), false).await;
}
});
});