From a6f019ebe63b73f47ac5d6f85a04478101a7b29f Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sun, 4 Oct 2026 22:51:47 +0200 Subject: [PATCH] fix(fuse): a folder trashed while sync is paused drops the creates in it (B178) --- crates/pdfs-fuse/src/drain.rs | 25 +++++++++++++------------ crates/pdfs-fuse/src/lib.rs | 11 +++++++++++ docs/BUGS.md | 21 +++++++++++++++++++++ docs/CHANGELOG.md | 2 ++ docs/MILESTONE-3.0.0-STATUS.md | 6 ++++-- 5 files changed, 51 insertions(+), 14 deletions(-) diff --git a/crates/pdfs-fuse/src/drain.rs b/crates/pdfs-fuse/src/drain.rs index 46ec7b5..4e8c4ac 100644 --- a/crates/pdfs-fuse/src/drain.rs +++ b/crates/pdfs-fuse/src/drain.rs @@ -299,29 +299,30 @@ impl Core { return; } let now = now_millis(); + // Offline or paused, nothing is claimed. A claimed create may be on + // the wire, so trashing its folder kept it as a trash; claimed + // only to be handed back, it was kept although nothing had sent + // it (`docs/BUGS.md` B178). + let online = self.online.load(Ordering::Relaxed); + let paused = self.sync_paused(); // One row, chosen and *claimed* by the database. Reading the whole // queue to pick one op made a long queue quadratic to drain, and // held the shared connection — and so every FUSE metadata call — // for the duration. - let due = match self.db.claim_next_due_op(now) { - Ok(due) => due, + let due = match (online && !paused) + .then(|| self.db.claim_next_due_op(now)) + .transpose() + { + Ok(due) => due.flatten(), Err(error) => { error!(%error, "claiming the next pending operation failed"); self.wait_for_drain_work(); continue; } }; - let online = self.online.load(Ordering::Relaxed); - let paused = self.sync_paused(); let op = match due { - Some(op) if online && !paused => op, - idle => { - // Claimed but offline: hand it straight back, or it stays - // invisible to every worker until this one happens to be - // the next to notice the reconnect. - if let Some(op) = idle { - self.release_claim(&op); - } + Some(op) => op, + None => { if primary { // Nothing to upload is exactly when the connection is // free, so this is where the read path's buffered LRU diff --git a/crates/pdfs-fuse/src/lib.rs b/crates/pdfs-fuse/src/lib.rs index bfc35db..d7a3298 100644 --- a/crates/pdfs-fuse/src/lib.rs +++ b/crates/pdfs-fuse/src/lib.rs @@ -9034,6 +9034,17 @@ mod tests { assert!(!rename.contains(".lock().relocate(")); } + #[test] + fn a_paused_drain_claims_nothing() { + // A claimed create counts as sent, so trashing its folder kept it as + // a trash, although the paused worker only handed it back (B178). + let worker: String = + function_source(include_str!("drain.rs"), "pub(crate) fn run_pending_drain(") + .split_whitespace() + .collect(); + assert!(worker.contains("(online&&!paused).then(||self.db.claim_next_due_op(now))")); + } + #[test] fn a_landed_upload_stays_pending_until_its_bytes_are_cached() { let drain = function_source(include_str!("drain.rs"), "pub(crate) fn drain_revision("); diff --git a/docs/BUGS.md b/docs/BUGS.md index aa5ef24..b0742a5 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -12,6 +12,27 @@ Conventions: --- +## B178 — Trashing a folder while sync is paused keeps a create in it as a trash + +**Status:** Fixed (unverified). +**Found:** 2026-10-04, by the account run on 378667f, on an on-demand sync folder: B116's +`pdfs rm b116` with sync paused left the create of `b116/sub/deeper/b116 held.bin` queued as +trash #16098. + +**Where:** `Core::run_pending_drain` in `crates/pdfs-fuse/src/drain.rs`. + +**Cause.** Trashing a folder drops the creates queued below it, but keeps a create that may +already be on the wire as a trash, for a node Drive may have made (B151). A claimed create +counts as such. A drain worker that woke while sync was paused or offline claimed the next due +op and only then saw it could not send it, and handed it back. The rename that lets a parked +create go wakes the workers, so a trash in that moment kept a create nothing had sent. + +**Fix.** A worker checks that it is online and not paused before it claims anything. + +**Test:** `a_paused_drain_claims_nothing` in `crates/pdfs-fuse/src/lib.rs`. + +--- + ## B177 — A folder listed just as its create lands answers "Input/output error" **Status:** Fixed (unverified). diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 2ca1ad2..ec18e3c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -81,6 +81,8 @@ by its local id, in the queue, the stored tree and pins alike. - **A write closed just as its file reached Drive is no longer lost** (B176). - **Opening a folder just as it reaches Drive no longer fails with "Input/output error"** (B177). +- **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 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 e679ced..4cecd2b 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 B177. +- Fixes from the simulation and account runs since then: B127 and B141 to B178. - **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`. @@ -122,7 +122,9 @@ Tracks `docs/MILESTONE-3.0.0.md`. Update it with every commit that moves a phase seed 8: a file replaced by a rename in that gap stayed on Drive (B175, fixed), and seed 12: a write closed as its file's create landed was dropped (B176, fixed). The account run on 378667f: a folder listed as its create landed asked Drive by - its stand-in and answered `EIO` (B177, fixed). + 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). 5. **Release**: 2.10.0 is tagged; the 2.9.0 tag still waits for the user's go-ahead. ## After 3.0.0