fix(fuse): a kernel notice sent from the runtime hands its worker's tasks on (B130)

This commit is contained in:
nvrl committed 2026-10-05 00:02:56 +02:00
1 parent e743cd60cf
commit a68dae9422
4 files changed
+79 -17

No files matched your search

+64 -10
View File
@@ -1364,24 +1364,42 @@ impl KernelChannel {
pub(crate) fn inval_inode(&self, ino: u64) -> std::io::Result<()> { pub(crate) fn inval_inode(&self, ino: u64) -> std::io::Result<()> {
let ino = filesystem::kernel_ino(self.root, ino); let ino = filesystem::kernel_ino(self.root, ino);
self.notifier.inval_inode(ino, 0, 0) off_the_runtime(|| self.notifier.inval_inode(ino, 0, 0))
} }
/// Drop the cached attributes of `ino` but not its page cache. /// Drop the cached attributes of `ino` but not its page cache.
fn inval_attrs(&self, ino: u64) -> std::io::Result<()> { fn inval_attrs(&self, ino: u64) -> std::io::Result<()> {
let ino = filesystem::kernel_ino(self.root, ino); let ino = filesystem::kernel_ino(self.root, ino);
self.notifier.inval_inode(ino, -1, 0) off_the_runtime(|| self.notifier.inval_inode(ino, -1, 0))
} }
pub(crate) fn inval_entry(&self, parent: u64, name: &OsStr) -> std::io::Result<()> { pub(crate) fn inval_entry(&self, parent: u64, name: &OsStr) -> std::io::Result<()> {
let parent = filesystem::kernel_ino(self.root, parent); let parent = filesystem::kernel_ino(self.root, parent);
self.notifier.inval_entry(parent, name) off_the_runtime(|| self.notifier.inval_entry(parent, name))
} }
fn delete(&self, parent: u64, child: u64, name: &OsStr) -> std::io::Result<()> { fn delete(&self, parent: u64, child: u64, name: &OsStr) -> std::io::Result<()> {
let parent = filesystem::kernel_ino(self.root, parent); let parent = filesystem::kernel_ino(self.root, parent);
let child = filesystem::kernel_ino(self.root, child); let child = filesystem::kernel_ino(self.root, child);
self.notifier.delete(parent, child, name) off_the_runtime(|| self.notifier.delete(parent, child, name))
}
}
/// Send a notice to the kernel without holding up the runtime.
///
/// The kernel answers an invalidation only once it has the pages' locks, and
/// a read holds those until the daemon answers it. Sent from a runtime worker,
/// the notice blocked that worker in the kernel; when it was the one driving
/// the timers and I/O, nothing drove them any more, so the read's fetch never
/// finished and the daemon hung (`docs/BUGS.md` B130). On a worker the notice
/// hands the worker's tasks to another thread first; anywhere else it is sent
/// as it is.
fn off_the_runtime<T>(notice: impl FnOnce() -> T) -> T {
match tokio::runtime::Handle::try_current() {
Ok(handle) if handle.runtime_flavor() == tokio::runtime::RuntimeFlavor::MultiThread => {
tokio::task::block_in_place(notice)
}
_ => notice(),
} }
} }
@@ -7319,12 +7337,12 @@ mod tests {
SELF_CHANGE_TTL_MS, ShareId, SharedWithMeItem, StateRegistry, VirtualRootPlan, SELF_CHANGE_TTL_MS, ShareId, SharedWithMeItem, StateRegistry, VirtualRootPlan,
accepted_share_provenance, conflict_name, copy_pending_for_truncate, expand_restore, accepted_share_provenance, conflict_name, copy_pending_for_truncate, expand_restore,
first_seen_in_trash, fuse_name, is_stale_mount, node_visible, note_self_change, first_seen_in_trash, fuse_name, is_stale_mount, node_visible, note_self_change,
orphan_past_retention, parse_node_uid, prepare_shared_roots, preserve_on_access_denied, off_the_runtime, orphan_past_retention, parse_node_uid, prepare_shared_roots,
publish_virtual_root_in_listing, reconcile_virtual_root_in_listing, preserve_on_access_denied, publish_virtual_root_in_listing,
release_can_discard_unlinked, release_must_retain_queued_trash, release_unlinked_entry, reconcile_virtual_root_in_listing, release_can_discard_unlinked,
rename_needs_queue, require_node_parent_access, require_rename_access, release_must_retain_queued_trash, release_unlinked_entry, rename_needs_queue,
resolve_anywhere_with, shared_with_me_uid, take_self_change, uid_write_authority, require_node_parent_access, require_rename_access, resolve_anywhere_with,
virtual_node, shared_with_me_uid, take_self_change, uid_write_authority, virtual_node,
}; };
use super::{Db, WriteAuthority}; use super::{Db, WriteAuthority};
use std::time::Duration; use std::time::Duration;
@@ -9000,6 +9018,42 @@ mod tests {
assert!(remote.contains("async { tokio::time::timeout(budget, fetch).await }")); assert!(remote.contains("async { tokio::time::timeout(budget, fetch).await }"));
} }
#[test]
fn a_kernel_notice_sent_from_the_runtime_leaves_its_timers_running() {
// A notice waits in the kernel for a read whose fetch waits on a
// timer of the same runtime (B130). Here the runtime's one worker sends
// it, and the timer has to fire for the notice to return.
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_all()
.build()
.unwrap();
let (fetched, fetch) = std::sync::mpsc::channel();
rt.spawn(async move {
tokio::time::sleep(Duration::from_millis(10)).await;
let _ = fetched.send(());
});
let notice =
rt.spawn(async move { off_the_runtime(|| fetch.recv_timeout(Duration::from_secs(5))) });
assert!(rt.block_on(notice).unwrap().is_ok());
// Every notice to a session goes through it.
let lib = include_str!("lib.rs");
for method in [
"fn inval_inode(&self",
"fn inval_attrs(&self",
"fn inval_entry(&self",
"fn delete(&self, parent: u64",
] {
let source = function_source(lib, method);
assert_eq!(source.matches("self.notifier.").count(), 1, "{method}");
assert!(
source.contains("off_the_runtime(|| self.notifier."),
"{method}"
);
}
}
#[test] #[test]
fn a_deadline_built_under_block_on_needs_no_ambient_runtime() { fn a_deadline_built_under_block_on_needs_no_ambient_runtime() {
let rt = tokio::runtime::Builder::new_multi_thread() let rt = tokio::runtime::Builder::new_multi_thread()
+11 -4
View File
@@ -1218,7 +1218,7 @@ timing, and then on the refused write.
## B130 — A read of a file that a remote change invalidates can hang the daemon ## B130 — A read of a file that a remote change invalidates can hang the daemon
**Status:** Open. **Status:** Fixed.
**Found:** 2026-10-03, by the simulation runs (`sim::run`, profile one-client-flaky, seed 1). **Found:** 2026-10-03, by the simulation runs (`sim::run`, profile one-client-flaky, seed 1).
After the last step, a read of `c0/z/f.txt` never returned. The watchdog's stacks showed the After the last step, a read of `c0/z/f.txt` never returned. The watchdog's stacks showed the
read's worker in `read_range_remote`, waiting in `block_on` for its fetch, and a runtime worker read's worker in `read_range_remote`, waiting in `block_on` for its fetch, and a runtime worker
@@ -1239,9 +1239,16 @@ sleep on a condvar. The read's fetch waits on a timer (here the fake's latency,
HTTP response), so it never finishes and never lets go of the page lock. The daemon stays stuck HTTP response), so it never finishes and never lets go of the page lock. The daemon stays stuck
until some other work wakes a worker, and on an idle machine that may never happen. until some other work wakes a worker, and on an idle machine that may never happen.
**Test:** the simulation runs recognise it in the watchdog's stacks (a `tokio-rt-worker` in **Fix.** Every notice to the kernel goes through `off_the_runtime`, which on a runtime worker
state `D` inside `NotifyBatch::flush`), count it and end the seed there. With sends it from inside `block_in_place`: the worker's tasks, and the driver with them, go to
`PDFS_SIM_KNOWN=fail`, seed 1 of one-client-flaky fails on it. another thread while it waits. Anywhere else the notice is sent as before.
**Test:** `a_kernel_notice_sent_from_the_runtime_leaves_its_timers_running` in
`crates/pdfs-fuse/src/lib.rs`: the one worker of a runtime sends a notice that returns only once
a timer of that runtime has fired. The simulation runs still recognise it in the watchdog's
stacks (a `tokio-rt-worker` in state `D` inside `NotifyBatch::flush`); with
`PDFS_SIM_KNOWN=fail`, seed 1 of one-client-flaky fails on it, though it no longer reaches it
since phase 3.
## B129 — A file deleted or replaced while its create is on the wire stays on Drive ## B129 — A file deleted or replaced while its create is on the wire stays on Drive
+1
View File
@@ -88,6 +88,7 @@ by its local id, in the queue, the stored tree and pins alike.
worker missed the stop (B180). worker missed the stop (B180).
- **A folder made while offline or with sync paused no longer fails with "Input/output - **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). error"** once it is refreshed or the connection comes back (B125).
- **The daemon no longer hangs when a file being read changes on Drive** (B130).
- **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).
+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 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: B125, B127 and B141 to B180. - Fixes from the simulation and account runs since then: B125, B127, B130 and B141 to B180.
- **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`.
@@ -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). 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 Seed 4 failed on a folder removed and made again, whose create adopted the removed
folder (B162, fixed). CI on 903452a passed every profile. folder (B162, fixed). CI on 903452a passed every profile.
- [ ] Known-bug list in `sim/run.rs` is empty: B130 open; B125, B127, B129 and B132 fixed but - [ ] Known-bug list in `sim/run.rs` is empty: none open; B125, B127, B129, B130 and B132
unconfirmed. fixed but not yet confirmed by the runs.
- [ ] A clean account run. The one on a6f36d5 (2026-10-04, LAN: 427 passed, 12 failed) - [ ] 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 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 the journal check. B121 was a bug (B149, fixed); B113's case still expected creates