You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by xu...@apache.org on 2026/10/26 10:34:49 UTC
(opendal-yinyang) branch xuanwo/linux-fuse-writable updated (88cf08c -> 0048790)
This is an automated email from the ASF dual-hosted git repository.
Xuanwo pushed a change to branch xuanwo/linux-fuse-writable
in repository https://gitbox.apache.org/repos/asf/opendal-yinyang.git
from 88cf08c fix(fuse): honor per-write append and sync flags
add 5817e84 feat: refresh known kernel state on SIGHUP
add 745fdef fix: format refresh diagnostics without Display impls
add 0f0f838 refactor: track the SIGHUP loop inside teardown
add eb76502 fix: add the connection-ended errno classifier
add c97b801 build: record the Linux libc dependency in the lockfile
add 6851a7f fix: avoid re-polling the session handle during teardown
add 8ccf8ff fix: cover both teardown owners in the expiry diagnostic
new 0048790 Merge foundation 8ccf8ff into the writable layer
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
Cargo.lock | 1 +
Cargo.toml | 1 +
specs/linux-fuse.md | 46 ++++++++++++--
src/file_cli.rs | 6 +-
src/fuse/frontend.rs | 165 +++++++++++++++++++++++++++++++++++++++++++++++++--
src/fuse/inode.rs | 6 ++
src/fuse/mod.rs | 85 +++++++++++++++++---------
7 files changed, 271 insertions(+), 39 deletions(-)
(opendal-yinyang) 01/01: Merge foundation 8ccf8ff into the writable layer
Posted by xu...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
Xuanwo pushed a commit to branch xuanwo/linux-fuse-writable
in repository https://gitbox.apache.org/repos/asf/opendal-yinyang.git
commit 00487906de01260a7d73ccd27456b2aa1c5506b4
Merge: 88cf08c 8ccf8ff
Author: Xuanwo <gi...@xuanwo.io>
AuthorDate: Mon Oct 26 18:34:43 2026 +0800
Merge foundation 8ccf8ff into the writable layer
Keep shared staged-attribute lookup semantics while recording positive
lookup dentries for explicit refresh. Retain writable callback and contract
documentation alongside the notifier-backed SIGHUP lifecycle.
Co-authored-by: Chojan Shang <ps...@apache.org>
Cargo.lock | 1 +
Cargo.toml | 1 +
specs/linux-fuse.md | 46 ++++++++++++--
src/file_cli.rs | 6 +-
src/fuse/frontend.rs | 165 +++++++++++++++++++++++++++++++++++++++++++++++++--
src/fuse/inode.rs | 6 ++
src/fuse/mod.rs | 85 +++++++++++++++++---------
7 files changed, 271 insertions(+), 39 deletions(-)
diff --cc specs/linux-fuse.md
index 0eb2698,ea8024b..31e29c0
--- a/specs/linux-fuse.md
+++ b/specs/linux-fuse.md
@@@ -91,55 -89,43 +91,90 @@@ version remains coherent until a later
that open's metadata and content never disagree. The frontend does not call
`Mount::refresh` while references exist, because refresh rebases live handles;
a writable layer that in-place refreshes must first define its open-fd and
-kernel-cache invalidation policy.
+kernel-cache invalidation policy. Before a new open or a path attribute, the
+adapter retries reclaiming a clean unreferenced NodeId; a successful reclaim
+makes the next observation use the latest remote version, while Busy retains
+all still-pinned or dirty/frozen/error state.
+
+## Writable callbacks
+
+`write` stages bytes in the shared per-NodeId Mount handle. Each request uses
+the file flags supplied by the kernel, so `fcntl(F_SETFL)` may toggle
+`O_APPEND` and `O_SYNC`/`O_DSYNC` for later writes. An append request uses the
+node's append path, which chooses its offset under the same per-node lock as
+other writes; a regular write uses the kernel offset. The writable access mode
+is fixed by the open request. `O_TRUNC` stages a zero length before the open
+reply. A write with `O_SYNC`/`O_DSYNC` in its current flags attempts
+publication before acknowledging the write. Unsupported write flags return an
+explicit error instead of silently changing semantics.
+
+`flush` publishes a writable handle and reports the real remote outcome to the
+closing descriptor. `release` never publishes and never discards staging.
+Flush or fsync on a read-only reference is a successful no-op even when another
+writable reference shares the node, so a reader close cannot publish somebody
+else's dirty bytes. A failed publication remains attached to the shared handle
+and volume ledger; later fsync/flush retries the original frozen request.
+
+`create` and `mkdir` use the Runtime helpers that return the NodeId produced by
+the committing Planner, avoiding a post-commit path lookup. `unlink`, `rmdir`,
+and path renames use the path-addressed Runtime methods, so lookup and commit
+share one guarded Snapshot. Plain `rename` replaces an existing destination;
+`RENAME_NOREPLACE` uses the no-replace form. Exchange and whiteout renames are
+explicitly unsupported.
+
+`setattr` supports size and the core executable flag only. Size on an open
+descriptor is staged for that descriptor's later flush/fsync; path truncation
+publishes before returning success. A mode request is accepted only when it is
+one of the synthesized modes above (`0444`/`0555` or `0644`/`0755` according to
+read-only policy and the executable bit); other permission combinations return
+`EOPNOTSUPP`. Linux may include `S_IFREG`/`S_IFDIR` type bits in the mode;
+they are accepted when they match the node, while setuid/setgid/sticky and
+other permission combinations are rejected. Kernel bookkeeping timestamps
+attached to a chmod or truncate request are accepted and ignored, and the reply
+always carries revision-derived timestamps. Standalone timestamp changes and
+ownership changes remain explicitly unsupported. Before changing the
+executable flag, the shared node lock
+publishes dirty content, commits a Planner based on the handle's recorded
+content revision, and advances only that clean handle to the receipt revision.
+An external content change therefore produces Conflict rather than rebasing
+old staged bytes onto new content. Ownership, timestamps and other mode bits
+are not persisted.
+ ## Explicit refresh
+
+ `SIGHUP` triggers one explicit refresh; this is a manual invalidation entry
+ point, not an automatic change watch. There is no background polling, no
+ change-feed watcher, and no whole-tree enumeration. The refresh observes the
+ latest snapshot once and then:
+
+ - reclaims every clean, unreferenced node through the shared `Mount` contract,
+ so the next open materializes the current remote revision; nodes with live
+ references or retained state are left pinned (`Busy`);
+ - invalidates known inode attributes with `inval_inode(ino, -1, 0)` and
+ replied dentries with `inval_entry(parent, name)` through the fuser
+ `Notifier`. Only dentries this adapter actually replied to the kernel are
+ recorded: successful `LOOKUP` results. Negative lookups carry no timeout in
+ FUSE replies and are not recorded, and plain `READDIR` does not install
+ dentries without `READDIRPLUS`, so directory names are not recorded.
+
+ The adapter never calls `Mount::refresh`, because that replaces the shared
+ staged handle and would rebase open file descriptors. The refresh must also
+ hold no adapter lock while reclaiming nodes or sending notifications; targets
+ are collected under short locks, then the sends run on a blocking thread. At
+ most one refresh runs at a time and duplicate `SIGHUP`s while one is running
+ are coalesced by the signal stream. The SIGHUP loop is a tracked Tokio task:
+ the signal handler is registered synchronously before the mount becomes
+ visible, teardown sends it a stop signal, and the session join and the refresh
+ task share the same five-second grace period; a refresh already in progress
+ may finish inside that window. A `NotFound` notification result is expected
+ (the kernel already dropped the entry or inode) and is counted separately, so
+ only explicit connection-ended errors (`ENODEV`, `ENOTCONN`, `EPIPE`,
+ `ECONNRESET`, `ESHUTDOWN`) stop the batch before it would otherwise finish.
+
+ Stable `yy: fuse refresh:` log lines report the observed revision, the
+ reclaimed/active node counts, and the native notification counts at
+ completion, so an acceptance harness can confirm a refresh finished.
+
## Pinned directory cursors
`opendir` pins one `Snapshot` revision and one directory identity. `readdir`
@@@ -202,17 -190,15 +237,16 @@@ there is no mid-session cancellation si
- Every open materializes the complete file through the existing shared Mount
staging contract; there is no partial-download or mmap coherence claim, and
`FOPEN_DIRECT_IO` does not provide page-cache-backed mmap behavior.
-- Writes, create, mkdir, unlink, rename, links, xattrs, ioctl, poll, locking
- and capacity accounting are not implemented; the kernel read-only mount and
- the callback errno policy reject them.
+- Hard links, symlinks, mknod, xattrs, ioctl, poll, fallocate and capacity
+ accounting are not implemented and return explicit `EOPNOTSUPP`/`ENOSYS`.
+- The frontend does not negotiate `FUSE_POSIX_LOCKS` or `FLOCK_LOCKS`, and its
+ getlk/setlk/flock callbacks are not implemented. If the kernel still provides
+ POSIX or flock locks, they are local to this mount instance and carry no
+ cross-mount or cross-client meaning; no distributed lock semantics are
+ claimed. A writable mount must not claim that locking is rejected outright,
+ because the kernel may return success for its own local lock manager.
- - The shared Mount node map is not held across first materialization or other
- authority/staging I/O; the adapter adds no mount-wide lock across those
- awaits.
- - Extension points for the next layer are the runtime namespace operations,
- `Mount::refresh` guarded by reference counts, and the session `Notifier`
- for kernel entry/inode invalidation once an in-place refresh policy exists.
+ - The module keeps existing core Mount behavior, including the node-map lock
+ that already spans first materialization; the adapter adds no mount-wide
+ lock across authority or staging I/O.
+ - The session `Notifier` is used only by the explicit `SIGHUP` refresh above;
- there is no automatic external-change watch, and its capability set does
- not claim recoverable staging or any write-side operation beyond the
- read-only contract. Remaining extension points for the next layer are the
- runtime namespace operations and `Mount::refresh` guarded by reference
- counts.
++ there is no automatic external-change watch.
diff --cc src/fuse/frontend.rs
index 5253bb5,239982b..a4e7142
--- a/src/fuse/frontend.rs
+++ b/src/fuse/frontend.rs
@@@ -46,11 -47,12 +46,12 @@@ use super::inode::{CachedInfo, InodeTab
use crate::mount::{Mount, MountFile};
use fuser::{
AccessFlags, BsdFileFlags, FileAttr, FileHandle, FileType, Filesystem, FopenFlags, Generation,
- INodeNo, InitFlags, KernelConfig, LockOwner, OpenAccMode, OpenFlags, RenameFlags, ReplyAttr,
- ReplyCreate, ReplyData, ReplyDirectory, ReplyEmpty, ReplyEntry, ReplyOpen, ReplyStatfs,
- ReplyWrite, ReplyXattr, Request, TimeOrNow, WriteFlags,
- INodeNo, InitFlags, KernelConfig, LockOwner, Notifier, OpenAccMode, OpenFlags, ReplyAttr,
- ReplyData, ReplyDirectory, ReplyEmpty, ReplyEntry, ReplyOpen, ReplyStatfs, ReplyWrite,
- ReplyXattr, Request, TimeOrNow, WriteFlags,
++ INodeNo, InitFlags, KernelConfig, LockOwner, Notifier, OpenAccMode, OpenFlags, RenameFlags,
++ ReplyAttr, ReplyCreate, ReplyData, ReplyDirectory, ReplyEmpty, ReplyEntry, ReplyOpen,
++ ReplyStatfs, ReplyWrite, ReplyXattr, Request, TimeOrNow, WriteFlags,
};
- use std::collections::BTreeMap;
+ use std::collections::{BTreeMap, BTreeSet};
+ use std::ffi::OsStr;
use std::future::Future;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
@@@ -67,9 -69,12 +68,13 @@@ pub(super) struct Inner
inodes: Mutex<InodeTable>,
files: Mutex<BTreeMap<u64, Arc<OpenFile>>>,
dirs: Mutex<BTreeMap<u64, Arc<AsyncMutex<DirCursor>>>>,
+ /// Dentry names this adapter actually replied to the kernel; only those
+ /// can be cached and invalidated by an explicit refresh.
+ dentries: Mutex<BTreeSet<(u64, String)>>,
+ notifier: Mutex<Option<Notifier>>,
next_handle: AtomicU64,
runtime: Handle,
+ read_only: bool,
}
/// One kernel open maps to one reference on the node's shared staged version.
@@@ -141,9 -113,10 +146,11 @@@ impl Inner
inodes: Mutex::new(InodeTable::new(root)),
files: Mutex::new(BTreeMap::new()),
dirs: Mutex::new(BTreeMap::new()),
+ dentries: Mutex::new(BTreeSet::new()),
+ notifier: Mutex::new(None),
next_handle: AtomicU64::new(0),
runtime,
+ read_only,
}
}
@@@ -290,8 -366,13 +421,13 @@@
Ok(None) => return reply.error(fuser::Errno::ENOENT),
Err(error) => return reply.error(core_errno(&error)),
};
+ // A positive LOOKUP reply may install a kernel dentry; remember it
+ // for the explicit refresh. Negative results carry no timeout and are
+ // skipped, and a plain READDIR does not install positive dentries
+ // (that is READDIRPLUS), so scanned names are not recorded.
+ self.record_dentry(parent, &name);
- match self.attr_for(&snapshot, entry.node_id).await {
- Ok(attr) => {
+ match self.shared_attr(entry.node_id).await {
+ Ok(Some(attr)) => {
self.remember_lookup(attr.ino);
reply.entry(&ATTR_TTL, &attr, Generation(0));
}
diff --cc src/fuse/mod.rs
index 2ba4675,f655759..fda4ab0
--- a/src/fuse/mod.rs
+++ b/src/fuse/mod.rs
@@@ -90,26 -85,34 +90,38 @@@ pub async fn run
)
.into());
}
- let inner = Arc::new(frontend::Inner::new(
+ let inner = Arc::new(frontend::Inner::with_read_only(
mount,
tokio::runtime::Handle::current(),
+ read_only,
));
- let filesystem = frontend::FuseFrontend { inner };
+ let filesystem = frontend::FuseFrontend {
+ inner: inner.clone(),
+ };
let mut options = Config::default();
options.mount_options = vec![
- MountOption::RO,
MountOption::FSName(format!("yinyang-{}", config.name)),
MountOption::Subtype("yinyang".into()),
];
+ if read_only {
+ options.mount_options.insert(0, MountOption::RO);
+ }
options.acl = SessionACL::Owner;
+ // Register SIGHUP before the mount becomes externally visible so a
+ // refresh signal cannot fall through to the default handler while the loop
+ // task waits for its first poll.
+ let hangup = match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::hangup()) {
+ Ok(signal) => Some(signal),
+ Err(error) => {
+ eprintln!("yy: fuse refresh: cannot handle SIGHUP: {error}");
+ None
+ }
+ };
let session = Session::new(filesystem, &mountpoint, &options)?;
println!(
- "yy: mounted {} read-only at {} (fuse.yinyang)",
+ "yy: mounted {} {} at {} (fuse.yinyang)",
config.name,
+ if read_only { "read-only" } else { "read-write" },
mountpoint.display()
);
// `Session::spawn` moves the mount object into `BackgroundSession`, so a