core.fsyncmethod: add writeout-only mode

This commit introduces the `core.fsyncMethod` configuration knob, which can currently be set to `fsync` or `writeout-only`. The new writeout-only mode attempts to tell the operating system to flush its in-memory page cache to the storage hardware without issuing a CACHE_FLUSH command to the storage controller. Writeout-only fsync is significantly faster than a vanilla fsync on common hardware, since data is written to a disk-side cache rather than all the way to a durable medium. Later changes in this patch series will take advantage of this primitive to implement batching of hardware flushes. When git_fsync is called with FSYNC_WRITEOUT_ONLY, it may fail and the caller is expected to do an ordinary fsync as needed. On Apple platforms, the fsync system call does not issue a CACHE_FLUSH directive to the storage controller. This change updates fsync to do fcntl(F_FULLFSYNC) to make fsync actually durable. We maintain parity with existing behavior on Apple platforms by setting the default value of the new core.fsyncMethod option. Signed-off-by: Neeraj Singh <neerajsi@microsoft.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Neeraj Singh committed Mar 10, 2022 at 22:43 UTC abf38abec201cded6094801766d69e11a6c112b6
13 files changed +184 -8
Documentation/config/core.txt
+9
@@ -547,6 +547,15 @@ core.whitespace::
547 is relevant for `indent-with-non-tab` and when Git fixes `tab-in-indent`
548 errors. The default tab width is 8. Allowed values are 1 to 63.
549
550 +core.fsyncMethod::
551 + A value indicating the strategy Git will use to harden repository data
552 + using fsync and related primitives.
553 ++
554 +* `fsync` uses the fsync() system call or platform equivalents.
555 +* `writeout-only` issues pagecache writeback requests, but depending on the
556 + filesystem and storage hardware, data added to the repository may not be
557 + durable in the event of a system crash. This is the default mode on macOS.
558 +
559 core.fsyncObjectFiles::
560 This boolean will enable 'fsync()' when writing object files.
561 +
Makefile
+6
@@ -411,6 +411,8 @@ all::
411 #
412 # Define HAVE_CLOCK_MONOTONIC if your platform has CLOCK_MONOTONIC.
413 #
414 +# Define HAVE_SYNC_FILE_RANGE if your platform has sync_file_range.
415 +#
416 # Define NEEDS_LIBRT if your platform requires linking with librt (glibc version
417 # before 2.17) for clock_gettime and CLOCK_MONOTONIC.
418 #
@@ -1897,6 +1899,10 @@ ifdef HAVE_CLOCK_MONOTONIC
1899 BASIC_CFLAGS += -DHAVE_CLOCK_MONOTONIC
1900 endif
1901
1902 +ifdef HAVE_SYNC_FILE_RANGE
1903 + BASIC_CFLAGS += -DHAVE_SYNC_FILE_RANGE
1904 +endif
1905 +
1906 ifdef NEEDS_LIBRT
1907 EXTLIBS += -lrt
1908 endif
cache.h
+7
@@ -995,6 +995,13 @@ extern char *git_replace_ref_base;
995
996 extern int fsync_object_files;
997 extern int use_fsync;
998 +
999 +enum fsync_method {
1000 + FSYNC_METHOD_FSYNC,
1001 + FSYNC_METHOD_WRITEOUT_ONLY
1002 +};
1003 +
1004 +extern enum fsync_method fsync_method;
1005 extern int core_preload_index;
1006 extern int precomposed_unicode;
1007 extern int protect_hfs;
compat/mingw.h
+3
@@ -329,6 +329,9 @@ int mingw_getpagesize(void);
329 #define getpagesize mingw_getpagesize
330 #endif
331
332 +int win32_fsync_no_flush(int fd);
333 +#define fsync_no_flush win32_fsync_no_flush
334 +
335 struct rlimit {
336 unsigned int rlim_cur;
337 };
compat/win32/flush.c new
+28
@@ -0,0 +1,28 @@
1 +#include "git-compat-util.h"
2 +#include <winternl.h>
3 +#include "lazyload.h"
4 +
5 +int win32_fsync_no_flush(int fd)
6 +{
7 + IO_STATUS_BLOCK io_status;
8 +
9 +#define FLUSH_FLAGS_FILE_DATA_ONLY 1
10 +
11 + DECLARE_PROC_ADDR(ntdll.dll, NTSTATUS, NTAPI, NtFlushBuffersFileEx,
12 + HANDLE FileHandle, ULONG Flags, PVOID Parameters, ULONG ParameterSize,
13 + PIO_STATUS_BLOCK IoStatusBlock);
14 +
15 + if (!INIT_PROC_ADDR(NtFlushBuffersFileEx)) {
16 + errno = ENOSYS;
17 + return -1;
18 + }
19 +
20 + memset(&io_status, 0, sizeof(io_status));
21 + if (NtFlushBuffersFileEx((HANDLE)_get_osfhandle(fd), FLUSH_FLAGS_FILE_DATA_ONLY,
22 + NULL, 0, &io_status)) {
23 + errno = EINVAL;
24 + return -1;
25 + }
26 +
27 + return 0;
28 +}
config.c
+12
@@ -1600,6 +1600,18 @@ static int git_default_core_config(const char *var, const char *value, void *cb)
1600 return 0;
1601 }
1602
1603 + if (!strcmp(var, "core.fsyncmethod")) {
1604 + if (!value)
1605 + return config_error_nonbool(var);
1606 + if (!strcmp(value, "fsync"))
1607 + fsync_method = FSYNC_METHOD_FSYNC;
1608 + else if (!strcmp(value, "writeout-only"))
1609 + fsync_method = FSYNC_METHOD_WRITEOUT_ONLY;
1610 + else
1611 + warning(_("ignoring unknown core.fsyncMethod value '%s'"), value);
1612 +
1613 + }
1614 +
1615 if (!strcmp(var, "core.fsyncobjectfiles")) {
1616 fsync_object_files = git_config_bool(var, value);
1617 return 0;
config.mak.uname
+3
@@ -57,6 +57,7 @@ ifeq ($(uname_S),Linux)
57 HAVE_CLOCK_MONOTONIC = YesPlease
58 # -lrt is needed for clock_gettime on glibc <= 2.16
59 NEEDS_LIBRT = YesPlease
60 + HAVE_SYNC_FILE_RANGE = YesPlease
61 HAVE_GETDELIM = YesPlease
62 FREAD_READS_DIRECTORIES = UnfortunatelyYes
63 BASIC_CFLAGS += -DHAVE_SYSINFO
@@ -463,6 +464,7 @@ endif
464 CFLAGS =
465 BASIC_CFLAGS = -nologo -I. -Icompat/vcbuild/include -DWIN32 -D_CONSOLE -DHAVE_STRING_H -D_CRT_SECURE_NO_WARNINGS -D_CRT_NONSTDC_NO_DEPRECATE
466 COMPAT_OBJS = compat/msvc.o compat/winansi.o \
467 + compat/win32/flush.o \
468 compat/win32/path-utils.o \
469 compat/win32/pthread.o compat/win32/syslog.o \
470 compat/win32/trace2_win32_process_info.o \
@@ -640,6 +642,7 @@ ifeq ($(uname_S),MINGW)
642 COMPAT_CFLAGS += -DSTRIP_EXTENSION=\".exe\"
643 COMPAT_OBJS += compat/mingw.o compat/winansi.o \
644 compat/win32/trace2_win32_process_info.o \
645 + compat/win32/flush.o \
646 compat/win32/path-utils.o \
647 compat/win32/pthread.o compat/win32/syslog.o \
648 compat/win32/dirent.o
configure.ac
+8
@@ -1082,6 +1082,14 @@ AC_COMPILE_IFELSE([CLOCK_MONOTONIC_SRC],
1082 [AC_MSG_RESULT([no])
1083 HAVE_CLOCK_MONOTONIC=])
1084 GIT_CONF_SUBST([HAVE_CLOCK_MONOTONIC])
1085 +
1086 +#
1087 +# Define HAVE_SYNC_FILE_RANGE=YesPlease if sync_file_range is available.
1088 +GIT_CHECK_FUNC(sync_file_range,
1089 + [HAVE_SYNC_FILE_RANGE=YesPlease],
1090 + [HAVE_SYNC_FILE_RANGE])
1091 +GIT_CONF_SUBST([HAVE_SYNC_FILE_RANGE])
1092 +
1093 #
1094 # Define NO_SETITIMER if you don't have setitimer.
1095 GIT_CHECK_FUNC(setitimer,
contrib/buildsystems/CMakeLists.txt
+12 -4
@@ -261,10 +261,18 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
261 NOGDI OBJECT_CREATION_MODE=1 __USE_MINGW_ANSI_STDIO=0
262 USE_NED_ALLOCATOR OVERRIDE_STRDUP MMAP_PREVENTS_DELETE USE_WIN32_MMAP
263 UNICODE _UNICODE HAVE_WPGMPTR ENSURE_MSYSTEM_IS_SET HAVE_RTLGENRANDOM)
264 - list(APPEND compat_SOURCES compat/mingw.c compat/winansi.c compat/win32/path-utils.c
265 - compat/win32/pthread.c compat/win32mmap.c compat/win32/syslog.c
266 - compat/win32/trace2_win32_process_info.c compat/win32/dirent.c
267 - compat/nedmalloc/nedmalloc.c compat/strdup.c)
264 + list(APPEND compat_SOURCES
265 + compat/mingw.c
266 + compat/winansi.c
267 + compat/win32/flush.c
268 + compat/win32/path-utils.c
269 + compat/win32/pthread.c
270 + compat/win32mmap.c
271 + compat/win32/syslog.c
272 + compat/win32/trace2_win32_process_info.c
273 + compat/win32/dirent.c
274 + compat/nedmalloc/nedmalloc.c
275 + compat/strdup.c)
276 set(NO_UNIX_SOCKETS 1)
277
278 elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
environment.c
+1
@@ -44,6 +44,7 @@ int zlib_compression_level = Z_BEST_SPEED;
44 int pack_compression_level = Z_DEFAULT_COMPRESSION;
45 int fsync_object_files;
46 int use_fsync = -1;
47 +enum fsync_method fsync_method = FSYNC_METHOD_DEFAULT;
48 size_t packed_git_window_size = DEFAULT_PACKED_GIT_WINDOW_SIZE;
49 size_t packed_git_limit = DEFAULT_PACKED_GIT_LIMIT;
50 size_t delta_base_cache_limit = 96 * 1024 * 1024;
git-compat-util.h
+24
@@ -1271,6 +1271,30 @@ __attribute__((format (printf, 1, 2))) NORETURN
1271 void BUG(const char *fmt, ...);
1272 #endif
1273
1274 +#ifdef __APPLE__
1275 +#define FSYNC_METHOD_DEFAULT FSYNC_METHOD_WRITEOUT_ONLY
1276 +#else
1277 +#define FSYNC_METHOD_DEFAULT FSYNC_METHOD_FSYNC
1278 +#endif
1279 +
1280 +enum fsync_action {
1281 + FSYNC_WRITEOUT_ONLY,
1282 + FSYNC_HARDWARE_FLUSH
1283 +};
1284 +
1285 +/*
1286 + * Issues an fsync against the specified file according to the specified mode.
1287 + *
1288 + * FSYNC_WRITEOUT_ONLY attempts to use interfaces available on some operating
1289 + * systems to flush the OS cache without issuing a flush command to the storage
1290 + * controller. If those interfaces are unavailable, the function fails with
1291 + * ENOSYS.
1292 + *
1293 + * FSYNC_HARDWARE_FLUSH does an OS writeout and hardware flush to ensure that
1294 + * changes are durable. It is not expected to fail.
1295 + */
1296 +int git_fsync(int fd, enum fsync_action action);
1297 +
1298 /*
1299 * Preserves errno, prints a message, but gives no warning for ENOENT.
1300 * Returns 0 on success, which includes trying to unlink an object that does
wrapper.c
+64
@@ -546,6 +546,70 @@ int xmkstemp_mode(char *filename_template, int mode)
546 return fd;
547 }
548
549 +/*
550 + * Some platforms return EINTR from fsync. Since fsync is invoked in some
551 + * cases by a wrapper that dies on failure, do not expose EINTR to callers.
552 + */
553 +static int fsync_loop(int fd)
554 +{
555 + int err;
556 +
557 + do {
558 + err = fsync(fd);
559 + } while (err < 0 && errno == EINTR);
560 + return err;
561 +}
562 +
563 +int git_fsync(int fd, enum fsync_action action)
564 +{
565 + switch (action) {
566 + case FSYNC_WRITEOUT_ONLY:
567 +
568 +#ifdef __APPLE__
569 + /*
570 + * On macOS, fsync just causes filesystem cache writeback but
571 + * does not flush hardware caches.
572 + */
573 + return fsync_loop(fd);
574 +#endif
575 +
576 +#ifdef HAVE_SYNC_FILE_RANGE
577 + /*
578 + * On linux 2.6.17 and above, sync_file_range is the way to
579 + * issue a writeback without a hardware flush. An offset of
580 + * 0 and size of 0 indicates writeout of the entire file and the
581 + * wait flags ensure that all dirty data is written to the disk
582 + * (potentially in a disk-side cache) before we continue.
583 + */
584 +
585 + return sync_file_range(fd, 0, 0, SYNC_FILE_RANGE_WAIT_BEFORE |
586 + SYNC_FILE_RANGE_WRITE |
587 + SYNC_FILE_RANGE_WAIT_AFTER);
588 +#endif
589 +
590 +#ifdef fsync_no_flush
591 + return fsync_no_flush(fd);
592 +#endif
593 +
594 + errno = ENOSYS;
595 + return -1;
596 +
597 + case FSYNC_HARDWARE_FLUSH:
598 + /*
599 + * On macOS, a special fcntl is required to really flush the
600 + * caches within the storage controller. As of this writing,
601 + * this is a very expensive operation on Apple SSDs.
602 + */
603 +#ifdef __APPLE__
604 + return fcntl(fd, F_FULLFSYNC);
605 +#else
606 + return fsync_loop(fd);
607 +#endif
608 + default:
609 + BUG("unexpected git_fsync(%d) call", action);
610 + }
611 +}
612 +
613 static int warn_if_unremovable(const char *op, const char *file, int rc)
614 {
615 int err;
write-or-die.c
+7 -4
@@ -62,10 +62,13 @@ void fsync_or_die(int fd, const char *msg)
62 use_fsync = git_env_bool("GIT_TEST_FSYNC", 1);
63 if (!use_fsync)
64 return;
65 - while (fsync(fd) < 0) {
66 - if (errno != EINTR)
67 - die_errno("fsync error on '%s'", msg);
68 - }
65 +
66 + if (fsync_method == FSYNC_METHOD_WRITEOUT_ONLY &&
67 + git_fsync(fd, FSYNC_WRITEOUT_ONLY) >= 0)
68 + return;
69 +
70 + if (git_fsync(fd, FSYNC_HARDWARE_FLUSH) < 0)
71 + die_errno("fsync error on '%s'", msg);
72 }
73
74 void write_or_die(int fd, const void *buf, size_t count)