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 09:06:35 UTC
(opendal-yinyang) branch xuanwo/linux-fuse-acceptance updated: test: cover busy SIGTERM grace and open-fd teardown
This is an automated email from the ASF dual-hosted git repository.
Xuanwo pushed a commit to branch xuanwo/linux-fuse-acceptance
in repository https://gitbox.apache.org/repos/asf/opendal-yinyang.git
The following commit(s) were added to refs/heads/xuanwo/linux-fuse-acceptance by this push:
new 90e873c test: cover busy SIGTERM grace and open-fd teardown
90e873c is described below
commit 90e873c913d98aa3de211a5b37e0970910d5674d
Author: Xuanwo <gi...@xuanwo.io>
AuthorDate: Mon Oct 26 17:06:28 2026 +0800
test: cover busy SIGTERM grace and open-fd teardown
Parameterize pending-fsync interruption by SIGKILL or SIGTERM, require forced non-zero exit within 10s with a held descriptor or frozen request, verify fd I/O errors and lease release, and check same-mount fresh reopens after external changes.
Co-authored-by: Chojan Shang <ps...@apache.org>
---
docs/linux-fuse-acceptance.md | 5 +-
tests/linux_fuse/acceptance.rs | 132 +++++++++++++++++++++++++++++++++++++++--
tests/linux_fuse/harness.rs | 13 ++++
3 files changed, 143 insertions(+), 7 deletions(-)
diff --git a/docs/linux-fuse-acceptance.md b/docs/linux-fuse-acceptance.md
index f4150b4..2f7ce25 100644
--- a/docs/linux-fuse-acceptance.md
+++ b/docs/linux-fuse-acceptance.md
@@ -96,7 +96,10 @@ cargo x test-linux-fuse --read-only
after the descriptor is released.
- Unmount lifecycle: after a successful external `fusermount3 -u`, `yy` exits
on its own, removes the mount entry, and releases the staging lease. A
- separate SIGTERM scenario verifies the same process and lease lifecycle.
+ separate clean SIGTERM scenario verifies the same process and lease
+ lifecycle. Busy SIGTERM with an open descriptor or a pending fsync must exit
+ through the forced non-zero grace path within 10s, leave the held descriptor
+ with I/O errors, release the lease, and allow the same staging to remount.
The assertions follow the core specs. They do not add POSIX guarantees that the
transactional filesystem does not provide; for example symlinks are rejected,
diff --git a/tests/linux_fuse/acceptance.rs b/tests/linux_fuse/acceptance.rs
index 6f0a8df..7e03dc5 100644
--- a/tests/linux_fuse/acceptance.rs
+++ b/tests/linux_fuse/acceptance.rs
@@ -18,7 +18,7 @@
//! Real /dev/fuse acceptance scenarios for the YinYang mount adapter.
use crate::harness::{
- Fixture, Frontend, Publication, assert_errno, run_external, sync_all_with_deadline,
+ Fixture, Frontend, Mounted, Publication, assert_errno, run_external, sync_all_with_deadline,
};
use nix::unistd::truncate;
use std::fs::{self, File, OpenOptions};
@@ -36,8 +36,16 @@ const EPERM: i32 = 1;
const EOPNOTSUPP: i32 = 95;
const EROFS: i32 = 30;
const EINVAL: i32 = 22;
+const ENOTCONN: i32 = 107;
+const EIO: i32 = 5;
const ATTR_TTL_WAIT: Duration = Duration::from_millis(1500);
+#[derive(Clone, Copy)]
+enum Interrupt {
+ Kill,
+ Terminate,
+}
+
#[test]
#[ignore = "requires /dev/fuse and isolated S3; run with cargo x test-linux-fuse"]
fn fuse_read_only_both_publications() {
@@ -718,6 +726,19 @@ fn external_changes(fixture: &Fixture) {
"pread past the old EOF must not leak bytes from the externally grown file"
);
drop(old);
+ let mut fresh = File::open(mounted.mountpoint().join("external.txt"))
+ .expect("reopen externally grown file on the live mount");
+ let mut fresh_bytes = Vec::new();
+ fresh
+ .read_to_end(&mut fresh_bytes)
+ .expect("read externally grown file on the live mount");
+ assert_eq!(fresh_bytes, b"BBBBBBBB");
+ assert_eq!(
+ fresh.metadata().expect("fstat fresh grow").len(),
+ 8,
+ "fresh open must materialize the external grow length"
+ );
+ drop(fresh);
let dirty_path = mounted.mountpoint().join("dirty.txt");
let mut dirty = OpenOptions::new()
@@ -783,6 +804,19 @@ fn external_changes(fixture: &Fixture) {
);
assert_eq!(&tail, b"89");
drop(old_shrink);
+ let mut fresh_shrink = File::open(reopen.mountpoint().join("shrink.txt"))
+ .expect("reopen externally shrunk file on the live mount");
+ let mut fresh_shrink_bytes = Vec::new();
+ fresh_shrink
+ .read_to_end(&mut fresh_shrink_bytes)
+ .expect("read externally shrunk file on the live mount");
+ assert_eq!(fresh_shrink_bytes, b"0123");
+ assert_eq!(
+ fresh_shrink.metadata().expect("fstat fresh shrink").len(),
+ 4,
+ "fresh open must materialize the external shrink length"
+ );
+ drop(fresh_shrink);
reopen.unmount();
let mountpoint = fixture
@@ -973,8 +1007,30 @@ fn fuse_daemon_crash_interrupts_pending_fsync_service() {
let config = fixture.volume_config("stage-interrupted", Frontend::Fuse, false);
let mountpoint = fixture.temp.path().join("mnt-interrupted");
fs::create_dir(&mountpoint).expect("create mountpoint");
- let mut mounted = fixture.mount(&config, &mountpoint);
+ let mounted = fixture.mount(&config, &mountpoint);
+ interrupt_pending_fsync(&fixture, &library, &config, mounted, Interrupt::Kill);
+}
+
+#[test]
+#[ignore = "requires /dev/fuse and isolated S3; run with cargo x test-linux-fuse"]
+fn fuse_sigterm_pending_fsync_service_exits_after_grace() {
+ let fixture = Fixture::new(Publication::Service);
+ let library = fixture.volume_config("lib-sigterm-fsync", Frontend::Library, false);
+ fixture.library_seed(&library, "interrupted.txt", b"seed");
+ let config = fixture.volume_config("stage-sigterm-fsync", Frontend::Fuse, false);
+ let mountpoint = fixture.temp.path().join("mnt-sigterm-fsync");
+ fs::create_dir(&mountpoint).expect("create mountpoint");
+ let mounted = fixture.mount(&config, &mountpoint);
+ interrupt_pending_fsync(&fixture, &library, &config, mounted, Interrupt::Terminate);
+}
+fn interrupt_pending_fsync(
+ fixture: &Fixture,
+ library: &Path,
+ config: &Path,
+ mut mounted: Mounted,
+ interrupt: Interrupt,
+) {
let mut dirty = OpenOptions::new()
.read(true)
.write(true)
@@ -999,16 +1055,25 @@ fn fuse_daemon_crash_interrupts_pending_fsync_service() {
.expect("read staged bytes while frozen");
assert_eq!(frozen_bytes, b"interrupted-local");
drop(frozen_reader);
- mounted.kill();
+ match interrupt {
+ Interrupt::Kill => mounted.kill(),
+ Interrupt::Terminate => {
+ let status = mounted.terminate_busy();
+ assert!(
+ !status.success(),
+ "SIGTERM with a pending fsync must force a non-zero exit after grace"
+ );
+ }
+ }
let outcome = receiver
.recv_timeout(Duration::from_secs(20))
.unwrap_or_else(|error| {
- panic!("fsync did not return after the mount daemon was killed: {error}")
+ panic!("fsync did not return after the mount daemon stopped: {error}")
});
caller.join().expect("fsync caller panicked");
assert!(
outcome.is_err(),
- "an fsync interrupted by killing the mount daemon must fail, not report remote durability"
+ "an fsync interrupted by stopping the mount daemon must fail, not report remote durability"
);
drop(dirty);
@@ -1018,7 +1083,10 @@ fn fuse_daemon_crash_interrupts_pending_fsync_service() {
b"seed",
"the interrupted publication must not have reached the authority"
);
- let mountpoint = fixture.temp.path().join("mnt-interrupted-reopen");
+ let mountpoint = fixture.temp.path().join(format!(
+ "mnt-interrupted-reopen-{}",
+ fixture.publication().as_str()
+ ));
fs::create_dir(&mountpoint).expect("create reopen mountpoint");
let mut reopened = fixture.mount(&config, &mountpoint);
assert_eq!(
@@ -1100,6 +1168,58 @@ fn fuse_delayed_completion_survives_client_interruption_service() {
mounted.unmount();
}
+#[test]
+#[ignore = "requires /dev/fuse and isolated S3; run with cargo x test-linux-fuse"]
+fn fuse_sigterm_with_open_fd_exits_and_releases_lease_both_publications() {
+ for publication in [Publication::Object, Publication::Service] {
+ let fixture = Fixture::new(publication);
+ let suffix = fixture.publication().as_str();
+ let library = fixture.volume_config(
+ &format!("lib-sigterm-open-{suffix}"),
+ Frontend::Library,
+ false,
+ );
+ fixture.library_seed(&library, "sigterm-open.txt", b"seed");
+ let config = fixture.volume_config(
+ &format!("stage-sigterm-open-{suffix}"),
+ Frontend::Fuse,
+ false,
+ );
+ let mountpoint = fixture
+ .temp
+ .path()
+ .join(format!("mnt-sigterm-open-{suffix}"));
+ fs::create_dir(&mountpoint).expect("create mountpoint");
+ let mut mounted = fixture.mount(&config, &mountpoint);
+ let path = mounted.mountpoint().join("sigterm-open.txt");
+ let mut held = File::open(&path).expect("hold a descriptor across SIGTERM");
+ let status = mounted.terminate_busy();
+ assert!(
+ !status.success(),
+ "SIGTERM with an open descriptor must exit through the forced non-zero path"
+ );
+ let mut after = Vec::new();
+ let error = held
+ .read_to_end(&mut after)
+ .expect_err("a held descriptor must report an I/O error after daemon exit");
+ assert_errno(error, &[ENOTCONN, EIO], "read after busy SIGTERM");
+ drop(held);
+
+ let mountpoint = fixture
+ .temp
+ .path()
+ .join(format!("mnt-sigterm-open-reopen-{suffix}"));
+ fs::create_dir(&mountpoint).expect("create reopen mountpoint");
+ let mut reopened = fixture.mount(&config, &mountpoint);
+ assert_eq!(
+ fs::read(reopened.mountpoint().join("sigterm-open.txt"))
+ .expect("read after SIGTERM remount"),
+ b"seed"
+ );
+ reopened.unmount();
+ }
+}
+
#[test]
#[ignore = "requires /dev/fuse and isolated S3; run with cargo x test-linux-fuse"]
fn fuse_busy_unmount_object() {
diff --git a/tests/linux_fuse/harness.rs b/tests/linux_fuse/harness.rs
index 8c5d6d2..0a5a052 100644
--- a/tests/linux_fuse/harness.rs
+++ b/tests/linux_fuse/harness.rs
@@ -718,6 +718,19 @@ impl Mounted {
self.assert_staging_lease_released();
}
+ /// SIGTERM with open descriptors or a pending fsync must exit under the
+ /// forced non-zero grace path and release the staging lease. No success
+ /// assertion is made here; the caller requires the busy exit contract.
+ pub fn terminate_busy(&mut self) -> ExitStatus {
+ let pid = self.child.id().to_string();
+ let output = run_external("kill", &["-TERM", &pid], Duration::from_secs(30));
+ output.success_or("send SIGTERM to yy mount");
+ let status = self.wait_exit(Duration::from_secs(10));
+ self.unmount_force();
+ self.assert_staging_lease_released();
+ status
+ }
+
pub fn unmount(&mut self) {
let mountpoint = self.mountpoint.to_str().expect("UTF-8 mountpoint");
let output = run_external("fusermount3", &["-u", mountpoint], Duration::from_secs(30));