Skip to content

Commit 5326a68

Browse files
NikratioCopilot
andauthored
Fix test_passthroughfs() failure due to unexpected mtime/ctime differences (#130)
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. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent d34dfbd commit 5326a68

2 files changed

Lines changed: 26 additions & 11 deletions

File tree

examples/passthroughfs.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,9 @@
7272

7373

7474
class Operations(pyfuse3.Operations):
75-
enable_writeback_cache = True
76-
77-
def __init__(self, source: str) -> None:
75+
def __init__(self, source: str, enable_writeback_cache: bool = False) -> None:
7876
super().__init__()
77+
self.enable_writeback_cache = enable_writeback_cache
7978
self._inode_path_map: dict[InodeT, str | set[str]] = {pyfuse3.ROOT_INODE: source}
8079
self._lookup_cnt: defaultdict[InodeT, int] = defaultdict(lambda: 0)
8180
self._fd_inode_map: dict[int, InodeT] = dict()
@@ -534,14 +533,20 @@ def parse_args(args: list[str]) -> Namespace:
534533
parser.add_argument(
535534
'--debug-fuse', action='store_true', default=False, help='Enable FUSE debugging output'
536535
)
536+
parser.add_argument(
537+
'--enable-writeback-cache',
538+
action='store_true',
539+
default=False,
540+
help='Enable writeback cache (default: disabled)',
541+
)
537542

538543
return parser.parse_args(args)
539544

540545

541546
def main() -> None:
542547
options = parse_args(sys.argv[1:])
543548
init_logging(options.debug)
544-
operations = Operations(options.source)
549+
operations = Operations(options.source, enable_writeback_cache=options.enable_writeback_cache)
545550

546551
log.debug('Mounting...')
547552
fuse_options = set(pyfuse3.default_options)

test/test_examples.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ def test_tmpfs(tmpdir):
9696
umount(mount_process, mnt_dir)
9797

9898

99-
def test_passthroughfs(tmpdir):
99+
@pytest.mark.parametrize('enable_writeback_cache', (True, False))
100+
def test_passthroughfs(tmpdir, enable_writeback_cache):
100101
mnt_dir = str(tmpdir.mkdir('mnt'))
101102
src_dir = str(tmpdir.mkdir('src'))
102103
cmdline = [
@@ -105,6 +106,8 @@ def test_passthroughfs(tmpdir):
105106
src_dir,
106107
mnt_dir,
107108
]
109+
if enable_writeback_cache:
110+
cmdline.append('--enable-writeback-cache')
108111
mount_process = subprocess.Popen(cmdline, stdin=subprocess.DEVNULL, universal_newlines=True)
109112
try:
110113
wait_for_mount(mount_process, mnt_dir)
@@ -125,7 +128,7 @@ def test_passthroughfs(tmpdir):
125128
tst_truncate_path(mnt_dir)
126129
tst_truncate_fd(mnt_dir)
127130
tst_unlink(mnt_dir)
128-
tst_passthrough(src_dir, mnt_dir)
131+
tst_passthrough(src_dir, mnt_dir, enable_writeback_cache=enable_writeback_cache)
129132
except:
130133
cleanup(mount_process, mnt_dir)
131134
raise
@@ -408,7 +411,7 @@ def tst_rounding(mnt_dir, ns_tol=0):
408411
checked_unlink(filename, mnt_dir, isdir=True)
409412

410413

411-
def tst_passthrough(src_dir, mnt_dir):
414+
def tst_passthrough(src_dir, mnt_dir, enable_writeback_cache: bool = False):
412415
# Test propagation from source to mirror
413416
name = name_generator()
414417
src_name = os.path.join(src_dir, name)
@@ -419,7 +422,7 @@ def tst_passthrough(src_dir, mnt_dir):
419422
fh.write('Hello, world')
420423
assert name in os.listdir(src_dir)
421424
assert name in os.listdir(mnt_dir)
422-
assert_same_stats(src_name, mnt_name)
425+
assert_same_stats(src_name, mnt_name, check_times=not enable_writeback_cache)
423426

424427
# Test propagation from mirror to source
425428
name = name_generator()
@@ -431,7 +434,7 @@ def tst_passthrough(src_dir, mnt_dir):
431434
fh.write('Hello, world')
432435
assert name in os.listdir(src_dir)
433436
assert name in os.listdir(mnt_dir)
434-
assert_same_stats(src_name, mnt_name)
437+
assert_same_stats(src_name, mnt_name, check_times=not enable_writeback_cache)
435438

436439
# Test propagation inside subdirectory
437440
name = name_generator()
@@ -446,10 +449,10 @@ def tst_passthrough(src_dir, mnt_dir):
446449
fh.write('Hello, world')
447450
assert name in os.listdir(src_dir)
448451
assert name in os.listdir(mnt_dir)
449-
assert_same_stats(src_name, mnt_name)
452+
assert_same_stats(src_name, mnt_name, check_times=not enable_writeback_cache)
450453

451454

452-
def assert_same_stats(name1, name2):
455+
def assert_same_stats(name1, name2, check_times: bool = True):
453456
stat1 = os.stat(name1)
454457
stat2 = os.stat(name2)
455458

@@ -471,4 +474,11 @@ def assert_same_stats(name1, name2):
471474
if name.endswith('_ns') and os.getenv('CI') == 'true':
472475
continue
473476

477+
# When FUSE writeback cache is enabled, the kernel maintains mtime/ctime
478+
# internally and only flushes them to the underlying filesystem on close.
479+
# Until then, the timestamps reported for the passthrough mount and the
480+
# backing directory may legitimately differ, so skip strict time checks.
481+
if name.endswith('_ns') and not check_times:
482+
continue
483+
474484
assert v1 == v2, 'Attribute {} differs by {} ({} vs {})'.format(name, v1 - v2, v1, v2)

0 commit comments

Comments
 (0)