mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-03 16:41:26 +01:00
fix(fuse): a file deleted just as its create lands is trashed on Drive (B173)
This commit is contained in:
1 parent
3b81cc5240
commit
378667fb37
7 files changed
+83
-8
No files matched your search
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user