From 9b0413e09c34b2351c1f0221fcb177717e620855 Mon Sep 17 00:00:00 2001 From: Nikolaus Rath Date: Sun, 4 Jan 2026 18:44:53 +0000 Subject: [PATCH 1/3] Fix test_passthroughfs() failure due to unexpected mtime/ctime differences. What's happening here is the following: - If the FUSE (kernel-level) writeback cache is enabled, the filesystem daemon cannot be trusted by the kernel to produce accurate mtime and ctime values, because there may be pending writes in the cache that have not been flushed to the daemon. - Therefore, the kernel still calls the getattr() handler, but ignores the mtime and ctime values, and instead maintains them internally. - When a file is closed, the kernel communicates the correct mtime and ctime values to the filesystem daemon through an extra setattr() call. - Therefore, the (correct) mtime value that a userspace program gets from the kernel for the FUSE filesystem will not agree with the (also correct) mtime value reported by the underlying filesystem as long as the file is open. - Once the file has been closed, the mtime values agree, but the filesystem daemon has updated this with an utimens() call which has resulted in ctime change, so now the ctime values do not agree. To fix this, make the writeback cache configurable and only check mtimes if writeback caching is disabled. Fixes: #57. --- examples/passthroughfs.py | 13 +++++++++---- test/test_examples.py | 21 ++++++++++++++------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/examples/passthroughfs.py b/examples/passthroughfs.py index 2f754bc..72767de 100755 --- a/examples/passthroughfs.py +++ b/examples/passthroughfs.py @@ -72,10 +72,9 @@ class Operations(pyfuse3.Operations): - enable_writeback_cache = True - - def __init__(self, source: str) -> None: + def __init__(self, source: str, enable_writeback_cache: bool = True) -> None: super().__init__() + self.enable_writeback_cache = enable_writeback_cache self._inode_path_map: dict[InodeT, str | set[str]] = {pyfuse3.ROOT_INODE: source} self._lookup_cnt: defaultdict[InodeT, int] = defaultdict(lambda: 0) self._fd_inode_map: dict[int, InodeT] = dict() @@ -534,6 +533,12 @@ def parse_args(args: list[str]) -> Namespace: parser.add_argument( '--debug-fuse', action='store_true', default=False, help='Enable FUSE debugging output' ) + parser.add_argument( + '--enable-writeback-cache', + action='store_true', + default=False, + help='Enable writeback cache (default: disabled)', + ) return parser.parse_args(args) @@ -541,7 +546,7 @@ def parse_args(args: list[str]) -> Namespace: def main() -> None: options = parse_args(sys.argv[1:]) init_logging(options.debug) - operations = Operations(options.source) + operations = Operations(options.source, enable_writeback_cache=options.enable_writeback_cache) log.debug('Mounting...') fuse_options = set(pyfuse3.default_options) diff --git a/test/test_examples.py b/test/test_examples.py index e9f7f14..701d187 100755 --- a/test/test_examples.py +++ b/test/test_examples.py @@ -96,7 +96,8 @@ def test_tmpfs(tmpdir): umount(mount_process, mnt_dir) -def test_passthroughfs(tmpdir): +@pytest.mark.parametrize('enable_writeback_cache', (True, False)) +def test_passthroughfs(tmpdir, enable_writeback_cache): mnt_dir = str(tmpdir.mkdir('mnt')) src_dir = str(tmpdir.mkdir('src')) cmdline = [ @@ -105,6 +106,8 @@ def test_passthroughfs(tmpdir): src_dir, mnt_dir, ] + if enable_writeback_cache: + cmdline.append('--enable-writeback-cache') mount_process = subprocess.Popen(cmdline, stdin=subprocess.DEVNULL, universal_newlines=True) try: wait_for_mount(mount_process, mnt_dir) @@ -125,7 +128,7 @@ def test_passthroughfs(tmpdir): tst_truncate_path(mnt_dir) tst_truncate_fd(mnt_dir) tst_unlink(mnt_dir) - tst_passthrough(src_dir, mnt_dir) + tst_passthrough(src_dir, mnt_dir, enable_writeback_cache=enable_writeback_cache) except: cleanup(mount_process, mnt_dir) raise @@ -408,7 +411,7 @@ def tst_rounding(mnt_dir, ns_tol=0): checked_unlink(filename, mnt_dir, isdir=True) -def tst_passthrough(src_dir, mnt_dir): +def tst_passthrough(src_dir, mnt_dir, enable_writeback_cache: bool = False): # Test propagation from source to mirror name = name_generator() src_name = os.path.join(src_dir, name) @@ -419,7 +422,7 @@ def tst_passthrough(src_dir, mnt_dir): fh.write('Hello, world') assert name in os.listdir(src_dir) assert name in os.listdir(mnt_dir) - assert_same_stats(src_name, mnt_name) + assert_same_stats(src_name, mnt_name, check_times=not enable_writeback_cache) # Test propagation from mirror to source name = name_generator() @@ -431,7 +434,7 @@ def tst_passthrough(src_dir, mnt_dir): fh.write('Hello, world') assert name in os.listdir(src_dir) assert name in os.listdir(mnt_dir) - assert_same_stats(src_name, mnt_name) + assert_same_stats(src_name, mnt_name, check_times=not enable_writeback_cache) # Test propagation inside subdirectory name = name_generator() @@ -446,10 +449,10 @@ def tst_passthrough(src_dir, mnt_dir): fh.write('Hello, world') assert name in os.listdir(src_dir) assert name in os.listdir(mnt_dir) - assert_same_stats(src_name, mnt_name) + assert_same_stats(src_name, mnt_name, check_times=not enable_writeback_cache) -def assert_same_stats(name1, name2): +def assert_same_stats(name1, name2, check_times: bool = True): stat1 = os.stat(name1) stat2 = os.stat(name2) @@ -471,4 +474,8 @@ def assert_same_stats(name1, name2): if name.endswith('_ns') and os.getenv('CI') == 'true': continue + # Skip time checks when writeback cache is enabled + if name.endswith('_ns') and not check_times: + continue + assert v1 == v2, 'Attribute {} differs by {} ({} vs {})'.format(name, v1 - v2, v1, v2) From c22c72b02f22bfe9f689bd137ea60c4c465222d5 Mon Sep 17 00:00:00 2001 From: Nikolaus Rath Date: Sun, 4 Jan 2026 18:52:37 +0000 Subject: [PATCH 2/3] Update test/test_examples.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/test_examples.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/test_examples.py b/test/test_examples.py index 701d187..59acaa6 100755 --- a/test/test_examples.py +++ b/test/test_examples.py @@ -474,7 +474,10 @@ def assert_same_stats(name1, name2, check_times: bool = True): if name.endswith('_ns') and os.getenv('CI') == 'true': continue - # Skip time checks when writeback cache is enabled + # When FUSE writeback cache is enabled, the kernel maintains mtime/ctime + # internally and only flushes them to the underlying filesystem on close. + # Until then, the timestamps reported for the passthrough mount and the + # backing directory may legitimately differ, so skip strict time checks. if name.endswith('_ns') and not check_times: continue From 106a7dde106ed425015c6fb516d10e7facadf881 Mon Sep 17 00:00:00 2001 From: Nikolaus Rath Date: Sun, 4 Jan 2026 18:52:45 +0000 Subject: [PATCH 3/3] Update examples/passthroughfs.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- examples/passthroughfs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/passthroughfs.py b/examples/passthroughfs.py index 72767de..5331bec 100755 --- a/examples/passthroughfs.py +++ b/examples/passthroughfs.py @@ -72,7 +72,7 @@ class Operations(pyfuse3.Operations): - def __init__(self, source: str, enable_writeback_cache: bool = True) -> None: + def __init__(self, source: str, enable_writeback_cache: bool = False) -> None: super().__init__() self.enable_writeback_cache = enable_writeback_cache self._inode_path_map: dict[InodeT, str | set[str]] = {pyfuse3.ROOT_INODE: source}