diff --git a/apps/desktop/src-tauri/src/media_preview.rs b/apps/desktop/src-tauri/src/media_preview.rs index 0fdb3eb..acf42ac 100644 --- a/apps/desktop/src-tauri/src/media_preview.rs +++ b/apps/desktop/src-tauri/src/media_preview.rs @@ -413,6 +413,51 @@ pub fn error_response(status: StatusCode) -> Response> { mod tests { use super::*; + #[test] + fn concurrent_admission_stays_bounded_and_unwind_releases_capacity() { + let registry = Arc::new(PreviewRegistry::default()); + let active = Arc::new(AtomicUsize::new(0)); + let barrier = Arc::new(std::sync::Barrier::new(8)); + let workers: Vec<_> = (0..8) + .map(|_| { + let registry = registry.clone(); + let active = active.clone(); + let barrier = barrier.clone(); + std::thread::spawn(move || { + barrier.wait(); + for _ in 0..500 { + if let Some(admission) = registry.admit() { + let count = active.fetch_add(1, Ordering::SeqCst) + 1; + assert!(count <= MAX_REQUESTS); + std::thread::yield_now(); + active.fetch_sub(1, Ordering::SeqCst); + drop(admission); + } + } + }) + }) + .collect(); + for worker in workers { + worker.join().unwrap(); + } + assert_eq!(active.load(Ordering::SeqCst), 0); + assert_eq!(registry.requests.load(Ordering::SeqCst), 0); + let first = registry.admit().unwrap(); + let interrupted = registry.clone(); + assert!(std::panic::catch_unwind(move || { + let _second = interrupted.admit().unwrap(); + panic!("injected preview worker panic"); + }) + .is_err()); + let second = registry + .admit() + .expect("Unwinding must release the second slot"); + assert!(registry.admit().is_none()); + drop(first); + drop(second); + assert_eq!(registry.requests.load(Ordering::SeqCst), 0); + } + struct Fixture { _directory: tempfile::TempDir, database: PathBuf, diff --git a/docs/MEDIA-PREVIEW.md b/docs/MEDIA-PREVIEW.md index a6b239b..faaf49a 100644 --- a/docs/MEDIA-PREVIEW.md +++ b/docs/MEDIA-PREVIEW.md @@ -21,7 +21,8 @@ Index-change notifications also advance the preview selection identity, so a same-path image/recording reloads rather than retaining previously decoded media. Failed native preview preparation and image/audio/video loading now offer an explicit **Retry preview** button. It invalidates the old request revision and cannot restart a selection -that has since changed. There is no automatic retry loop. The added regression +that has since changed. Busy preparation receives at most one delayed retry; +other failures require explicit user action, with no unbounded retry loop. The added regression passed with all 60 frontend tests, formatting and the production frontend build. This does not hash every recording on every request. A same-size/mtime in-place @@ -53,7 +54,7 @@ for these protocol regressions. Ubuntu 24.04 WSL compiled the immutable `370db7e` Git snapshot with the locked dependencies and `SEARCHI_FORCE_CPU=1`. All nine preview tests passed in 0.73 s; the complete native desktop suite then passed 45/45 in 2.78 s, with no skips. -The native source tree remains identical to that snapshot; subsequent commits +At the time of validation the native source tree matched that snapshot; subsequent commits changed frontend retry behavior only. Compilation took 8m 18s after missing locked dependencies were fetched. An initial offline attempt could not resolve the uncached Single-Instance plugin; it was not a product test failure. @@ -69,3 +70,10 @@ The existing vendored GLib code emitted 34 compiler warnings. This was a debug Rust test build, not strict Linux Clippy, a packaged desktop launch, WebKit playback, GPU inference or macOS validation. No model inference was executed. Native custom-protocol playback and seeking remain open on all release systems. + +A subsequent Windows CPU concurrency regression ran eight threads with 500 +admission attempts each and checked that no more than two admitted jobs overlap. +It also injected a worker unwind and verified that capacity is released rather +than remaining busy. All 46 native tests passed in 1.04 s and strict all-target +Clippy passed. This added test was not included in the earlier Linux 45-test +snapshot; no additional production protocol behavior changed.