breadsearch/breadmill/src/sync_ext.rs
Breadway d01a3841d9 breadmill/breadsearch: narrow embedder lock scope, recover from poisoned mutexes, atomic index save + corrupt rebuild, fix GUI busy-poll, bump theme pin
- indexer.rs: embedder mutex is now locked only around each chunk's
  embed_document() call instead of the whole file's chunk loop, so a slow
  MIGraphX JIT compile on one file no longer blocks every query for minutes
- sync_ext.rs (new)/indexer.rs/main.rs/serve.rs: added MutexExt::lock_recover(),
  replacing every .lock().unwrap() so a panic on one thread (poisoning the
  mutex) logs a warning and recovers instead of cascading into every future
  lock().unwrap() call
- store.rs: save_index now writes to a .tmp sibling and renames atomically
  into place; Store::open recovers from an index that fails to load by
  wiping it and the SQLite files/chunks tables so the next scan rebuilds
  from scratch, instead of refusing to start at all. Note: this recovery
  only catches corruption usearch's loader reports as Err — verified
  experimentally that sufficiently garbled input segfaults the process
  instead, which no Rust-side handling can catch; the atomic save is what
  actually prevents the realistic mid-crash corruption case from arising
- breadsearch/src/main.rs: GUI query-result wait switched from
  glib::idle_add_local (re-invoked every main-loop tick, pegging a core for
  the whole wait) to a 15ms glib::timeout_add_local poll
- breadsearch/Cargo.toml: bread-theme pin bumped v0.2.8 -> v0.2.10 to match
  the rest of the family
2026-07-17 08:16:25 +08:00

41 lines
1.8 KiB
Rust

//! Poison-tolerant `Mutex` locking.
//!
//! A panic on any thread while holding `SharedState::store` or
//! `SharedState::embedder` (e.g. an untrapped ONNX Runtime panic — only PDF
//! extraction is `catch_unwind`-guarded, see `extract.rs`) poisons the
//! `Mutex`. Every subsequent plain `.lock().unwrap()` — in the indexer *and*
//! in every query handler in `serve.rs` — would then immediately panic too,
//! silently bricking indexing and search until the daemon is restarted by
//! hand.
//!
//! `Mutex` poisoning exists to flag "the data guarded by this lock might be
//! in an inconsistent state," but every lock scope in this crate is a short,
//! single-step SQLite call or usearch operation — nothing here spans a
//! multi-step invariant across a single `lock()` call — so recovering the
//! guard and logging loudly is a reasonable trade here: continuing to serve
//! (and re-attempting the operation that panicked, on the next file/query)
//! beats a daemon that silently stops answering everything after one panic.
use std::sync::{Mutex, MutexGuard};
pub trait MutexExt<T> {
/// Like `.lock().unwrap()`, but recovers from a poisoned mutex instead
/// of panicking again — logs once per recovery so it's visible in the
/// daemon's own output, not just silently swallowed.
fn lock_recover(&self) -> MutexGuard<'_, T>;
}
impl<T> MutexExt<T> for Mutex<T> {
fn lock_recover(&self) -> MutexGuard<'_, T> {
match self.lock() {
Ok(guard) => guard,
Err(poisoned) => {
eprintln!(
"breadmill: WARNING: a mutex was poisoned by a panic on another thread; \
recovering it and continuing instead of cascading the panic"
);
poisoned.into_inner()
}
}
}
}