mirror of
https://github.com/borgbackup/borg.git
synced 2026-06-09 00:32:37 -04:00
platform: use F_FULLSYNC on macOS for SyncFile data durability, fixes #9383
This commit is contained in:
parent
9533b5083e
commit
0354697c56
4 changed files with 94 additions and 1 deletions
|
|
@ -50,6 +50,7 @@ elif is_darwin: # pragma: darwin only
|
|||
from .darwin import acl_get, acl_set
|
||||
from .darwin import is_darwin_feature_64_bit_inode, _get_birthtime_ns
|
||||
from .darwin import set_flags
|
||||
from .darwin import fdatasync, sync_dir # type: ignore[no-redef]
|
||||
from .base import get_flags
|
||||
from .base import SyncFile
|
||||
from .posix import process_alive, local_pid_alive
|
||||
|
|
|
|||
|
|
@ -151,7 +151,6 @@ class SyncFile:
|
|||
|
||||
Calling SyncFile(path) for an existing path will raise FileExistsError. See the comment in __init__.
|
||||
|
||||
TODO: Use F_FULLSYNC on macOS.
|
||||
TODO: A Windows implementation should use CreateFile with FILE_FLAG_WRITE_THROUGH.
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -262,3 +262,33 @@ def set_flags(path, bsd_flags, fd=None):
|
|||
path_bytes = os.fsencode(path)
|
||||
if lchflags(path_bytes, c_flags) == -1:
|
||||
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path_bytes))
|
||||
|
||||
|
||||
import errno as errno_mod
|
||||
import fcntl as fcntl_mod
|
||||
|
||||
|
||||
def fdatasync(fd):
|
||||
"""macOS fdatasync using F_FULLFSYNC for true data durability.
|
||||
|
||||
On macOS, os.fsync() only flushes to the drive's write cache.
|
||||
fcntl F_FULLFSYNC flushes to persistent storage.
|
||||
Falls back to os.fsync() if F_FULLFSYNC is not supported.
|
||||
"""
|
||||
try:
|
||||
fcntl_mod.fcntl(fd, fcntl_mod.F_FULLFSYNC)
|
||||
except OSError:
|
||||
# F_FULLFSYNC not supported (e.g. network filesystem), fall back
|
||||
os.fsync(fd)
|
||||
|
||||
|
||||
def sync_dir(path):
|
||||
"""Sync a directory to persistent storage on macOS using F_FULLFSYNC."""
|
||||
fd = os.open(str(path), os.O_RDONLY)
|
||||
try:
|
||||
fdatasync(fd)
|
||||
except OSError as os_error:
|
||||
if os_error.errno != errno_mod.EINVAL:
|
||||
raise
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import os
|
|||
import tempfile
|
||||
|
||||
from ...platform import acl_get, acl_set
|
||||
from ...platform import fdatasync, sync_dir
|
||||
from .platform_test import skipif_not_darwin, skipif_fakeroot_detected, skipif_acls_not_working
|
||||
|
||||
# Set module-level skips
|
||||
|
|
@ -46,3 +47,65 @@ def test_extended_acl():
|
|||
b"group:ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000000::0:allow:read"
|
||||
in get_acl(file2.name, numeric_ids=True)["acl_extended"]
|
||||
)
|
||||
|
||||
|
||||
def test_fdatasync_uses_f_fullfsync(monkeypatch):
|
||||
"""Verify fcntl F_FULLFSYNC is called."""
|
||||
import fcntl as fcntl_mod
|
||||
from ...platform import darwin
|
||||
|
||||
calls = []
|
||||
original_fcntl = fcntl_mod.fcntl
|
||||
|
||||
def mock_fcntl(fd, cmd, *args):
|
||||
calls.append((fd, cmd))
|
||||
return original_fcntl(fd, cmd, *args)
|
||||
|
||||
monkeypatch.setattr(fcntl_mod, "fcntl", mock_fcntl)
|
||||
|
||||
with tempfile.NamedTemporaryFile() as tmp:
|
||||
tmp.write(b"test data")
|
||||
tmp.flush()
|
||||
darwin.fdatasync(tmp.fileno())
|
||||
|
||||
assert any(cmd == fcntl_mod.F_FULLFSYNC for _, cmd in calls), "fdatasync should call fcntl with F_FULLFSYNC"
|
||||
|
||||
|
||||
def test_fdatasync_falls_back_to_fsync(monkeypatch):
|
||||
"""Verify os.fsync fallback when F_FULLFSYNC fails."""
|
||||
import fcntl as fcntl_mod
|
||||
from ...platform import darwin
|
||||
|
||||
fsync_calls = []
|
||||
|
||||
def mock_fcntl(fd, cmd, *args):
|
||||
if cmd == fcntl_mod.F_FULLFSYNC:
|
||||
raise OSError("F_FULLFSYNC not supported")
|
||||
return 0
|
||||
|
||||
def mock_fsync(fd):
|
||||
fsync_calls.append(fd)
|
||||
|
||||
monkeypatch.setattr(fcntl_mod, "fcntl", mock_fcntl)
|
||||
monkeypatch.setattr(os, "fsync", mock_fsync)
|
||||
|
||||
with tempfile.NamedTemporaryFile() as tmp:
|
||||
tmp.write(b"test data")
|
||||
tmp.flush()
|
||||
darwin.fdatasync(tmp.fileno())
|
||||
|
||||
assert len(fsync_calls) == 1, "Should fall back to os.fsync when F_FULLFSYNC fails"
|
||||
|
||||
|
||||
def test_fdatasync_basic():
|
||||
"""Integration: fdatasync completes on a real file without error."""
|
||||
with tempfile.NamedTemporaryFile() as tmp:
|
||||
tmp.write(b"test data for fdatasync")
|
||||
tmp.flush()
|
||||
fdatasync(tmp.fileno())
|
||||
|
||||
|
||||
def test_sync_dir_basic():
|
||||
"""Integration: sync_dir completes on a real directory without error."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
sync_dir(tmpdir)
|
||||
|
|
|
|||
Loading…
Reference in a new issue