diff --git a/crates/pdfs-fuse/src/lib.rs b/crates/pdfs-fuse/src/lib.rs index 0d97040..2459978 100644 --- a/crates/pdfs-fuse/src/lib.rs +++ b/crates/pdfs-fuse/src/lib.rs @@ -2437,7 +2437,24 @@ impl Core { /// Enumerate `ino`'s children from the remote and cache them. No-op if the /// directory has already been listed. Network I/O happens without the lock /// held so concurrent metadata reads aren't blocked behind a fetch. + /// + /// A listing that changed while it was being published answers EAGAIN: the + /// root listed again after a restart, an event, a newer refresh of a shared + /// folder. The listing is still owed, so it is taken again rather than + /// handed to the lookup or open that asked (B159). fn ensure_children(&self, ino: u64) -> Result<(), Errno> { + for _ in 1..LISTING_ATTEMPTS { + match self.list_children(ino) { + Err(e) if e.code() == Errno::EAGAIN.code() => { + debug!(ino, "listing changed while it was published; listing again"); + } + done => return done, + } + } + self.list_children(ino) + } + + fn list_children(&self, ino: u64) -> Result<(), Errno> { let (folder_uid, cached) = { let st = self.state(); match st.entries.get(&ino) { @@ -5777,6 +5794,10 @@ fn api_code(e: &(dyn std::error::Error + 'static)) -> Option { } } +/// How often [`Core::ensure_children`] takes a listing that changed while it +/// was being published. +const LISTING_ATTEMPTS: usize = 4; + /// How often a rename or move that Drive calls out of date is retried, and how /// long apart. const OUT_OF_DATE_RETRIES: u32 = 20; @@ -8776,11 +8797,28 @@ mod tests { assert!(changed.contains("self.note_self_change(uid)")); } + #[test] + fn a_listing_changed_while_published_is_taken_again() { + // The root listed again after a restart while a lookup was publishing + // the "Shared with me" entry, and the lookup answered EAGAIN (B159). + let ensure = function_source(include_str!("lib.rs"), "fn ensure_children("); + assert_before( + ensure, + "for _ in 1..LISTING_ATTEMPTS", + "Errno::EAGAIN.code()", + ); + assert_eq!(ensure.matches("self.list_children(ino)").count(), 2); + assert!( + !ensure.contains("?"), + "an attempt's error must not skip the retry" + ); + } + #[test] fn a_folder_lists_from_the_db_while_the_link_is_down() { // A listing dropped as stale mid-run, then a drop: every create in that // folder looks the name up first, and answered EIO for want of it. - let ensure = function_source(include_str!("lib.rs"), "fn ensure_children("); + let ensure = function_source(include_str!("lib.rs"), "fn list_children("); assert_before( ensure, "if !self.is_online()", diff --git a/docs/BUGS.md b/docs/BUGS.md index 282c535..bab1cea 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -12,6 +12,31 @@ Conventions: --- +## B159 — A write right after a restart fails with "Resource temporarily unavailable" + +**Status:** Fixed (unverified). +**Found:** 2026-10-04, by the simulation's flaky-link profile (`one_client_on_a_flaky_link`, +seed 4, one of ten runs on c2f9344). Half a second after the client restarted, a write into its +folder answered `EAGAIN`, and nothing was logged. The folder was still a queued create. + +**Where:** `Core::ensure_children` in `crates/pdfs-fuse/src/lib.rs`. + +**Cause.** A lookup in the root also places the "Shared with me" entry. It reads that entry's +plan from the database against a snapshot of the root listing, then publishes it only if the +listing has not changed meanwhile, and answers `EAGAIN` if it has. Right after a restart the +root is listed again while the first lookups come in, so a lookup could lose that race. Listing +a shared folder answers `EAGAIN` the same way when a newer refresh overtook it. No caller +retries, so the error reached the application. + +**Fix.** `ensure_children` takes a listing again, up to four times, when an attempt answers +`EAGAIN`, and logs each retry at debug level. + +**Test:** unit test `a_listing_changed_while_published_is_taken_again`. A stress test with +lookups and creates in the root at once did not hit the window (about 1,900 plans, none stale), +so the race itself has no regression test. + +--- + ## B158 — A file renamed twice in a row ends up under the first name, again **Status:** Fixed (unverified). diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0fad2c4..db692ad 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -46,6 +46,8 @@ by its local id, in the queue, the stored tree and pins alike. lost Drive's answer to reading back the file's last upload** (B157). - **A file renamed twice in a row, while the first name was still taken, keeps the second name** (B158). +- **Opening a file right after the daemon restarts no longer fails with "Resource temporarily + unavailable"** (B159). - **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 diff --git a/docs/MILESTONE-3.0.0-STATUS.md b/docs/MILESTONE-3.0.0-STATUS.md index 8d940cc..b96a7f3 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 B158. +- Fixes from the simulation and account runs since then: B127 and B141 to B159. - **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`. @@ -82,7 +82,9 @@ Tracks `docs/MILESTONE-3.0.0.md`. Update it with every commit that moves a phase again after its create lost its answer (B156, fixed). With sixteen drain workers (16fca10), seed 4 failed every replay on a write after an upload that was not read back (B157, fixed). Seed 3 then failed four of ten runs on two renames of one file - landing out of order (B158, fixed). + landing out of order (B158, fixed). Flaky-link seed 4 failed one of ten runs on a + lookup that answered `EAGAIN` while the root was listed again after a restart (B159, + fixed). - [ ] Known-bug list in `sim/run.rs` is empty: B125 and B130 open; B127, B129 and B132 fixed but unconfirmed. - [ ] A clean account run. The one on a6f36d5 (2026-10-04, LAN: 427 passed, 12 failed)