fix(fuse): a folder trashed while sync is paused drops the creates in it (B178)

This commit is contained in:
nvrl committed 2026-10-04 22:51:47 +02:00
1 parent 0f3c06f982
commit a6f019ebe6
5 files changed
+51 -14

No files matched your search

+13 -12
View File
@@ -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
+11
View File
@@ -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(");
+21
View File
@@ -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).
+2
View File
@@ -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).
+4 -2
View File
@@ -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