Use F_FULLFSYNC on macOS with fallback to fsync for full barrier durability

This commit is contained in:
Andrea Mazzoleni 2026-09-05 09:51:38 +02:00
parent b5a8ef7c33
commit 8ed0224e41
3 changed files with 49 additions and 2 deletions

View file

@ -893,8 +893,27 @@ int parity_sync(struct snapraid_parity_handle* handle)
* Ensure that data changes are written to disk.
* This is required to ensure that parity is more updated than content
* in case of a system crash.
*
* On macOS, standard fsync() only pushes data from kernel buffers to the
* drive's volatile cache without requesting a physical media flush. F_FULLFSYNC
* forces the drive controller to commit all cached data to permanent storage.
* Because F_FULLFSYNC is not supported on directory descriptors, the subsequent
* directory fsync() only flushes to the drive cache and is not guaranteed to be
* instantly persistent across power loss. However, major database engines such
* as SQLite and PostgreSQL follow this exact same pattern on Darwin, making it
* the standard industry practice.
*
* See:
* https://github.com/sqlite/sqlite/blob/f3b9f74d81132426dee1ccc07a67fdad2ccfeaa9/src/os_unix.c#L3928-L3953
* https://github.com/postgres/postgres/blob/74c77052bc20017076100f0649ca0f44b1b98c29/src/backend/storage/file/fd.c#L835-L852
*/
#if defined(__APPLE__) && defined(F_FULLFSYNC)
ret = fcntl(split->f, F_FULLFSYNC);
if (ret != 0 && (errno == ENOTSUP || errno == EINVAL))
ret = fsync(split->f);
#else
ret = fsync(split->f);
#endif
if (ret != 0) {
/* LCOV_EXCL_START */
log_fatal(errno, "Error syncing parity file '%s'. %s.\n", split->path, strerror(errno));

View file

@ -5108,7 +5108,8 @@ static void state_rename_content(struct snapraid_state* state)
/*
* This sequence relies on POSIX file-system semantics.
*
* The temporary content file has already been flushed and fsync'ed.
* The temporary content file has already been flushed and fsync'ed in
* ssync() (using F_FULLFSYNC on macOS to force a physical media flush).
* rename()/renameat() atomically replaces the old content file, but the
* directory entry update is not necessarily durable when the rename
* returns.
@ -5119,6 +5120,19 @@ static void state_rename_content(struct snapraid_state* state)
* may start only after the pre-sync content state has been durably
* published.
*
* On macOS, standard fsync() only pushes data from kernel buffers to the
* drive's volatile cache without requesting a physical media flush. F_FULLFSYNC
* forces the drive controller to commit all cached data to permanent storage.
* Because F_FULLFSYNC is not supported on directory descriptors, the subsequent
* directory fsync() only flushes to the drive cache and is not guaranteed to be
* instantly persistent across power loss. However, major database engines such
* as SQLite and PostgreSQL follow this exact same pattern on Darwin, making it
* the standard industry practice.
*
* See:
* https://github.com/sqlite/sqlite/blob/f3b9f74d81132426dee1ccc07a67fdad2ccfeaa9/src/os_unix.c#L3928-L3953
* https://github.com/postgres/postgres/blob/74c77052bc20017076100f0649ca0f44b1b98c29/src/backend/storage/file/fd.c#L835-L852
*
* When renameat() is available, perform the rename relative to the same
* directory descriptor that is subsequently fsync'ed. Use O_DIRECTORY
* when available, while retaining compatibility with POSIX systems where

View file

@ -835,7 +835,21 @@ int ssync(STREAM* s)
unsigned i;
for (i = 0; i < s->handle_size; ++i) {
if (fsync(s->handle[i].f) != 0) {
int ret;
/*
* On macOS, standard fsync() only pushes data from kernel buffers to the
* drive's volatile cache without requesting a physical media flush. F_FULLFSYNC
* forces the drive controller to commit all cached data to permanent storage.
*/
#if defined(__APPLE__) && defined(F_FULLFSYNC)
ret = fcntl(s->handle[i].f, F_FULLFSYNC);
if (ret != 0 && (errno == ENOTSUP || errno == EINVAL))
ret = fsync(s->handle[i].f);
#else
ret = fsync(s->handle[i].f);
#endif
if (ret != 0) {
/* LCOV_EXCL_START */
s->state = STREAM_STATE_ERROR;
s->state_index = i;