fix(fuse): never adopt a node another queued create is making

A file renamed away from a.txt while its create was on the wire, and a new
a.txt made after it, could both land on the first file's node: the second
create found a fresh, empty a.txt and took it for its own unanswered create.
The first file's rename then carried the second one's bytes away.

A create now leaves alone a node that another create is making under the
same name, and one the tree already knows. It waits for the name like for
any other queued change (docs/BUGS.md B147).
This commit is contained in:
nvrl committed 2026-10-04 03:15:50 +02:00
1 parent 4eccdeeda6
commit 77d0f254fc
3 files changed
+40

No files matched your search

+19
View File
@@ -1160,6 +1160,19 @@ impl Core {
parent: &NodeUid,
name: &str,
) -> Result<Option<NodeUid>, Box<dyn std::error::Error>> {
// A second file queued under the same name, after the first was
// renamed away, finds the first one's node fresh and empty while its
// create is on the wire. Taking it put both files on one node, and the
// first one's rename took the second one's bytes along
// (docs/BUGS.md B147).
let racing = self
.creating
.lock()
.iter()
.any(|(id, (p, n))| *id != op.id && p == parent && n == name);
if racing {
return Ok(None);
}
let uids = self
.rt
.block_on(self.drive.enumerate_folder_children_node_uids(parent))?;
@@ -1170,6 +1183,12 @@ impl Core {
else {
return Ok(None);
};
// A node in the tree was answered for, by this mount or by a listing:
// the same race after the first create landed. Without the adoption,
// an unanswered twin forks a conflict copy, which loses nothing.
if self.state().by_uid.contains_key(&twin.uid) {
return Ok(None);
}
// The light listing carries no file size, and an adoptable file is
// exactly an empty one.
let twin = match twin.kind {
+19
View File
@@ -12,6 +12,25 @@ Conventions:
---
## B147 — A new file made under a name just renamed away can share the first file's node
**Status:** Fixed (unverified).
**Found:** 2026-10-04, by `sim::run::tests::three_clients_one_writer_each`, seed 2, in about one
run of eight. Client 1 made `a.txt`, renamed it to `b.txt`, and made `a.txt` again. Drive ended
up without `b.txt`, and `e.md`, a later rename of the new `a.txt`, held `b.txt`'s bytes.
**Where:** `Core::adoptable_twin` in `crates/pdfs-fuse/src/drain.rs`.
**Cause.** The first file's create went out as `a.txt` and the rename was queued behind it. While
that create was on the wire, the second file's create found `a.txt` taken by a fresh, empty file,
which is how the node of an unanswered create of its own looks, and adopted it. Both local files
landed on the one node, and the first file's rename took it to `b.txt`.
**Fix.** A create no longer adopts a node another create is making under the same name and folder:
it waits for that one to land, as for any name a queued change holds. Nor does it adopt a node
already in the tree, which was answered for. An unanswered twin the tree has already listed
lands as a conflict copy, which loses nothing.
## B146 — A file renamed just as its upload lands can go back to its old name
**Status:** Fixed (unverified).
+2
View File
@@ -44,6 +44,8 @@ are kept.
A file made a moment before was taken for deleted and dropped from the queue (B145).
- **A file renamed just as its upload lands keeps its new name.** The tree could take back the
name Drive still had (B146).
- **A new file made under a name another file just left no longer takes that file's place on
Drive.** Both could land on one node, and one file's content was lost (B147).
## [2.10.0] — 2026-10-03