From e743cd60cf6dc5980a7b96a821a4dbbcff4b12f9 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Mon, 5 Oct 2026 00:02:40 +0200 Subject: [PATCH] fix(fuse): a folder whose mkdir is queued is listed from the database (B125) --- crates/pdfs-fuse/src/lib.rs | 11 ++++++++ crates/pdfs-fuse/src/sim/daemon.rs | 44 ++++++++++++++++++++++++++++++ docs/BUGS.md | 15 ++++++++-- docs/CHANGELOG.md | 2 ++ docs/MILESTONE-3.0.0-STATUS.md | 6 ++-- 5 files changed, 72 insertions(+), 6 deletions(-) diff --git a/crates/pdfs-fuse/src/lib.rs b/crates/pdfs-fuse/src/lib.rs index 79a4711..a3d6b85 100644 --- a/crates/pdfs-fuse/src/lib.rs +++ b/crates/pdfs-fuse/src/lib.rs @@ -2521,6 +2521,17 @@ impl Core { Ok(None) => {} Err(e) => warn!(%folder_uid, error = %e, "db children_if_listed failed"), } + // Drive knows nothing by a stand-in. Until its mkdir lands the folder + // is only here, and once it has, the DB holds what Drive does. A + // listing dropped meanwhile was asked of Drive, and every lookup in + // the folder failed with EIO (`docs/BUGS.md` B125). + if is_local_uid(&folder_uid) { + let nodes = self.db.known_children(&folder_uid).map_err(|e| { + warn!(%folder_uid, error = %e, "db known_children failed"); + Errno::EIO + })?; + return self.adopt_db_listing(ino, &folder_uid, nodes, primary_root); + } if !self.is_online() { return self.adopt_stale_listing(ino, &folder_uid, cached, primary_root); } diff --git a/crates/pdfs-fuse/src/sim/daemon.rs b/crates/pdfs-fuse/src/sim/daemon.rs index 329b738..c8ddd0f 100644 --- a/crates/pdfs-fuse/src/sim/daemon.rs +++ b/crates/pdfs-fuse/src/sim/daemon.rs @@ -278,6 +278,7 @@ pub(crate) fn wait_until(limit: Duration, mut done: impl FnMut() -> bool) -> boo mod tests { use super::*; use crate::sim::fake_drive::{Entry, FakeDrive, Faults}; + use pdfs_core::control::RefreshScope; use std::collections::BTreeMap; #[test] @@ -644,6 +645,49 @@ mod tests { let _ = std::fs::remove_dir_all(&dir); } + #[test] + #[ignore = "mounts FUSE: run with `cargo test -p pdfs-fuse sim:: -- --ignored`"] + fn a_folder_whose_mkdir_is_queued_lists_after_a_refresh() { + // Once its listing was dropped, the folder was asked of Drive by its + // stand-in, and every listing and lookup in it failed with EIO until + // its mkdir landed (B125). + let drive = FakeDrive::new(); + let dir = scratch("queued-folder-refresh"); + let daemon = Daemon::start(&dir, drive.client(1, Faults::lan())).unwrap(); + let mnt = daemon.mountpoint.clone(); + let paused = |paused| Request::SetSyncPaused { + paused, + until: None, + }; + daemon.request(&paused(true)).unwrap(); + std::fs::create_dir(mnt.join("d")).unwrap(); + std::fs::write(mnt.join("d/f.txt"), b"queued").unwrap(); + let path = mnt.join("d").to_string_lossy().into_owned(); + let refresh = Request::Refresh { + scope: RefreshScope::Dir { path: path.clone() }, + }; + assert!(matches!(daemon.request(&refresh), Ok(Response::Ok { .. }))); + let Ok(Response::Entries { entries }) = daemon.request(&Request::ListDir { path }) else { + panic!("the daemon did not list d"); + }; + let names: Vec<_> = entries.iter().map(|entry| entry.name.as_str()).collect(); + assert_eq!(names, ["f.txt"]); + assert_eq!(std::fs::read(mnt.join("d/f.txt")).unwrap(), b"queued"); + daemon.request(&paused(false)).unwrap(); + assert!( + wait_until(Duration::from_secs(30), || { + daemon.pending().is_ok_and(|items| items.is_empty()) + && drive.tree().get("d/f.txt") + == Some(&Entry::File(Arc::new(b"queued".to_vec()))) + }), + "{:?} {:?}", + daemon.pending(), + drive.tree() + ); + 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_file_moved_while_its_create_was_on_the_wire_keeps_its_new_name() { diff --git a/docs/BUGS.md b/docs/BUGS.md index 709fdf5..279c2b7 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -1344,7 +1344,7 @@ profile fails on it when it comes up, which depends on timing. ## B125 — A folder made offline fails with `EIO` once its listing is invalidated -**Status:** Open. +**Status:** Fixed. **Found:** 2026-10-03, by the simulation runs (`sim::run`, profile one-client-flaky, seeds 3 and 4). Right after a restart, `rmdir y` (seed 3), and a write to a file in the client's folder (seed 4), failed with `EIO`. The daemon logged "enumerate folder children failed" with Drive's @@ -1362,8 +1362,17 @@ uid. Drive answers 404, and every lookup in the folder fails with `EIO` until it Right after it lands, Drive may not list the new folder yet (the lag of B118). Then the listing fails with a 404 for the real uid too. -**Test:** the simulation runs count it and go on. With `PDFS_SIM_KNOWN=fail` the flaky profile -fails on it. +**Fix.** A folder still known by its stand-in is listed from the database and never asked of +Drive. Until its mkdir lands the folder is only here; once it has, the database holds what Drive +does. The second case, a listing by the real uid that Drive does not show yet, needs a create +that landed without being read back: the read-back waits up to 1.25 s for Drive to list the +folder, longer than the lag of any simulation profile. The runs keep counting both until they +show neither again. + +**Test:** `a_folder_whose_mkdir_is_queued_lists_after_a_refresh` in +`crates/pdfs-fuse/src/sim/daemon.rs`: with sync paused, a folder is made with a file in it, a +refresh drops its listing, and it is listed again. With `PDFS_SIM_KNOWN=fail` the flaky profile +fails on it in the simulation runs; seeds 3 and 4 no longer reach it since phase 3. ## B124 — After a replacing rename fails, the file it was replacing is gone from the mount diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3690373..cb9d180 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -86,6 +86,8 @@ by its local id, in the queue, the stored tree and pins alike. - **A file read just as it reaches Drive no longer reads empty** (B179). - **Stopping the daemon no longer takes 10 seconds more than it needs** when an idle upload worker missed the stop (B180). +- **A folder made while offline or with sync paused no longer fails with "Input/output + error"** once it is refreshed or the connection comes back (B125). - **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, B172). diff --git a/docs/MILESTONE-3.0.0-STATUS.md b/docs/MILESTONE-3.0.0-STATUS.md index cb18c66..26b810b 100644 --- a/docs/MILESTONE-3.0.0-STATUS.md +++ b/docs/MILESTONE-3.0.0-STATUS.md @@ -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 B180. +- Fixes from the simulation and account runs since then: B125, B127 and B141 to B180. - **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`. @@ -89,8 +89,8 @@ Tracks `docs/MILESTONE-3.0.0.md`. Update it with every commit that moves a phase create was on the wire, which the mount showed under its old name (B161, fixed). Seed 4 failed on a folder removed and made again, whose create adopted the removed folder (B162, fixed). CI on 903452a passed every profile. - - [ ] Known-bug list in `sim/run.rs` is empty: B125 and B130 open; B127, B129 and B132 fixed - but unconfirmed. + - [ ] Known-bug list in `sim/run.rs` is empty: B130 open; B125, B127, B129 and B132 fixed but + unconfirmed. - [ ] A clean account run. The one on a6f36d5 (2026-10-04, LAN: 427 passed, 12 failed) failed B113 and B121 on every on-demand mount, two mirror-to-mirror move cases, and the journal check. B121 was a bug (B149, fixed); B113's case still expected creates