diff --git a/src/library_maintenance.rs b/src/library_maintenance.rs index ffa87bb..a4f44b9 100644 --- a/src/library_maintenance.rs +++ b/src/library_maintenance.rs @@ -156,6 +156,25 @@ pub fn detect_missing_files_for_library( (deleted, next_offset) } +/// Re-gather query-planner statistics (`sqlite_stat1`). +/// +/// The migrations run `ANALYZE` once, but the stats never refresh after +/// that. A snapshot taken while `image_exif.content_hash` was all-NULL +/// told the planner every hash matches every row, so the correlated +/// `content_hash = ...` subqueries in [`refresh_back_refs`] scanned +/// the index instead of seeking it: ~20s per tick holding the write +/// lock, starving chat persistence past `busy_timeout`. With fresh +/// stats the same pass takes ~0.1s. `PRAGMA optimize` doesn't catch +/// this on SQLite 3.45 (verified), hence a plain `ANALYZE`. It costs +/// well under a second at current scale, so run it on full-scan ticks. +pub fn refresh_planner_stats(conn: &mut SqliteConnection) { + let started = std::time::Instant::now(); + match sql_query("ANALYZE").execute(conn) { + Ok(_) => debug!("ANALYZE completed in {:?}", started.elapsed()), + Err(e) => warn!("ANALYZE failed: {:?}", e), + } +} + /// Refresh the `(library_id, rel_path)` back-refs on hash-keyed /// tables. A back-ref is stale when: /// - its `content_hash` is non-null, diff --git a/src/utils.rs b/src/utils.rs index 4e8ba3f..6439d2a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -27,7 +27,7 @@ where Err(e) => e, }; for attempt in 1..=max_retries { - let base = Duration::from_millis(100).saturating_mul(1_u32.pow(attempt - 1)); + let base = Duration::from_millis(100).saturating_mul(2_u32.pow(attempt - 1)); let jitter_range = base.as_millis() as f64 * 0.25; let jitter = rand::thread_rng().gen_range(-jitter_range..=jitter_range) as u128; let delay = base.as_millis() as i128 + jitter as i128; @@ -92,7 +92,7 @@ where ); break; } - let base = Duration::from_millis(100).saturating_mul(1_u32.pow(attempt - 1)); + let base = Duration::from_millis(100).saturating_mul(2_u32.pow(attempt - 1)); let jitter_range = base.as_millis() as f64 * 0.25; let jitter = rand::thread_rng().gen_range(-jitter_range..=jitter_range) as u128; let delay = base.as_millis() as i128 + jitter as i128; diff --git a/src/watcher.rs b/src/watcher.rs index 39127ca..6d2a727 100644 --- a/src/watcher.rs +++ b/src/watcher.rs @@ -577,6 +577,9 @@ pub fn watch_files( // model" for the rules. { let mut conn = image_api::database::connect(); + if is_full_scan { + library_maintenance::refresh_planner_stats(&mut conn); + } let _ = image_api::database::reconcile::run(&mut conn); // Back-ref refresh: hash-keyed rows whose