Skip to content

Commit 72ca9fb

Browse files
committed
Bugfix: Verifier + replay transaction conflict
The verifier fired during cold-start replay, causing two failures: `upsert_dir_stats_by_id` hit "cannot start a transaction within a transaction" (replay's `BEGIN IMMEDIATE` was active), and `scan_subtree` couldn't see uncommitted writes via WAL isolation. Result: dirs created without `dir_stats` rows → wrong sizes. - Set `scanning = true` at the top of `resume_or_scan()` so the verifier is suppressed during both replay and full scan, not just full scan - Reset `scanning = false` after `run_replay_event_loop` returns (live mode starts) - Replace `unchecked_transaction()` with named savepoints (`SAVEPOINT`/`RELEASE`) in `insert_entries_v2_batch` and `upsert_dir_stats_by_id` — savepoints nest correctly inside explicit transactions, unlike `BEGIN` which fails
1 parent af2bf7a commit 72ca9fb

2 files changed

Lines changed: 45 additions & 24 deletions

File tree

apps/desktop/src-tauri/src/indexing/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,11 @@ impl IndexManager {
266266
///
267267
/// **No existing index:** Full scan via `start_scan()`.
268268
pub fn resume_or_scan(&mut self) -> Result<(), String> {
269+
// Suppress verifier and other concurrent reads until replay/scan completes.
270+
// start_scan() sets this again (harmless), and both scan and replay
271+
// completion paths reset it to false.
272+
self.scanning.store(true, Ordering::Relaxed);
273+
269274
let status = self
270275
.store
271276
.get_index_status()
@@ -379,6 +384,7 @@ impl IndexManager {
379384
let app = self.app.clone();
380385
let volume_id = self.volume_id.clone();
381386
let live_event_task_slot = Arc::clone(&self.live_event_task);
387+
let scanning = Arc::clone(&self.scanning);
382388

383389
// We need a way for the replay loop to signal "journal unavailable, need full scan".
384390
// Use a oneshot channel: if the replay detects a gap, it sends a signal.
@@ -402,6 +408,9 @@ impl IndexManager {
402408
)
403409
.await;
404410

411+
// Replay done (now in live mode) — allow verifier to run.
412+
scanning.store(false, Ordering::Relaxed);
413+
405414
if let Err(e) = result {
406415
log::warn!("Replay event loop error: {e}");
407416
}

apps/desktop/src-tauri/src/indexing/store.rs

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -776,26 +776,29 @@ impl IndexStore {
776776
Ok(conn.last_insert_rowid())
777777
}
778778

779-
/// Batch insert entries with pre-assigned IDs inside a transaction.
779+
/// Batch insert entries with pre-assigned IDs inside a savepoint.
780+
///
781+
/// Uses a savepoint instead of `unchecked_transaction()` so it nests correctly
782+
/// inside explicit transactions (replay's `BEGIN IMMEDIATE`).
780783
pub fn insert_entries_v2_batch(conn: &Connection, entries: &[EntryRow]) -> Result<(), IndexStoreError> {
781784
if entries.is_empty() {
782785
return Ok(());
783786
}
784-
let tx = conn.unchecked_transaction()?;
785-
{
786-
// Plain INSERT: the only unique constraint is the integer PK (`id`), and
787-
// ScanContext assigns unique IDs, so conflicts shouldn't occur. The table is
788-
// truncated before full scans and descendants are deleted before subtree scans.
789-
#[cfg(target_os = "macos")]
790-
let mut stmt = tx.prepare_cached(
791-
"INSERT INTO entries (id, parent_id, name, name_folded, is_directory, is_symlink, size, modified_at)
792-
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
793-
)?;
794-
#[cfg(not(target_os = "macos"))]
795-
let mut stmt = tx.prepare_cached(
796-
"INSERT INTO entries (id, parent_id, name, is_directory, is_symlink, size, modified_at)
797-
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
798-
)?;
787+
conn.execute_batch("SAVEPOINT insert_entries")?;
788+
// Plain INSERT: the only unique constraint is the integer PK (`id`), and
789+
// ScanContext assigns unique IDs, so conflicts shouldn't occur. The table is
790+
// truncated before full scans and descendants are deleted before subtree scans.
791+
#[cfg(target_os = "macos")]
792+
let mut stmt = conn.prepare_cached(
793+
"INSERT INTO entries (id, parent_id, name, name_folded, is_directory, is_symlink, size, modified_at)
794+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
795+
)?;
796+
#[cfg(not(target_os = "macos"))]
797+
let mut stmt = conn.prepare_cached(
798+
"INSERT INTO entries (id, parent_id, name, is_directory, is_symlink, size, modified_at)
799+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
800+
)?;
801+
let result: Result<(), IndexStoreError> = (|| {
799802
for e in entries {
800803
#[cfg(target_os = "macos")]
801804
{
@@ -822,9 +825,12 @@ impl IndexStore {
822825
e.modified_at,
823826
])?;
824827
}
828+
Ok(())
829+
})();
830+
match result {
831+
Ok(()) => { conn.execute_batch("RELEASE insert_entries")?; Ok(()) }
832+
Err(e) => { let _ = conn.execute_batch("ROLLBACK TO insert_entries"); Err(e) }
825833
}
826-
tx.commit()?;
827-
Ok(())
828834
}
829835

830836
/// Update an existing entry by ID.
@@ -867,14 +873,17 @@ impl IndexStore {
867873
Ok(())
868874
}
869875

870-
/// Batch upsert dir_stats by entry ID inside a transaction.
876+
/// Batch upsert dir_stats by entry ID inside a savepoint.
877+
///
878+
/// Uses a savepoint instead of `unchecked_transaction()` so it nests correctly
879+
/// inside explicit transactions (replay's `BEGIN IMMEDIATE`).
871880
pub fn upsert_dir_stats_by_id(conn: &Connection, stats: &[DirStatsById]) -> Result<(), IndexStoreError> {
872881
if stats.is_empty() {
873882
return Ok(());
874883
}
875-
let tx = conn.unchecked_transaction()?;
876-
{
877-
let mut stmt = tx.prepare_cached(
884+
conn.execute_batch("SAVEPOINT upsert_stats")?;
885+
let result: Result<(), IndexStoreError> = (|| {
886+
let mut stmt = conn.prepare_cached(
878887
"INSERT OR REPLACE INTO dir_stats
879888
(entry_id, recursive_size, recursive_file_count, recursive_dir_count)
880889
VALUES (?1, ?2, ?3, ?4)",
@@ -887,9 +896,12 @@ impl IndexStore {
887896
s.recursive_dir_count,
888897
])?;
889898
}
899+
Ok(())
900+
})();
901+
match result {
902+
Ok(()) => { conn.execute_batch("RELEASE upsert_stats")?; Ok(()) }
903+
Err(e) => { let _ = conn.execute_batch("ROLLBACK TO upsert_stats"); Err(e) }
890904
}
891-
tx.commit()?;
892-
Ok(())
893905
}
894906

895907
/// Set a meta key-value pair.

0 commit comments

Comments
 (0)