Skip to content

Commit 9f89e16

Browse files
Merge pull request #9385 from mr-raj12/darwin-f-fullsync
platform: use F_FULLSYNC on macOS for SyncFile data durability, fixes #9383
2 parents 9533b50 + 0354697 commit 9f89e16

File tree

4 files changed

+94
-1
lines changed

4 files changed

+94
-1
lines changed

src/borg/platform/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
from .darwin import acl_get, acl_set
5151
from .darwin import is_darwin_feature_64_bit_inode, _get_birthtime_ns
5252
from .darwin import set_flags
53+
from .darwin import fdatasync, sync_dir # type: ignore[no-redef]
5354
from .base import get_flags
5455
from .base import SyncFile
5556
from .posix import process_alive, local_pid_alive

src/borg/platform/base.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,6 @@ class SyncFile:
151151
152152
Calling SyncFile(path) for an existing path will raise FileExistsError. See the comment in __init__.
153153
154-
TODO: Use F_FULLSYNC on macOS.
155154
TODO: A Windows implementation should use CreateFile with FILE_FLAG_WRITE_THROUGH.
156155
"""
157156

src/borg/platform/darwin.pyx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,3 +262,33 @@ def set_flags(path, bsd_flags, fd=None):
262262
path_bytes = os.fsencode(path)
263263
if lchflags(path_bytes, c_flags) == -1:
264264
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path_bytes))
265+
266+
267+
import errno as errno_mod
268+
import fcntl as fcntl_mod
269+
270+
271+
def fdatasync(fd):
272+
"""macOS fdatasync using F_FULLFSYNC for true data durability.
273+
274+
On macOS, os.fsync() only flushes to the drive's write cache.
275+
fcntl F_FULLFSYNC flushes to persistent storage.
276+
Falls back to os.fsync() if F_FULLFSYNC is not supported.
277+
"""
278+
try:
279+
fcntl_mod.fcntl(fd, fcntl_mod.F_FULLFSYNC)
280+
except OSError:
281+
# F_FULLFSYNC not supported (e.g. network filesystem), fall back
282+
os.fsync(fd)
283+
284+
285+
def sync_dir(path):
286+
"""Sync a directory to persistent storage on macOS using F_FULLFSYNC."""
287+
fd = os.open(str(path), os.O_RDONLY)
288+
try:
289+
fdatasync(fd)
290+
except OSError as os_error:
291+
if os_error.errno != errno_mod.EINVAL:
292+
raise
293+
finally:
294+
os.close(fd)

src/borg/testsuite/platform/darwin_test.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import tempfile
33

44
from ...platform import acl_get, acl_set
5+
from ...platform import fdatasync, sync_dir
56
from .platform_test import skipif_not_darwin, skipif_fakeroot_detected, skipif_acls_not_working
67

78
# Set module-level skips
@@ -46,3 +47,65 @@ def test_extended_acl():
4647
b"group:ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000000::0:allow:read"
4748
in get_acl(file2.name, numeric_ids=True)["acl_extended"]
4849
)
50+
51+
52+
def test_fdatasync_uses_f_fullfsync(monkeypatch):
53+
"""Verify fcntl F_FULLFSYNC is called."""
54+
import fcntl as fcntl_mod
55+
from ...platform import darwin
56+
57+
calls = []
58+
original_fcntl = fcntl_mod.fcntl
59+
60+
def mock_fcntl(fd, cmd, *args):
61+
calls.append((fd, cmd))
62+
return original_fcntl(fd, cmd, *args)
63+
64+
monkeypatch.setattr(fcntl_mod, "fcntl", mock_fcntl)
65+
66+
with tempfile.NamedTemporaryFile() as tmp:
67+
tmp.write(b"test data")
68+
tmp.flush()
69+
darwin.fdatasync(tmp.fileno())
70+
71+
assert any(cmd == fcntl_mod.F_FULLFSYNC for _, cmd in calls), "fdatasync should call fcntl with F_FULLFSYNC"
72+
73+
74+
def test_fdatasync_falls_back_to_fsync(monkeypatch):
75+
"""Verify os.fsync fallback when F_FULLFSYNC fails."""
76+
import fcntl as fcntl_mod
77+
from ...platform import darwin
78+
79+
fsync_calls = []
80+
81+
def mock_fcntl(fd, cmd, *args):
82+
if cmd == fcntl_mod.F_FULLFSYNC:
83+
raise OSError("F_FULLFSYNC not supported")
84+
return 0
85+
86+
def mock_fsync(fd):
87+
fsync_calls.append(fd)
88+
89+
monkeypatch.setattr(fcntl_mod, "fcntl", mock_fcntl)
90+
monkeypatch.setattr(os, "fsync", mock_fsync)
91+
92+
with tempfile.NamedTemporaryFile() as tmp:
93+
tmp.write(b"test data")
94+
tmp.flush()
95+
darwin.fdatasync(tmp.fileno())
96+
97+
assert len(fsync_calls) == 1, "Should fall back to os.fsync when F_FULLFSYNC fails"
98+
99+
100+
def test_fdatasync_basic():
101+
"""Integration: fdatasync completes on a real file without error."""
102+
with tempfile.NamedTemporaryFile() as tmp:
103+
tmp.write(b"test data for fdatasync")
104+
tmp.flush()
105+
fdatasync(tmp.fileno())
106+
107+
108+
def test_sync_dir_basic():
109+
"""Integration: sync_dir completes on a real directory without error."""
110+
with tempfile.TemporaryDirectory() as tmpdir:
111+
sync_dir(tmpdir)

0 commit comments

Comments
 (0)