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 07:22:54 UTC
(opendal-yinyang) 02/02: feat(mount): expose staged metadata and a reportable flush contract
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 8d677f151932513e808a14571832ec10768b06d4
Author: Xuanwo <gi...@xuanwo.io>
AuthorDate: Mon Oct 26 15:22:35 2026 +0800
feat(mount): expose staged metadata and a reportable flush contract
Add Mount::node_status for read-only shared staging state and MountFile::flush as the publication attempt for frontend close paths. Releasing a reference stays non-publishing; pending, frozen and failed states remain recoverable.
Co-authored-by: Chojan Shang <ps...@apache.org>
---
specs/mount-runtime.md | 14 ++++++++++
src/mount.rs | 17 ++++++++++++
tests/mount.rs | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 101 insertions(+)
diff --git a/specs/mount-runtime.md b/specs/mount-runtime.md
index ad05d60..70ac3a9 100644
--- a/specs/mount-runtime.md
+++ b/specs/mount-runtime.md
@@ -26,6 +26,20 @@ Instance-wide `fsync` visits all nodes, continues after individual failures, and
returns the first failure. Every failure remains in runtime status. A successful
local write or an OS close that cannot report errors is not remote completion.
+`flush` is the named publication attempt for a frontend close path that can
+report errors. It performs the same conditional publication as `fsync` and
+never reports success before the remote result is durable. Dropping or
+releasing the reference is different: it performs no I/O, publication or
+staging deletion. Pending, frozen and failed states remain recoverable, and
+`reclaim` rejects them. A frontend whose release callback cannot report errors
+must not use release as an implicit flush; if its kernel omits a reportable
+flush, the bytes remain staged rather than being acknowledged as remote.
+
+`node_status` observes the shared staged metadata for one NodeId without
+opening another reference. It reports the same staged length and
+pending/frozen/error state used by reads, so attribute paths do not have to
+materialize or refresh a file merely to inspect its accepted local state.
+
`refresh` replaces a clean staged observation with a fresh remote version for
all references. It rejects pending, frozen and failed states; it never silently
rebases writes. Failure to fetch leaves the old version available. Interrupted
diff --git a/src/mount.rs b/src/mount.rs
index 7b899b9..58baa9f 100644
--- a/src/mount.rs
+++ b/src/mount.rs
@@ -85,6 +85,18 @@ impl Mount {
pub async fn status(&self) -> Result<RuntimeStatus> {
self.inner.runtime.status().await
}
+ /// Observe staged metadata for one shared node without opening a reference.
+ /// `None` means this mount has no shared handle for the node.
+ pub async fn node_status(&self, id: NodeId) -> Result<Option<HandleStatus>> {
+ let node = {
+ let nodes = self.inner.nodes.lock().await;
+ nodes.get(&id).cloned()
+ };
+ let Some(node) = node else {
+ return Ok(None);
+ };
+ node.handle.lock().await.status().await.map(Some)
+ }
/// Namespace operations use the same conditional runtime contract. Do not
/// open private FileHandles through this runtime alongside Mount references.
pub fn namespace(&self) -> &Runtime {
@@ -223,4 +235,9 @@ impl MountFile {
pub async fn fsync(&self) -> Result<Option<Receipt>> {
self.node.handle.lock().await.fsync().await
}
+ /// Publish staged bytes for a FUSE flush or close that can report errors.
+ /// Dropping or releasing the reference never performs this operation.
+ pub async fn flush(&self) -> Result<Option<Receipt>> {
+ self.fsync().await
+ }
}
diff --git a/tests/mount.rs b/tests/mount.rs
index aea36f1..338a786 100644
--- a/tests/mount.rs
+++ b/tests/mount.rs
@@ -168,3 +168,73 @@ async fn frozen_mount_write_recovers_original_request() {
mount.reclaim(id).await.unwrap();
assert!(mount.status().await.unwrap().handles.is_empty());
}
+
+#[tokio::test]
+async fn flush_publishes_but_reference_release_does_not() {
+ let backend = TestBackend::default();
+ let authority: Arc<dyn Authority> = Arc::new(
+ Fs::create(backend.operator(), BackendProfile::Minio)
+ .await
+ .unwrap(),
+ );
+ let temp = tempfile::tempdir().unwrap();
+ let mount = Mount::open(authority.clone(), temp.path(), false)
+ .await
+ .unwrap();
+ let (pending, _) = mount
+ .namespace()
+ .create_file_with_node(authority.root(), "pending", false)
+ .await
+ .unwrap();
+ assert!(mount.node_status(pending).await.unwrap().is_none());
+
+ let file = mount.open_node(pending, true).await.unwrap();
+ file.write(0, b"pending").await.unwrap();
+ let state = mount.node_status(pending).await.unwrap().unwrap();
+ assert_eq!(state.length, 7);
+ assert!(state.pending);
+ assert!(
+ authority
+ .observe_latest()
+ .await
+ .unwrap()
+ .open_file(pending)
+ .await
+ .unwrap()
+ .read(0, 7)
+ .await
+ .unwrap()
+ .is_empty()
+ );
+
+ // Releasing the shared reference performs no remote publication.
+ drop(file);
+ assert!(mount.node_status(pending).await.unwrap().unwrap().pending);
+ assert_eq!(
+ mount.reclaim(pending).await.unwrap_err().kind(),
+ ErrorKind::Busy
+ );
+
+ let file = mount.open_node(pending, true).await.unwrap();
+ assert_eq!(file.read(0, 7).await.unwrap(), b"pending");
+ file.flush().await.unwrap().unwrap();
+ assert!(!mount.node_status(pending).await.unwrap().unwrap().pending);
+ assert_eq!(
+ authority
+ .observe_latest()
+ .await
+ .unwrap()
+ .open_file(pending)
+ .await
+ .unwrap()
+ .read(0, 7)
+ .await
+ .unwrap(),
+ b"pending"
+ );
+
+ drop(file);
+ assert!(mount.node_status(pending).await.unwrap().is_some());
+ mount.reclaim(pending).await.unwrap();
+ assert!(mount.node_status(pending).await.unwrap().is_none());
+}