Skip to content

FUSE3: do not clear fuse_config in init - #110

Open
peerbit-org wants to merge 1 commit into
winfsp:masterfrom
dao-xyz:fuse3-keep-fuse-config
Open

peerbit-org wants to merge 1 commit into
winfsp:masterfrom
dao-xyz:fuse3-keep-fuse-config

Conversation

@peerbit-org

Copy link
Copy Markdown

On FUSE 3, hostAsgnCconfig zeroes the struct fuse_config that libfuse passes to the init callback:

static inline void hostAsgnCconfig(struct fuse_config *conf, bool direct_io, bool use_ino)
{
	memset(conf, 0, sizeof *conf);   // <- wipes the parsed configuration
	conf->direct_io = direct_io;
	conf->use_ino = use_ino;
}

In libfuse 3 the high-level init receives &f->conf, the live configuration that fuse_new already filled from defaults and -o options. Clearing it sets entry_timeout, attr_timeout and negative_timeout to 0 for the whole mount, so -o entry_timeout=…,attr_timeout=… are silently ignored on FUSE 3. The kernel then issues a LOOKUP for every path component on every path syscall, and a GETATTR for every stat.

This PR removes the memset and keeps setting only the fields cgofuse owns (direct_io, use_ino). FUSE 2 builds are unaffected, because the function is empty there.

Measured on Linux (Ubuntu 24.04, libfuse 3.14, kernel 6.17) with a cgofuse-based filesystem, counting filesystem Getattr callbacks:

stock with this change and -o entry_timeout=0.1,attr_timeout=0,negative_timeout=0
stat of a 2-component path 3 callbacks 1
git status on 2,000 files 14,084 4,974
  • Passing the same -o options to the stock build leaves the counts unchanged, which shows they were being wiped.
  • A libfuse 2 build of the same filesystem (defaults 1/1/0) matches the patched FUSE 3 build at 1/1/0.

One behaviour change for FUSE 3 users: without explicit -o options they now get libfuse's defaults (entry and attr timeouts of 1 s) instead of 0. That matches FUSE 2 and macFUSE, and filesystems that need uncached attributes can pass -o attr_timeout=0.

hostAsgnCconfig zeroed the struct fuse_config that libfuse 3 passes to
init. That struct is the live configuration parsed by fuse_new, so the
memset silently reset entry_timeout, attr_timeout and negative_timeout
(and any other -o option) to 0 on every FUSE 3 mount. The kernel then
sends a LOOKUP per path component and a GETATTR per stat, and
-o entry_timeout=/attr_timeout= options have no effect.

Keep libfuse's parsed configuration and only set the fields cgofuse
owns (direct_io, use_ino). FUSE 2 builds are unaffected (the function
is empty there).
peerbit-org added a commit to dao-xyz/peerbit-examples that referenced this pull request Sep 29, 2026
* perf(shared-fs): cache Linux mount names for 0.1 s

cgofuse v1.6.0's hostAsgnCconfig memsets libfuse 3's live fuse_config at
init, so every entry/attr/negative timeout was 0 on Linux and each lstat
cost one LOOKUP per path component plus a GETATTR (~0.2 ms each over IPC).

- go.mod replaces cgofuse with github.com/dao-xyz/cgofuse
  v1.6.0-peerbit.1 (v1.6.0 minus the memset line; winfsp/cgofuse#110 is
  open so the replace can be dropped later). macOS (FUSE 2.8) and Windows
  (nocgo WinFsp) compile the same code as before.
- Linux mounts with -o entry_timeout=0.1,attr_timeout=0,negative_timeout=0:
  names cached for at most 0.1 s, attributes and missing names never. The
  fork and the -o land together; the fork alone would give libfuse's 1 s
  defaults. macOS keeps macFUSE's 1/1/0 defaults, Windows is unchanged.
- On Linux, Open maps the daemon's ENOENT to -ESTALE (116), so a create
  without O_EXCL on a name another peer removed within the name cache
  window is retried by the kernel with a fresh lookup instead of failing.
  Getattr (which also answers LOOKUP), Create and every other callback
  keep ENOENT. Profiles name the result ESTALE and the summarizer counts
  it as absent.
- Native and CLI READMEs document each platform's caching and staleness;
  changeset for @peerbit/shared-fs-cli (patch). The release script
  publishes the matching shared-fs-native-v<version> adapter for the new
  CLI version.

Measured on a GitHub Linux runner (runs 36515914255, 36526596414): stat
of a 2-component path 3 -> 1 adapter callbacks, git status on 2,000 files
2.91 s -> 1.23 s, git clone 35.6 s -> 27.0 s; other peers' edits, deletes
and renames visible with no added delay, a file replaced by a directory
can fail once with EIO (next call correct), create right after a remote
delete 0/10 failures, no lost appends. A local Linux FUSE container run
of this commit reproduced the probe and showed 10/10 create failures at
0.1 s without the ESTALE mapping.

* test(shared-fs): guard the Linux kernel cache policy in the profiled smoke

When the external native smoke runs on Linux with a mount profile
(PEERBIT_SHARED_FS_NATIVE_MOUNT_PROFILE_DIR, the Linux smoke workflow's
mount_profile passes), it stats docs/hello.txt twice 20 ms apart after the
cached names expired and, after unmount flushes the adapter profile,
requires exactly one getattr callback inside the second stat's window.
3 means the -o timeouts did not reach the kernel (a cgofuse that clears
libfuse's config again); 0 means attributes are cached. Unprofiled runs
and macOS are unchanged.

The Linux mounted benchmark also records the -o mount options it runs
with.

Checked on a local Linux FUSE 3.14 container (Docker, kernel 6.12): the
guard passes on this branch, and fails with 3 on stock cgofuse and with 0
on the fork without the -o.

* docs(shared-fs): scope the Linux name-cache staleness and stat-cost claims

The native README and the changeset said a file/directory swap by another
peer makes one operation fail with EIO and the next call succeed, and that a
two-component stat costs one adapter callback instead of three. Both held
only for what the probes measured.

- Type changes: only a stat (GETATTR type mismatch) marks the inode bad, so
  it fails once with EIO and the next call is correct (measured, file ->
  directory). Opening the stale name (OPEN/OPENDIR answer EISDIR, ENOTDIR or
  EINVAL), following a stale symlink (readlink EINVAL) and walking a path
  through it (link_path_walk ENOTDIR) leave the dentry cached and can fail on
  every try for up to 0.1 s. Symlink swaps are now covered too. Marked as not
  measured. No errno mapping beyond ENOENT -> ESTALE.
- Cost: one callback instead of three only for a stat repeated within 0.1 s
  of the path's last lookup; after the names expire the next stat again
  costs one per component plus one. git status/clone figures unchanged.

Docs only; prettier --check passes.

---------

Co-authored-by: peerbit-org <273107789+peerbit-org@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant