Skip to content

Let a tokio application drive FuseDevTask and take over a connection - #267

Merged
jiangliu merged 5 commits into
cloud-hypervisor:masterfrom
Kranzes:async-runtime-takeover
Oct 29, 2026
Merged

jiangliu merged 5 commits into
cloud-hypervisor:masterfrom
Kranzes:async-runtime-takeover

Conversation

@Kranzes

@Kranzes Kranzes commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

These changes let an application drive FuseDevTask from its own tokio runtime, and let a new server take over a connection that another one initialized, for example after a daemon restart that hands over the FUSE device fds.

  • core/async-io: let the application select the runtime type. Adds async_runtime::set_runtime_type(), so the application doesn't have to set FUSE_BACKEND_RS_ASYNC_RUNTIME with std::env::set_var, which is unsafe once other threads run.
  • fusedev: wait for requests instead of spinning on the tokio runtime. An idle FuseDevTask on tokio retried preadv() on EAGAIN in a loop and kept a CPU busy. It now waits until the fd is readable, and a 100ms timer lets an idle task see its state flag. Outside a tokio runtime the task keeps retrying reads as before.
  • core: let a server take over an initialized connection. Adds Server::init_state() and Server::restore_init(), so a server that never sees INIT parses requests with the options the old one negotiated.
  • fusedev: test async lookups, notifications and takeover on tokio. Mount tests for the three changes above, plus stopping an idle worker whose unmount fails and serving a mount without any tokio runtime.
  • fusedev: stop an async worker on a read error it can't retry. The async server logged read errors it didn't know and read again. With the wait above, a device that stays readable but fails every read made the task spin. EINVAL for a buffer smaller than the kernel's requests does that. It now retries ENOENT and stops on other errors, like the synchronous server.

fuse-backend-fusedev now needs tokio 1.53.3 for AsyncFd::register_with_interest(), because 1.53.3 deprecates with_interest() and make check runs clippy with -Dwarnings.

Dropping poll_handler() to stop an idle task is only safe on tokio, and the docs say so. tokio-uring leaves its read pending in the kernel, which then copies the next request into the freed buffer.

The mount tests need root or fusermount3 and skip without them. I ran them in a user namespace on Linux 7.2.10 with both runtimes.

@jiangliu

Copy link
Copy Markdown
Collaborator

@Kranzes The GitHub runner recently upgraded its kernel, which is causing CI test failures. Could you please help rebase the PR to resolve this?

The crate detects the runtime type on first use, and only the
FUSE_BACKEND_RS_ASYNC_RUNTIME environment variable overrides it. A
daemon that drives FuseDevTask from its own tokio runtime has to pick
tokio, because that runtime can't poll tokio-uring files. Setting the
variable from the daemon needs std::env::set_var, which is unsafe once
other threads may read the environment.

Add async_runtime::set_runtime_type() to select the type in code. It
overrides the variable and fails if the type in effect is a different
one. Add async_runtime::runtime_type() to read the type, and make
RuntimeType public for both.

Signed-off-by: Ilan Joselevich <personal@ilanjoselevich.com>
With the tokio runtime, FuseDevTask reads the nonblocking fuse device
with plain preadv(). While no request is pending, the read fails with
EAGAIN, and poll_handler() yields and reads again right away, so an
idle task keeps a CPU busy.

Register the fd with the runtime's IO driver and wait until it is
readable before each read and after EAGAIN. Wait for errors too,
because the kernel reports a torn down connection with EPOLLERR alone.
A 100ms timer wakes the wait, so an idle task still returns once its
state flag is set. Without the timer, stopping a worker whose unmount
fails would hang. An error that isn't an OS error, such as the runtime
shutting down, ends the task. The io_uring runtime is unchanged.
AsyncFd::register_with_interest() needs tokio 1.53.3, which deprecates
with_interest().

Registering panics outside of a tokio runtime, so a task that another
executor polls keeps retrying reads as before. The docs say that
dropping poll_handler() to stop an idle task is only safe with the
tokio runtime. tokio-uring leaves its read pending in the kernel,
which then copies the next request into the freed buffer.

One test checks that an idle task uses little CPU and returns on
unmount. Before this change it used all of the 500ms it idled. Another
checks that the state flag stops an idle task while the mount stays
up. A third serves a mount without any tokio runtime.

Signed-off-by: Ilan Joselevich <personal@ilanjoselevich.com>
A daemon can hand its FUSE device fds to a new process, which then
serves the connection without a FUSE_INIT. The new Server can't learn
what the old one negotiated, so it parses requests as if no option
were enabled, such as the FUSE_EXT_GROUPS extension of create
requests.

Add Server::init_state() to read the protocol version and the options
enabled at INIT, and Server::restore_init() to set them on the new
server. Add abi::fuse_abi::UNIQUE_RESEND, the bit the kernel sets in
requests that notify_resend() makes it send again. The server already
replies with the unique id unchanged, and a test now covers that.

Document that a thread that serves requests must not call
notify_inval_entry(). It waits for the parent's lock, which a lookup
in that directory holds until its reply.

Signed-off-by: Ilan Joselevich <personal@ilanjoselevich.com>
Add tests that serve a mount with FuseDevTask on a current-thread
tokio runtime, from a file system that can make a lookup wait. They
cover two cases:

- A waiting lookup doesn't hold up another lookup in the same
  directory, and notify_inval_entry() from another thread works while
  the task serves.
- A second server takes over the connection from one that stopped with
  a lookup in flight. It restores the INIT state, sends
  notify_resend() and answers the lookup that the kernel sends again.

The file system enables FUSE_PARALLEL_DIROPS, because without it the
kernel serializes lookups in a directory.

Signed-off-by: Ilan Joselevich <personal@ilanjoselevich.com>
The async server logged a read error it didn't know and read again.
Now that a tokio task waits for the device to become readable, a device
that stays readable but fails every read made the task spin at full CPU
and log on every pass. EINVAL for a buffer smaller than the kernel's
requests does that.

Retry ENOENT, which the kernel returns when a request was interrupted
before the read, and stop the task on any other error. The synchronous
server does the same.

Signed-off-by: Ilan Joselevich <personal@ilanjoselevich.com>
@Kranzes
Kranzes force-pushed the async-runtime-takeover branch from c392fce to a51ee32 Compare October 28, 2026 14:52
@jiangliu
jiangliu merged commit 68490ea into cloud-hypervisor:master Oct 29, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants