fix(fuse): land a conflict copy in the root when its folder is gone (B150)

This commit is contained in:
nvrl committed 2026-10-04 04:45:48 +02:00
1 parent 50ae25bb88
commit 23dc5c3723
5 files changed
+78 -3

No files matched your search

+8 -2
View File
@@ -1585,8 +1585,14 @@ impl Core {
.map(|(_, n)| n.clone()) .map(|(_, n)| n.clone())
.unwrap_or_else(|| self.node_name(uid)); .unwrap_or_else(|| self.node_name(uid));
// A node the tree has forgotten still has bytes worth keeping, so the // A node the tree has forgotten still has bytes worth keeping, so the
// copy falls back to the root rather than being abandoned. // copy falls back to the root rather than being abandoned. So does one
let Some(parent) = place.map(|(p, _)| p).or_else(|| self.root_uid()) else { // whose folder went with it: an upload into a folder Drive no longer
// has fails on every retry (B150).
let folder = match place.map(|(p, _)| p) {
Some(p) if self.fetch_node_remote(&p)?.is_some_and(|n| !n.trashed) => Some(p),
_ => None,
};
let Some(parent) = folder.or_else(|| self.root_uid()) else {
return self.abandon_to_staging(op, blob, uid, &name, reason); return self.abandon_to_staging(op, blob, uid, &name, reason);
}; };
warn!(%uid, name, reason, "queued write conflicts; keeping a conflict copy"); warn!(%uid, name, reason, "queued write conflicts; keeping a conflict copy");
+47
View File
@@ -278,6 +278,7 @@ pub(crate) fn wait_until(limit: Duration, mut done: impl FnMut() -> bool) -> boo
mod tests { mod tests {
use super::*; use super::*;
use crate::sim::fake_drive::{Entry, FakeDrive, Faults}; use crate::sim::fake_drive::{Entry, FakeDrive, Faults};
use std::collections::BTreeMap;
#[test] #[test]
#[ignore = "mounts FUSE: run with `cargo test -p pdfs-fuse sim:: -- --ignored`"] #[ignore = "mounts FUSE: run with `cargo test -p pdfs-fuse sim:: -- --ignored`"]
@@ -534,6 +535,52 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir); let _ = std::fs::remove_dir_all(&dir);
} }
#[test]
#[ignore = "mounts FUSE: run with `cargo test -p pdfs-fuse sim:: -- --ignored`"]
fn a_queued_write_whose_folder_was_trashed_lands_in_the_root() {
// The conflict copy of a write to a file trashed elsewhere went into
// the file's folder. With that folder gone too, the upload failed on
// every retry, every two minutes (B150).
let drive = FakeDrive::new();
let dir = scratch("conflict-copy-folder-gone");
let daemon = Daemon::start(&dir, drive.client(1, Faults::lan())).unwrap();
let mount = &daemon.mountpoint;
std::fs::create_dir(mount.join("d")).unwrap();
std::fs::write(mount.join("d/f.txt"), b"first").unwrap();
assert!(wait_until(Duration::from_secs(30), || {
drive.tree().contains_key("d/f.txt") && daemon.pending().is_ok_and(|ops| ops.is_empty())
}));
let paused = |paused| Request::SetSyncPaused {
paused,
until: None,
};
daemon.request(&paused(true)).unwrap();
std::fs::write(mount.join("d/f.txt"), b"second").unwrap();
drive.device().trash("d/f.txt");
drive.device().trash("d");
daemon.request(&paused(false)).unwrap();
let copy = |tree: &BTreeMap<String, Entry>| {
tree.iter()
.find(|(path, _)| path.starts_with("f (sync-conflict "))
.map(|(_, entry)| entry.clone())
};
assert!(
wait_until(Duration::from_secs(30), || {
copy(&drive.tree()) == Some(Entry::File(Arc::new(b"second".to_vec())))
&& daemon.pending().is_ok_and(|ops| ops.is_empty())
}),
"{:?} {:?}",
drive.tree().keys().collect::<Vec<_>>(),
daemon.pending()
);
assert!(matches!(daemon.stop(), Ok(MountOutcome::Shutdown)));
let _ = std::fs::remove_dir_all(&dir);
}
/// Make 100 folders of 10 empty files each through a mount on `faults`, and /// Make 100 folders of 10 empty files each through a mount on `faults`, and
/// time the syscalls and then the drain until Drive holds all of it. /// time the syscalls and then the drain until Drive holds all of it.
fn time_a_thousand_files(faults: Faults) -> (Duration, Duration) { fn time_a_thousand_files(faults: Faults) -> (Duration, Duration) {
+20
View File
@@ -12,6 +12,26 @@ Conventions:
--- ---
## B150 — A conflict copy whose folder is gone retries forever
**Status:** Fixed (unverified).
**Found:** 2026-10-04, by the journal check of the account run on a6f36d5: 33 warnings, one
every two minutes for the whole run, all "queued write conflicts; keeping a conflict copy" for a
`write.bin` from an earlier run's B79 case, each followed by `DoesNotExist` from Drive.
**Where:** `Core::keep_as_conflict_copy` in `crates/pdfs-fuse/src/drain.rs`.
**Cause.** A queued write to a file that was trashed or deleted on Drive is kept as a conflict
copy beside the file. When the file's folder was deleted too, the upload into it failed, the op
backed off and tried again, with no end. The copy only fell back to the root when the tree had
forgotten the file, not when Drive had lost its folder.
**Fix.** The copy checks that the folder is still on Drive and not trashed, and lands in the
location's root otherwise. Sim test `a_queued_write_whose_folder_was_trashed_lands_in_the_root`.
**Left open.** How the B79 case's bytes stayed in `staging/` after the file was deleted, so that
the next start queued them again ("reconciled orphaned staged writes queued=1"), is not known yet.
## B149 — `pdfs rename` or `pdfs move` right after a rename through the mount is undone ## B149 — `pdfs rename` or `pdfs move` right after a rename through the mount is undone
**Status:** Fixed (unverified). **Status:** Fixed (unverified).
+2
View File
@@ -33,6 +33,8 @@ folder by local id too.
for this release. for this release.
### Fixed ### Fixed
- **A change to a file deleted elsewhere, together with its folder, no longer retries forever.**
The conflict copy that keeps it now lands in the location's root (B150).
- **`pdfs rename` and `pdfs move` right after a change through the mount are no longer undone.** - **`pdfs rename` and `pdfs move` right after a change through the mount are no longer undone.**
They are queued behind it, and they work on a file that has not reached Drive yet (B149). They are queued behind it, and they work on a file that has not reached Drive yet (B149).
- **A folder kept offline before it reached Drive stays kept offline.** Its pin was lost when the - **A folder kept offline before it reached Drive stays kept offline.** Its pin was lost when the
+1 -1
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 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 - **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). locally and sent from the queue; `"local_first": false` brings back the old path (5e0f30c).
- Fixes from the simulation and account runs since then: B141 to B149. - Fixes from the simulation and account runs since then: B141 to B150.
- **Sync issues, first cut** (§6.2): refusals sorted into issues, shown at once in the app, - **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`. tray and CLI; Export button and `pdfs sync export`; `pdfs sync issues`.