fix(fuse): a file deleted just as its create lands is trashed on Drive (B173)

This commit is contained in:
nvrl committed 2026-10-04 21:51:52 +02:00
1 parent 3b81cc5240
commit 378667fb37
7 files changed
+83 -8

No files matched your search

+2
View File
@@ -1539,6 +1539,8 @@ impl Core {
} }
retired retired
}; };
#[cfg(test)]
self.drive.landing(real);
if retired == CreateRetired::Landed if retired == CreateRetired::Landed
&& let Some(blob) = uploaded && let Some(blob) = uploaded
{ {
+3 -2
View File
@@ -155,8 +155,9 @@ pub(crate) trait DriveApi: Send + Sync {
async fn quota(&self) -> Result<Quota>; async fn quota(&self) -> Result<Quota>;
/// Called by the drain once it has let go of the blob of an upload to /// Called by the drain once it has let go of the blob of an upload to
/// `uid` that landed, before the tree has the node, so a simulation can /// `uid` that landed, or retired the create that made `uid`, before the
/// hold it there. Blocks the drain worker it is called on. /// tree has the node, so a simulation can hold it there. Blocks the drain
/// worker it is called on.
#[cfg(test)] #[cfg(test)]
fn landing(&self, _uid: &NodeUid) {} fn landing(&self, _uid: &NodeUid) {}
} }
+15 -4
View File
@@ -2140,7 +2140,7 @@ impl ProtonFs {
reply.error(error); reply.error(error);
return; return;
} }
let (ino, uid) = match self.core.lookup_child(parent, name) { let (ino, mut uid) = match self.core.lookup_child(parent, name) {
Ok(x) => x, Ok(x) => x,
Err(e) => { Err(e) => {
reply.error(e); reply.error(e);
@@ -2168,9 +2168,20 @@ impl ProtonFs {
return; return;
} }
self.core.state().forget_or_unlink(&uid); self.core.state().forget_or_unlink(&uid);
debug!(%uid, name, "deleted a node that had not been created remotely yet"); // A create that landed before the ops were dropped left none: Drive
reply.ok(); // has the node, and the tree had not taken its uid yet. It is
return; // trashed there like any other (docs/BUGS.md B173).
match self.core.landed_row_uid(ino) {
Some(real) => {
debug!(local = %uid, %real, name, "create landed under a delete");
uid = real;
}
None => {
debug!(%uid, name, "deleted a node that had not been created remotely yet");
reply.ok();
return;
}
}
} }
// Offline: queue it. Trashing is the one mutation a user expects to work // Offline: queue it. Trashing is the one mutation a user expects to work
// regardless — the file is gone from their point of view the moment the // regardless — the file is gone from their point of view the moment the
+35
View File
@@ -525,6 +525,41 @@ 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_file_deleted_as_its_create_lands_is_trashed_on_drive() {
// The drain had retired the create and the tree still had the file
// under its stand-in. The delete found no op left to drop and took the
// file for one Drive had never seen, so Drive kept it (B173).
let drive = FakeDrive::new();
let dir = scratch("deleted-as-create-lands");
let daemon = Daemon::start(&dir, drive.client(1, Faults::lan())).unwrap();
let path = daemon.mountpoint.join("c.bin");
let create = daemon.client.hold_reply_to_next_create();
std::fs::write(&path, b"abc").unwrap();
assert!(wait_until(Duration::from_secs(30), || create.reached()));
let uid = drive.lookup("c.bin").unwrap();
let landing = daemon.client.hold_next_landing(&uid);
drop(create);
assert!(wait_until(Duration::from_secs(30), || landing.reached()));
std::fs::remove_file(&path).unwrap();
drop(landing);
assert!(
wait_until(Duration::from_secs(30), || {
daemon.pending().is_ok_and(|items| items.is_empty())
&& drive.lookup("c.bin").is_none()
}),
"{:?} {:?}",
daemon.pending(),
drive.tree()
);
assert!(!path.exists());
assert!(matches!(daemon.stop(), Ok(MountOutcome::Shutdown)));
let _ = std::fs::remove_dir_all(&dir);
}
#[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`"]
fn a_file_moved_while_its_create_was_on_the_wire_keeps_its_new_name() { fn a_file_moved_while_its_create_was_on_the_wire_keeps_its_new_name() {
+22
View File
@@ -12,6 +12,28 @@ Conventions:
--- ---
## B173 — A file deleted just as its create lands stays on Drive
**Status:** Fixed (unverified).
**Found:** 2026-10-04, by `one_client_on_a_good_link` (seed 12) on d850265, run three at a time
in a loop: one of nine runs failed. A deleted `c.bin` was still on Drive with its old bytes,
and the next `c.bin` made on the mount landed as a conflict copy beside it.
**Where:** `trash_child` in `crates/pdfs-fuse/src/filesystem.rs`.
**Cause.** When a create lands, the drain retires its op and moves the node's row to the uid
Drive gave it, then gives the tree that uid. In between, it caches the uploaded bytes. A delete
in that gap still found the file under its stand-in, found no op left to drop, and took it for
a file Drive had never seen. So nothing trashed it on Drive.
**Fix.** After dropping a stand-in's ops, the delete checks whether the node's row has a real
uid. If it has, the create landed first, and the file is trashed on Drive like any other.
**Test:** `a_file_deleted_as_its_create_lands_is_trashed_on_drive` in
`crates/pdfs-fuse/src/sim/daemon.rs`, which holds the drain between the two steps.
---
## B172 — A file renamed while its upload is read back shows its old name ## B172 — A file renamed while its upload is read back shows its old name
**Status:** Fixed (unverified). **Status:** Fixed (unverified).
+2
View File
@@ -72,6 +72,8 @@ by its local id, in the queue, the stored tree and pins alike.
it failed with "Input/output error" (B170). it failed with "Input/output error" (B170).
- **A file deleted while its upload was retrying is removed from Drive right away.** It no - **A file deleted while its upload was retrying is removed from Drive right away.** It no
longer waits for the upload's next retry (B171). longer waits for the upload's next retry (B171).
- **A file deleted just as it reached Drive is removed from Drive too.** It no longer stayed
there, and the next file with that name no longer became a conflict copy (B173).
- **A file renamed just as its upload finished keeps its new name.** The mount no longer showed - **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, the old name, and a second rename no longer failed with "No such file or directory" (B152,
B172). 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 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: B127 and B141 to B172. - Fixes from the simulation and account runs since then: B127 and B141 to B173.
- **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`.
@@ -115,7 +115,9 @@ Tracks `docs/MILESTONE-3.0.0.md`. Update it with every commit that moves a phase
it replaced as its base, and the next write failed with `EIO` (B170, fixed). The it replaced as its base, and the next write failed with `EIO` (B170, fixed). The
suite's seed 9 found a deleted file's trash waiting out its create's backoff (B171, suite's seed 9 found a deleted file's trash waiting out its create's backoff (B171,
fixed). Run three at a time on 2502af6, seed 8 failed two of nine runs: a file renamed fixed). Run three at a time on 2502af6, seed 8 failed two of nine runs: a file renamed
while its upload was read back showed its old name again (B172, fixed). while its upload was read back showed its old name again (B172, fixed). The loop
on d850265 failed seed 12 once in nine runs: a file deleted just as its create
landed stayed on Drive (B173, fixed).
5. **Release**: 2.10.0 is tagged; the 2.9.0 tag still waits for the user's go-ahead. 5. **Release**: 2.10.0 is tagged; the 2.9.0 tag still waits for the user's go-ahead.
## After 3.0.0 ## After 3.0.0