fix(fuse): list a new file once while its create lands (B164)

A listing from Drive overlays what is still queued for the folder. A create
Drive has made but whose answer is on its way, or which the drain is still
reading back, was in both: Drive's node under its new uid and the queue's
stand-in, under one name. A node queued in the folder now holds its name there.
This commit is contained in:
nvrl committed 2026-10-04 19:03:33 +02:00
1 parent b37d5e7928
commit ce2bc0ac84
5 files changed
+80 -4

No files matched your search

+7 -1
View File
@@ -6496,11 +6496,17 @@ fn node_visible(node: &Node, folder_uid: &NodeUid, hidden: &HashSet<NodeUid>) ->
/// Lay what is queued for a folder over Drive's listing of it, from
/// `Db::queued_children`: a node a queued rename moved away is dropped, and a
/// node a queued create, mkdir or rename put there is listed as the DB has it.
///
/// That node holds its name here: a create Drive already has but whose answer
/// is still on its way, or which the drain is still reading back, is listed by
/// Drive under its new uid and by the queue under its stand-in. Both were
/// listed, and a 128-file folder listed 129 (`docs/BUGS.md` B164).
fn overlay_queued(nodes: &mut Vec<Node>, queued: Vec<Node>, gone: &HashSet<String>) {
let here: HashSet<String> = queued.iter().map(|node| node.uid.to_string()).collect();
let names: HashSet<&str> = queued.iter().map(|node| node.name.as_str()).collect();
nodes.retain(|node| {
let uid = node.uid.to_string();
!gone.contains(&uid) && !here.contains(&uid)
!gone.contains(&uid) && !here.contains(&uid) && !names.contains(node.name.as_str())
});
nodes.extend(queued);
}
+41
View File
@@ -554,6 +554,47 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
#[ignore = "mounts FUSE: run with `cargo test -p pdfs-fuse sim:: -- --ignored`"]
fn a_create_drive_lists_before_it_is_read_back_is_listed_once() {
// The feed reported a new file while the drain still read it back, so
// the folder was listed from Drive again, and the file came twice: as
// Drive's node and as the queued create's stand-in. A 128-file folder
// listed 129 (B164).
let drive = FakeDrive::new();
let dir = scratch("listed-before-retired");
let daemon = Daemon::start(&dir, drive.client(1, Faults::lan())).unwrap();
let mnt = daemon.mountpoint.clone();
std::fs::create_dir(mnt.join("x")).unwrap();
assert!(wait_until(Duration::from_secs(30), || {
daemon.pending().is_ok_and(|items| items.is_empty()) && drive.lookup("x").is_some()
}));
let create = daemon.client.hold_reply_to_next_create();
std::fs::write(mnt.join("x/c.bin"), b"abc").unwrap();
assert!(wait_until(Duration::from_secs(30), || create.reached()));
let uid = drive.lookup("x/c.bin").unwrap();
let read_back = daemon.client.hold_next_read(&uid);
drop(create);
assert!(wait_until(Duration::from_secs(30), || read_back.reached()));
// Long enough for the next event poll to bring the file's event.
std::thread::sleep(crate::POLL_INTERVAL + Duration::from_secs(3));
let names: Vec<_> = std::fs::read_dir(mnt.join("x"))
.unwrap()
.map(|e| e.unwrap().file_name())
.collect();
drop(read_back);
assert_eq!(names, ["c.bin"]);
assert!(wait_until(Duration::from_secs(30), || {
daemon.pending().is_ok_and(|items| items.is_empty())
}));
assert_eq!(std::fs::read(mnt.join("x/c.bin")).unwrap(), b"abc");
assert!(matches!(daemon.stop(), Ok(MountOutcome::Shutdown)));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
#[ignore = "mounts FUSE: run with `cargo test -p pdfs-fuse sim:: -- --ignored`"]
fn a_folder_made_again_while_the_old_one_is_trashed_is_a_new_folder() {
+27
View File
@@ -12,6 +12,33 @@ Conventions:
---
## B164 — A folder lists a new file twice while its create lands
**Status:** Fixed.
**Found:** 2026-10-04, by the account run on 2.10.0 ("a wide directory", on-demand pairing 2).
Eight threads wrote 128 files into a new folder, and the listing that followed held 129
entries. Each name was created on Drive once, and nothing was logged.
**Where:** `overlay_queued` in `crates/pdfs-fuse/src/lib.rs`.
**Cause.** A listing from Drive shows what is still queued for the folder over what Drive has
(B132). A create Drive has made but whose answer is still on its way, or which the drain is
still reading back, is in both: Drive lists it under its new uid, the queue under its stand-in.
That listing only happens when the folder's listing was dropped, and the feed's event for a
new file does that when it comes before the drain has noted the create as its own. Both were
listed under the same name.
The run logged nothing for the folder, so the event is inferred, not observed. It is the one
path found that lists a name twice.
**Fix.** A node queued in the folder holds its name there: Drive's node of the same name is
left out of the listing until the queued op lands.
**Test:** simulation test `a_create_drive_lists_before_it_is_read_back_is_listed_once` (failed
before the fix: the folder listed `c.bin` twice).
---
## B163 — A file read right after its create landed is empty
**Status:** Fixed.
+2
View File
@@ -55,6 +55,8 @@ by its local id, in the queue, the stored tree and pins alike.
inside the removed folder and was trashed along with it (B162).
- **A new file no longer reads as empty just after it is uploaded.** Drive can list it a moment
before its contents are committed, and the mount took that answer (B163).
- **A folder no longer lists a new file twice while it is uploaded.** A listing taken as the
upload landed showed it both as Drive had it and as the queue did (B164).
- **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).
- **A change that waits for a name to be freed no longer waits seconds after it is.** A rename or
+3 -3
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 B163.
- Fixes from the simulation and account runs since then: B127 and B141 to B164.
- **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`.
@@ -99,8 +99,8 @@ Tracks `docs/MILESTONE-3.0.0.md`. Update it with every commit that moves a phase
mirror move carries the local copy along. The journal showed a conflict copy retrying
into a deleted folder (B150, fixed). Not confirmed yet. The run on 2.10.0
(2026-10-04: 260 passed, 3 failed) failed a new file that read back empty (B163,
fixed), a 128-entry folder that listed 129, and its cleanup, which ran while the
daemon was restarting.
fixed), a 128-entry folder that listed 129 (B164, fixed), and its cleanup, which ran
while the daemon was restarting.
5. **Release**: 2.10.0 is tagged; the 2.9.0 tag still waits for the user's go-ahead.
## After 3.0.0