diff --git a/cmdline/parity.c b/cmdline/parity.c index b590d6b5..382894d4 100644 --- a/cmdline/parity.c +++ b/cmdline/parity.c @@ -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)); diff --git a/cmdline/state.c b/cmdline/state.c index 33c553c3..1fbc564b 100644 --- a/cmdline/state.c +++ b/cmdline/state.c @@ -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 diff --git a/cmdline/stream.c b/cmdline/stream.c index 1b56b9a8..81003ff5 100644 --- a/cmdline/stream.c +++ b/cmdline/stream.c @@ -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;