diff --git a/crates/pdfs-fuse/src/drain.rs b/crates/pdfs-fuse/src/drain.rs index 4e8c4ac..1cace40 100644 --- a/crates/pdfs-fuse/src/drain.rs +++ b/crates/pdfs-fuse/src/drain.rs @@ -703,12 +703,11 @@ impl Core { /// Block until there is plausibly something to do: a new op, a reconnect, or /// the shortest outstanding backoff elapsing. pub(crate) fn wait_for_drain_work(&self) { - let (lock, cv) = &*self.drain_wake; - let mut woken = lock.lock(); - if !*woken { - cv.wait_for(&mut woken, DRAIN_IDLE_POLL); - } - *woken = false; + sleep_for_drain_work( + &self.drain_wake, + || self.shutdown.is_stopping(), + DRAIN_IDLE_POLL, + ); } /// Like [`wait_for_drain_work`](Self::wait_for_drain_work), but first @@ -722,12 +721,7 @@ impl Core { } _ => DRAIN_IDLE_POLL, }; - let (lock, cv) = &*self.drain_wake; - let mut woken = lock.lock(); - if !*woken { - cv.wait_for(&mut woken, timeout); - } - *woken = false; + sleep_for_drain_work(&self.drain_wake, || self.shutdown.is_stopping(), timeout); } /// Perform one queued op and retire it. @@ -2697,6 +2691,27 @@ fn keeps_landed_upload(pinned: bool, len: u64, budget: u64) -> bool { } /// Where `st` has `uid`: its name and parent. +/// Sleep on the drain's `wake` for up to `timeout`, unless a wake-up came +/// since a worker last took one, or the mount is `stopping`. +/// +/// One wake-up wakes every worker, and the first to wake takes it. A worker +/// that came to sleep after that slept out its timeout, the stop's included, +/// and the stop gave up on it after 10 s (`docs/BUGS.md` B180). The stop sets +/// its flag before it takes the lock, so a worker that finds it unset under +/// the lock is asleep when the wake-up comes. +fn sleep_for_drain_work( + wake: &(Mutex, parking_lot::Condvar), + stopping: impl Fn() -> bool, + timeout: Duration, +) { + let (lock, cv) = wake; + let mut woken = lock.lock(); + if !*woken && !stopping() { + cv.wait_for(&mut woken, timeout); + } + *woken = false; +} + fn placed_in(st: &crate::state::State, uid: &NodeUid) -> Option<(String, Option)> { let entry = st.by_uid.get(uid).and_then(|ino| st.entries.get(ino))?; Some((entry.node.name.clone(), entry.node.parent_uid.clone())) @@ -2712,6 +2727,16 @@ mod tests { use proton_drive_rs::proton_sdk::ids::{LinkId, VolumeId}; use std::cell::Cell; + #[test] + fn a_drain_worker_that_missed_the_stop_wake_up_does_not_sleep() { + // Another worker took the stop's wake-up, and this one slept out its + // timeout, past the stop's deadline (B180). + let wake = (Mutex::new(false), parking_lot::Condvar::new()); + let started = Instant::now(); + sleep_for_drain_work(&wake, || true, Duration::from_secs(5)); + assert!(started.elapsed() < Duration::from_secs(1)); + } + fn pending(kind: &str) -> PendingOp { PendingOp { id: 0, diff --git a/docs/BUGS.md b/docs/BUGS.md index 331fc0e..709fdf5 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -12,6 +12,32 @@ Conventions: --- +## B180 — A stop waits 10 s for a drain worker that slept through it + +**Status:** Fixed. +**Found:** 2026-10-04, by the simulation suite on 0f3c06f: `a_stop_ends_a_mirror_pass_after_the_uploads_it_is_sending` +found "background workers did not finish in time" in the log, left there by two earlier tests +whose stops had given up on a drain worker. On its own, +`a_folder_made_again_while_the_old_one_is_trashed_is_a_new_folder` took 11 s instead of 2 s in +about one run in three. + +**Where:** `Core::wait_for_drain_work` and `Core::wait_for_drain_work_or_due` in +`crates/pdfs-fuse/src/drain.rs`. + +**Cause.** The drain workers sleep on one flag and one condvar. A stop sets its flag, then +raises the wake-up and wakes them all, and the first worker to wake takes the wake-up. A worker +that was between its stop check and its sleep at that moment found no wake-up and slept its +timeout, up to 30 s. The stop gave up on it after 10 s and shut down around it. With 16 workers +since step 7 this was common. + +**Fix.** A worker checks the stop under the wake-up's lock before it sleeps. The stop sets its +flag before it takes that lock, so a worker that finds it unset is asleep when the wake-up comes. + +**Test:** `a_drain_worker_that_missed_the_stop_wake_up_does_not_sleep` in +`crates/pdfs-fuse/src/drain.rs`. + +--- + ## B179 — A file read just as its create lands reads empty **Status:** Fixed (unverified). diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index c213f39..3690373 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -84,6 +84,8 @@ by its local id, in the queue, the stored tree and pins alike. - **Deleting a folder while sync is paused no longer leaves a queued change behind** for a file in it that was never uploaded (B178). - **A file read just as it reaches Drive no longer reads empty** (B179). +- **Stopping the daemon no longer takes 10 seconds more than it needs** when an idle upload + worker missed the stop (B180). - **A file renamed just as its upload finished keeps its new name.** The mount no longer showed the old name, and a second rename no longer failed with "No such file or directory" (B152, B172). diff --git a/docs/MILESTONE-3.0.0-STATUS.md b/docs/MILESTONE-3.0.0-STATUS.md index bf00a57..29bb160 100644 --- a/docs/MILESTONE-3.0.0-STATUS.md +++ b/docs/MILESTONE-3.0.0-STATUS.md @@ -9,7 +9,7 @@ Tracks `docs/MILESTONE-3.0.0.md`. Update it with every commit that moves a phase - **Phase 2**: local ids, inode = local id, one tree for every mount (2.10.0). - **Phase 3, first part**: mkdir, create, rename, unlink and rmdir on the mount are recorded locally and sent from the queue; `"local_first": false` brings back the old path (5e0f30c). -- Fixes from the simulation and account runs since then: B127 and B141 to B179. +- Fixes from the simulation and account runs since then: B127 and B141 to B180. - **Sync issues, first cut** (§6.2): refusals sorted into issues, shown at once in the app, tray and CLI; Export button and `pdfs sync export`; `pdfs sync issues`. @@ -125,6 +125,8 @@ Tracks `docs/MILESTONE-3.0.0.md`. Update it with every commit that moves a phase its stand-in and answered `EIO` (B177, fixed). A folder trashed while sync was paused kept a create below it as a trash: a paused drain worker had claimed it (B178, fixed). A file read as its create landed read empty (B179, fixed). + The suite on 0f3c06f: a stop gave up on a drain worker that slept through its + wake-up (B180, fixed). 5. **Release**: 2.10.0 is tagged; the 2.9.0 tag still waits for the user's go-ahead. ## After 3.0.0