Skip to contentSkip to conversation

Implement a bulk-checkin option for core.fsyncObjectFiles

RFC PATCH Superseded Neeraj K. Singh via GitGitGadget 10 participants 160 messages Aug 25, 2021 – Mar 10, 2022 Page 2 of 2

Subsystems (2)
lore
Share & tools
lore
#114 lore raw

Re: [PATCH v7 2/9] tmp-objdir: new API for creating temporary writable databases

In reply to Neeraj Singh via GitGitGadget

Hi,

Thanks for working on this, and for moving this up in your series near the beginning.

Quoted 5 lines · Neeraj Singh via GitGitGadget

From: Neeraj Singh <[email protected]>

This patch is based on work by Elijah Newren. Any bugs however are my own.

This kind of information is often included in a commit message via a trailer such as:

or Helped-by: or Co-authored-by: or Contributions-by: .

Quoted 103 lines

The tmp_objdir API provides the ability to create temporary object directories, but was designed with the goal of having subprocesses access these object stores, followed by the main process migrating objects from it to the main object store or just deleting it. The subprocesses would view it as their primary datastore and write to it.

Here we add the tmp_objdir_replace_primary_odb function that replaces the current process's writable "main" object directory with the specified one. The previous main object directory is restored in either tmp_objdir_migrate or tmp_objdir_destroy.

For the --remerge-diff usecase, add a new will_destroy flag in struct object_database to mark ephemeral object databases that do not require fsync durability.

Add 'git prune' support for removing temporary object databases, and make sure that they have a name starting with tmp_ and containing an operation-specific name.

---
 builtin/prune.c        | 22 +++++++++++++++++----
 builtin/receive-pack.c |  2 +-
 object-file.c          | 45 ++++++++++++++++++++++++++++++++++++++++--
 object-store.h         | 21 +++++++++++++++++++-
 object.c               |  2 +-
 tmp-objdir.c           | 32 +++++++++++++++++++++++++++---
 tmp-objdir.h           | 14 ++++++++++---
 7 files changed, 123 insertions(+), 15 deletions(-)
builtin/prune.c +18 -4
index 02c6ab7cbaa..9c72ecf5a58 100644
@@ -18,6 +18,7 @@ static int show_only;
 static int verbose;
 static timestamp_t expire;
 static int show_progress = -1;
+static struct strbuf remove_dir_buf = STRBUF_INIT;
 
 static int prune_tmp_file(const char *fullpath)
 {
@@ -26,10 +27,19 @@ static int prune_tmp_file(const char *fullpath)
                return error("Could not stat '%s'", fullpath);
        if (st.st_mtime > expire)
                return 0;
-       if (show_only || verbose)
-               printf("Removing stale temporary file %s\n", fullpath);
-       if (!show_only)
-               unlink_or_warn(fullpath);
+       if (S_ISDIR(st.st_mode)) {
+               if (show_only || verbose)
+                       printf("Removing stale temporary directory %s\n", fullpath);
+               if (!show_only) {
+                       strbuf_addstr(&remove_dir_buf, fullpath);
+                       remove_dir_recursively(&remove_dir_buf, 0);
+               }
+       } else {
+               if (show_only || verbose)
+                       printf("Removing stale temporary file %s\n", fullpath);
+               if (!show_only)
+                       unlink_or_warn(fullpath);
+       }
        return 0;
 }
 
@@ -97,6 +107,9 @@ static int prune_cruft(const char *basename, const char *path, void *data)
 
 static int prune_subdir(unsigned int nr, const char *path, void *data)
 {
+       if (verbose)
+               printf("Removing directory %s\n", path);
+
        if (!show_only)
                rmdir(path);
        return 0;
@@ -185,5 +198,6 @@ int cmd_prune(int argc, const char **argv, const char *prefix)
                prune_shallow(show_only ? PRUNE_SHOW_ONLY : 0);
        }
 
+       strbuf_release(&remove_dir_buf);
        return 0;
 }
builtin/receive-pack.c +1 -1
index 48960a9575b..418a42ca069 100644
@@ -2208,7 +2208,7 @@ static const char *unpack(int err_fd, struct shallow_info *si)
                strvec_push(&child.args, alt_shallow_file);
        }
 
-       tmp_objdir = tmp_objdir_create();
+       tmp_objdir = tmp_objdir_create("incoming");
        if (!tmp_objdir) {
                if (err_fd > 0)
                        close(err_fd);
object-file.c +0 -0
index 49c53f801f7..1a3ad558c45 100644
@@ -751,6 +751,44 @@ void add_to_alternates_memory(const char *reference)
                             '\n', NULL, 0);
 }

object-file.c void add_to_alternates_memory(const char *reference) · line 762

Quoted 10 lines
object-file.c +9 -0
 
+struct object_directory *set_temporary_primary_odb(const char *dir, int will_destroy)
+{
+       struct object_directory *new_odb;
+
+       /*
+        * Make sure alternates are initialized, or else our entry may be
+        * overwritten when they are.
+        */
+       prepare_alt_odb(the_repository);

This implicit dependence on the_repository is unfortunate. My versions passed the repository parameter explicitly. While my remerge-diff code doesn't really make use of that currently, it could make sense to have temporary object stores for a submodule and do remerge-diff work on them. You've also got two more uses of the_repository later in this function.

Quoted 7 lines
object-file.c +7 -0
+
+       /*
+        * Make a new primary odb and link the old primary ODB in as an
+        * alternate
+        */
+       new_odb = xcalloc(1, sizeof(*new_odb));
+       new_odb->path = xstrdup(dir);

object-file.c void add_to_alternates_memory(const char *reference) · line 779

Quoted 10 lines
object-file.c +10 -0
+       new_odb->is_temp = 1;
+       new_odb->will_destroy = will_destroy;
+       new_odb->next = the_repository->objects->odb;
+       the_repository->objects->odb = new_odb;
+       return new_odb->next;
+}
+
+void restore_primary_odb(struct object_directory *restore_odb, const char *old_path)
+{
+       struct object_directory *cur_odb = the_repository->objects->odb;

Another use of the_repository, and some more below.

Quoted 34 lines
object-file.c +17 -2
+
+       if (strcmp(old_path, cur_odb->path))
+               BUG("expected %s as primary object store; found %s",
+                   old_path, cur_odb->path);
+
+       if (cur_odb->next != restore_odb)
+               BUG("we expect the old primary object store to be the first alternate");
+
+       the_repository->objects->odb = restore_odb;
+       free_object_directory(cur_odb);
+}
+
 /*
  * Compute the exact path an alternate is at and returns it. In case of
  * error NULL is returned and the human readable error is added to `err`
@@ -1893,8 +1931,11 @@ int hash_object_file(const struct git_hash_algo *algo, const void *buf,
 /* Finalize a file on disk, and close it. */
 static void close_loose_object(int fd)
 {
-       if (fsync_object_files)
-               fsync_or_die(fd, "loose object file");
+       if (!the_repository->objects->odb->will_destroy) {
+               if (fsync_object_files)
+                       fsync_or_die(fd, "loose object file");
+       }
+
        if (close(fd) != 0)
                die_errno(_("error when closing loose object file"));
 }
object-store.h +0 -0
index 551639f173d..5bc9da6634e 100644

object-store.h struct object_directory { · line 39

Quoted 10 lines
object-store.h +6 -1
@@ -31,7 +31,12 @@ struct object_directory {
         * This is a temporary object store, so there is no need to
         * create new objects via rename.
         */
-       int is_temp;
+       int is_temp : 8;
+
+       /*
+        * This object store is ephemeral, so there is no need to fsync.
+        */
+       int will_destroy : 8;

Why 8 bits wide rather than 1? I thought these were boolean values...was I mistaken?

(Also, if boolean and compressing to 1 bit, should probably be unsigned rather than signed.)

Quoted 102 lines
object-store.h +14 -0
        /*
         * Path to the alternative object store. If this is a relative path,
@@ -64,6 +69,17 @@ void add_to_alternates_file(const char *dir);
  */
 void add_to_alternates_memory(const char *dir);
 
+/*
+ * Replace the current writable object directory with the specified temporary
+ * object directory; returns the former primary object directory.
+ */
+struct object_directory *set_temporary_primary_odb(const char *dir, int will_destroy);
+
+/*
+ * Restore a previous ODB replaced by set_temporary_main_odb.
+ */
+void restore_primary_odb(struct object_directory *restore_odb, const char *old_path);
+
 /*
  * Populate and return the loose object cache array corresponding to the
  * given object ID.
@@ -74,6 +90,9 @@ struct oidtree *odb_loose_cache(struct object_directory *odb,
 /* Empty the loose object cache for the specified object directory. */
 void odb_clear_loose_cache(struct object_directory *odb);
 
+/* Clear and free the specified object directory */
+void free_object_directory(struct object_directory *odb);
+
 struct packed_git {
        struct hashmap_entry packmap_ent;
        struct packed_git *next;
object.c +1 -1
index 4e85955a941..98635bc4043 100644
@@ -513,7 +513,7 @@ struct raw_object_store *raw_object_store_new(void)
        return o;
 }
 
-static void free_object_directory(struct object_directory *odb)
+void free_object_directory(struct object_directory *odb)
 {
        free(odb->path);
        odb_clear_loose_cache(odb);
tmp-objdir.c +13 -3
index b8d880e3626..366ffe28511 100644
@@ -11,6 +11,7 @@
 struct tmp_objdir {
        struct strbuf path;
        struct strvec env;
+       struct object_directory *prev_odb;
 };
 
 /*
@@ -38,6 +39,9 @@ static int tmp_objdir_destroy_1(struct tmp_objdir *t, int on_signal)
        if (t == the_tmp_objdir)
                the_tmp_objdir = NULL;
 
+       if (!on_signal && t->prev_odb)
+               restore_primary_odb(t->prev_odb, t->path.buf);
+
        /*
         * This may use malloc via strbuf_grow(), but we should
         * have pre-grown t->path sufficiently so that this
@@ -52,6 +56,7 @@ static int tmp_objdir_destroy_1(struct tmp_objdir *t, int on_signal)
         */
        if (!on_signal)
                tmp_objdir_free(t);
+
        return err;
 }
 
@@ -121,7 +126,7 @@ static int setup_tmp_objdir(const char *root)
        return ret;
 }
 
-struct tmp_objdir *tmp_objdir_create(void)
+struct tmp_objdir *tmp_objdir_create(const char *prefix)
 {
        static int installed_handlers;
        struct tmp_objdir *t;
@@ -129,11 +134,16 @@ struct tmp_objdir *tmp_objdir_create(void)
        if (the_tmp_objdir)
                BUG("only one tmp_objdir can be used at a time");
 
-       t = xmalloc(sizeof(*t));
+       t = xcalloc(1, sizeof(*t));
        strbuf_init(&t->path, 0);
        strvec_init(&t->env);
 
-       strbuf_addf(&t->path, "%s/incoming-XXXXXX", get_object_directory());
+       /*
+        * Use a string starting with tmp_ so that the builtin/prune.c code
+        * can recognize any stale objdirs left behind by a crash and delete
+        * them.
+        */
+       strbuf_addf(&t->path, "%s/tmp_objdir-%s-XXXXXX", get_object_directory(), prefix);
 
        /*
         * Grow the strbuf beyond any filename we expect to be placed in it.

tmp-objdir.c int tmp_objdir_migrate(struct tmp_objdir *t) · line 283

Quoted 5 lines
tmp-objdir.c +2 -0
@@ -269,6 +279,15 @@ int tmp_objdir_migrate(struct tmp_objdir *t)
        if (!t)
                return 0;
 
+
+

Why so many blank lines?

tmp-objdir.c int tmp_objdir_migrate(struct tmp_objdir *t) · line 285

Quoted 2 lines
tmp-objdir.c +2 -0
+       if (t->prev_odb) {
+               if (the_repository->objects->odb->will_destroy)

Another implicit dependence on the_repository.

Quoted 58 lines
tmp-objdir.c +12 -0
+                       BUG("migrating and ODB that was marked for destruction");
+               restore_primary_odb(t->prev_odb, t->path.buf);
+               t->prev_odb = NULL;
+       }
+
        strbuf_addbuf(&src, &t->path);
        strbuf_addstr(&dst, get_object_directory());
 
@@ -292,3 +311,10 @@ void tmp_objdir_add_as_alternate(const struct tmp_objdir *t)
 {
        add_to_alternates_memory(t->path.buf);
 }
+
+void tmp_objdir_replace_primary_odb(struct tmp_objdir *t, int will_destroy)
+{
+       if (t->prev_odb)
+               BUG("the primary object database is already replaced");
+       t->prev_odb = set_temporary_primary_odb(t->path.buf, will_destroy);
+}
tmp-objdir.h +11 -4
index b1e45b4c75d..75754cbfba6 100644
@@ -10,7 +10,7 @@
  *
  * Example:
  *
- *     struct tmp_objdir *t = tmp_objdir_create();
+ *     struct tmp_objdir *t = tmp_objdir_create("incoming");
  *     if (!run_command_v_opt_cd_env(cmd, 0, NULL, tmp_objdir_env(t)) &&
  *         !tmp_objdir_migrate(t))
  *             printf("success!\n");
@@ -22,9 +22,10 @@
 struct tmp_objdir;
 
 /*
- * Create a new temporary object directory; returns NULL on failure.
+ * Create a new temporary object directory with the specified prefix;
+ * returns NULL on failure.
  */
-struct tmp_objdir *tmp_objdir_create(void);
+struct tmp_objdir *tmp_objdir_create(const char *prefix);
 
 /*
  * Return a list of environment strings, suitable for use with
@@ -51,4 +52,11 @@ int tmp_objdir_destroy(struct tmp_objdir *);
  */
 void tmp_objdir_add_as_alternate(const struct tmp_objdir *);
 
+/*
+ * Replaces the main object store in the current process with the temporary
+ * object directory and makes the former main object store an alternate.
+ * If will_destroy is nonzero, the object directory may not be migrated.
+ */
+void tmp_objdir_replace_primary_odb(struct tmp_objdir *, int will_destroy);
+
 #endif /* TMP_OBJDIR_H */
--
Quoted 1 line

gitgitgadget

Other than those minor things, I couldn't find any problems.

#115 lore raw

Re: [PATCH v7 2/9] tmp-objdir: new API for creating temporary writable databases

Quoted 11 lines · Elijah Newren

Hi,

Thanks for working on this, and for moving this up in your series near the beginning.

Quoted 3 lines · Neeraj Singh via GitGitGadget

From: Neeraj Singh <[email protected]>

Quoted 7 lines
Quoted 2 lines

This patch is based on work by Elijah Newren. Any bugs however are my own.

This kind of information is often included in a commit message via a trailer such as:

or Helped-by: or Co-authored-by: or Contributions-by: .

Will fix. I didn't know what some acceptable trailers were. I'll use:

Quoted 17 lines

object-file.c void add_to_alternates_memory(const char *reference) · line 762

Quoted 10 lines
object-file.c +9 -0
+struct object_directory *set_temporary_primary_odb(const char *dir, int will_destroy)
+{
+       struct object_directory *new_odb;
+
+       /*
+        * Make sure alternates are initialized, or else our entry may be
+        * overwritten when they are.
+        */
+       prepare_alt_odb(the_repository);

This implicit dependence on the_repository is unfortunate. My versions passed the repository parameter explicitly. While my remerge-diff code doesn't really make use of that currently, it could make sense to have temporary object stores for a submodule and do remerge-diff work on them. You've also got two more uses of the_repository later in this function.

The core loose object code in object-file.c is riven with the_repository assumptions. I'd have to refactor that code (including the alternates code) to take repository arguments. Given the extensive assumptions, I'd like to push back on this suggestion and all of the related suggestions.

Quoted 19 lines
Quoted 5 lines
object-store.h +0 -0
index 551639f173d..5bc9da6634e 100644

object-store.h struct object_directory { · line 39

Quoted 10 lines
object-store.h +6 -1
@@ -31,7 +31,12 @@ struct object_directory {
         * This is a temporary object store, so there is no need to
         * create new objects via rename.
         */
-       int is_temp;
+       int is_temp : 8;
+
+       /*
+        * This object store is ephemeral, so there is no need to fsync.
+        */
+       int will_destroy : 8;

Why 8 bits wide rather than 1? I thought these were boolean values...was I mistaken?

Quoted 2 lines

(Also, if boolean and compressing to 1 bit, should probably be unsigned rather than signed.)

This will go away when I drop the rename patch. I wish we had a standard bool_t type which is one char wide. This is a microoptimization, since accessing bits usually encodes to more or larger instructions than accessing bytes.

Quoted 9 lines
Quoted 9 lines
object-store.h +2 -0
+        */
+       strbuf_addf(&t->path, "%s/tmp_objdir-%s-XXXXXX", get_object_directory(), prefix);
 
        /*
         * Grow the strbuf beyond any filename we expect to be placed in it.
@@ -269,6 +279,15 @@ int tmp_objdir_migrate(struct tmp_objdir *t)
        if (!t)
                return 0;
 
Quoted 5 lines

object-store.h int tmp_objdir_migrate(struct tmp_objdir *t) · line 283

Quoted 2 lines
object-store.h +2 -0
+
+

Why so many blank lines?

This was an accident, will remove.

Quoted 1 line

Other than those minor things, I couldn't find any problems.

Thanks for the review!

#116 lore raw

PATCH v7 3/9 bulk-checkin: rename 'state' variable and separate 'plugged' boolean

In reply to Neeraj K. Singh via GitGitGadget

From: Neeraj Singh <[email protected]>

Preparation for adding bulk-fsync to the bulk-checkin.c infrastructure.

* Rename 'state' variable to 'bulk_checkin_state', since we will later be adding 'bulk_fsync_state'. This also makes the variable easier to find in the debugger, since the name is more unique.

* Move the 'plugged' data member of 'bulk_checkin_state' into a separate static variable. Doing this avoids resetting the variable in finish_bulk_checkin when zeroing the 'bulk_checkin_state'. As-is, we seem to unintentionally disable the plugging functionality the first time a new packfile must be created due to packfile size limits. While disabling the plugging state only results in suboptimal behavior for the current code, it would be fatal for the bulk-fsync functionality later in this patch series.

---
 bulk-checkin.c | 22 ++++++++++++----------
 1 file changed, 12 insertions(+), 10 deletions(-)
bulk-checkin.c +12 -10
index 8785b2ac806..6ae18401e04 100644
@@ -10,9 +10,9 @@
 #include "packfile.h"
 #include "object-store.h"
 
-static struct bulk_checkin_state {
-	unsigned plugged:1;
+static int bulk_checkin_plugged;
 
+static struct bulk_checkin_state {
 	char *pack_tmp_name;
 	struct hashfile *f;
 	off_t offset;
@@ -21,7 +21,7 @@ static struct bulk_checkin_state {
 	struct pack_idx_entry **written;
 	uint32_t alloc_written;
 	uint32_t nr_written;
-} state;
+} bulk_checkin_state;
 
 static void finish_tmp_packfile(struct strbuf *basename,
 				const char *pack_tmp_name,
@@ -277,21 +277,23 @@ int index_bulk_checkin(struct object_id *oid,
 		       int fd, size_t size, enum object_type type,
 		       const char *path, unsigned flags)
 {
-	int status = deflate_to_pack(&state, oid, fd, size, type,
+	int status = deflate_to_pack(&bulk_checkin_state, oid, fd, size, type,
 				     path, flags);
-	if (!state.plugged)
-		finish_bulk_checkin(&state);
+	if (!bulk_checkin_plugged)
+		finish_bulk_checkin(&bulk_checkin_state);
 	return status;
 }
 
 void plug_bulk_checkin(void)
 {
-	state.plugged = 1;
+	assert(!bulk_checkin_plugged);
+	bulk_checkin_plugged = 1;
 }
 
 void unplug_bulk_checkin(void)
 {
-	state.plugged = 0;
-	if (state.f)
-		finish_bulk_checkin(&state);
+	assert(bulk_checkin_plugged);
+	bulk_checkin_plugged = 0;
+	if (bulk_checkin_state.f)
+		finish_bulk_checkin(&bulk_checkin_state);
 }
Signature · 1 line
gitgitgadget
#117 lore raw

PATCH v7 4/9 core.fsyncobjectfiles: batched disk flushes

In reply to Neeraj K. Singh via GitGitGadget

From: Neeraj Singh <[email protected]>

When adding many objects to a repo with core.fsyncObjectFiles set to true, the cost of fsync'ing each object file can become prohibitive.

One major source of the cost of fsync is the implied flush of the hardware writeback cache within the disk drive. Fortunately, Windows, and macOS offer mechanisms to write data from the filesystem page cache without initiating a hardware flush. Linux has the sync_file_range API, which issues a pagecache writeback request reliably after version 5.2.

This patch introduces a new 'core.fsyncObjectFiles = batch' option that batches up hardware flushes. It hooks into the bulk-checkin plugging and unplugging functionality and takes advantage of tmp-objdir.

When the new mode is enabled do the following for each new object:
1. Create the object in a tmp-objdir.
2. Issue a pagecache writeback request and wait for it to complete.

At the end of the entire transaction when unplugging bulk checkin:
1. Issue an fsync against a dummy file to flush the hardware writeback cache, which should by now have processed the tmp-objdir writes.
2. Rename all of the tmp-objdir files to their final names.
3. When updating the index and/or refs, we assume that Git will issue another fsync internal to that operation. This is not the case today, but may be a good extension to those components.

On a filesystem with a singular journal that is updated during name operations (e.g. create, link, rename, etc), such as NTFS, HFS+, or XFS we would expect the fsync to trigger a journal writeout so that this sequence is enough to ensure that the user's data is durable by the time the git command returns.

This change also updates the macOS code to trigger a real hardware flush via fnctl(fd, F_FULLFSYNC) when fsync_or_die is called. Previously, on macOS there was no guarantee of durability since a simple fsync(2) call does not flush any hardware caches.

_Performance numbers_:

Linux - Hyper-V VM running Kernel 5.11 (Ubuntu 20.04) on a fast SSD. Mac - macOS 11.5.1 running on a Mac mini on a 1TB Apple SSD. Windows - Same host as Linux, a preview version of Windows 11.
This number is from a patch later in the series.

Adding 500 files to the repo with 'git add' Times reported in seconds.

core.fsyncObjectFiles | Linux | Mac   | Windows
----------------------|-------|-------|--------
                false | 0.06  |  0.35 | 0.61
                true  | 1.88  | 11.18 | 2.47
                batch | 0.15  |  0.41 | 1.53
---
 Documentation/config/core.txt | 29 ++++++++++++---
 Makefile                      |  6 +++
 bulk-checkin.c                | 70 +++++++++++++++++++++++++++++++++++
 bulk-checkin.h                |  2 +
 cache.h                       |  8 +++-
 config.c                      |  7 +++-
 config.mak.uname              |  1 +
 configure.ac                  |  8 ++++
 environment.c                 |  2 +-
 git-compat-util.h             |  7 ++++
 object-file.c                 | 12 +++++-
 tmp-objdir.c                  |  2 -
 wrapper.c                     | 44 ++++++++++++++++++++++
 write-or-die.c                |  2 +-
 14 files changed, 187 insertions(+), 13 deletions(-)
Documentation/config/core.txt +23 -6
index c04f62a54a1..200b4d9f06e 100644
@@ -548,12 +548,29 @@ core.whitespace::
   errors. The default tab width is 8. Allowed values are 1 to 63.
 
 core.fsyncObjectFiles::
-	This boolean will enable 'fsync()' when writing object files.
-+
-This is a total waste of time and effort on a filesystem that orders
-data writes properly, but can be useful for filesystems that do not use
-journalling (traditional UNIX filesystems) or that only journal metadata
-and not file contents (OS X's HFS+, or Linux ext3 with "data=writeback").
+	A value indicating the level of effort Git will expend in
+	trying to make objects added to the repo durable in the event
+	of an unclean system shutdown. This setting currently only
+	controls loose objects in the object store, so updates to any
+	refs or the index may not be equally durable.
++
+* `false` allows data to remain in file system caches according to
+  operating system policy, whence it may be lost if the system loses power
+  or crashes.
+* `true` triggers a data integrity flush for each loose object added to the
+  object store. This is the safest setting that is likely to ensure durability
+  across all operating systems and file systems that honor the 'fsync' system
+  call. However, this setting comes with a significant performance cost on
+  common hardware. Git does not currently fsync parent directories for
+  newly-added files, so some filesystems may still allow data to be lost on
+  system crash.
+* `batch` enables an experimental mode that uses interfaces available in some
+  operating systems to write loose object data with a minimal set of FLUSH
+  CACHE (or equivalent) commands sent to the storage controller. If the
+  operating system interfaces are not available, this mode behaves the same as
+  `true`. This mode is expected to be as safe as `true` on macOS for repos
+  stored on HFS+ or APFS filesystems and on Windows for repos stored on NTFS or
+  ReFS.
 
 core.preloadIndex::
 	Enable parallel index preload for operations like 'git diff'
Makefile +6 -0
index a9f9b689f0c..313b3dc7cd6 100644
@@ -406,6 +406,8 @@ all::
 #
 # Define HAVE_CLOCK_MONOTONIC if your platform has CLOCK_MONOTONIC.
 #
+# Define HAVE_SYNC_FILE_RANGE if your platform has sync_file_range.
+#
 # Define NEEDS_LIBRT if your platform requires linking with librt (glibc version
 # before 2.17) for clock_gettime and CLOCK_MONOTONIC.
 #
@@ -1874,6 +1876,10 @@ ifdef HAVE_CLOCK_MONOTONIC
 	BASIC_CFLAGS += -DHAVE_CLOCK_MONOTONIC
 endif
 
+ifdef HAVE_SYNC_FILE_RANGE
+	BASIC_CFLAGS += -DHAVE_SYNC_FILE_RANGE
+endif
+
 ifdef NEEDS_LIBRT
 	EXTLIBS += -lrt
 endif
bulk-checkin.c +70 -0

106 lines not on this page · raw message

bulk-checkin.h +2 -0
index b26f3dc3b74..08f292379b6 100644
@@ -6,6 +6,8 @@
 
 #include "cache.h"
 
+void fsync_loose_object_bulk_checkin(int fd);
+
 int index_bulk_checkin(struct object_id *oid,
 		       int fd, size_t size, enum object_type type,
 		       const char *path, unsigned flags);
cache.h +7 -1
index f6295f3b048..1ed8137b5e6 100644
@@ -984,7 +984,13 @@ void reset_shared_repository(void);
 extern int read_replace_refs;
 extern char *git_replace_ref_base;
 
-extern int fsync_object_files;
+enum fsync_object_files_mode {
+    FSYNC_OBJECT_FILES_OFF,
+    FSYNC_OBJECT_FILES_ON,
+    FSYNC_OBJECT_FILES_BATCH
+};
+
+extern enum fsync_object_files_mode fsync_object_files;
 extern int core_preload_index;
 extern int precomposed_unicode;
 extern int protect_hfs;
config.c +6 -1
index 2edf835262f..8315d020eeb 100644
@@ -1506,7 +1506,12 @@ static int git_default_core_config(const char *var, const char *value, void *cb)
 	}
 
 	if (!strcmp(var, "core.fsyncobjectfiles")) {
-		fsync_object_files = git_config_bool(var, value);
+		if (value && !strcmp(value, "batch"))
+			fsync_object_files = FSYNC_OBJECT_FILES_BATCH;
+		else if (git_config_bool(var, value))
+			fsync_object_files = FSYNC_OBJECT_FILES_ON;
+		else
+			fsync_object_files = FSYNC_OBJECT_FILES_OFF;
 		return 0;
 	}
 
config.mak.uname +1 -0
index 76516aaa9a5..e6d482fbcc6 100644
@@ -53,6 +53,7 @@ ifeq ($(uname_S),Linux)
 	HAVE_CLOCK_MONOTONIC = YesPlease
 	# -lrt is needed for clock_gettime on glibc <= 2.16
 	NEEDS_LIBRT = YesPlease
+	HAVE_SYNC_FILE_RANGE = YesPlease
 	HAVE_GETDELIM = YesPlease
 	SANE_TEXT_GREP=-a
 	FREAD_READS_DIRECTORIES = UnfortunatelyYes
configure.ac +8 -0
index 031e8d3fee8..c711037d625 100644
@@ -1090,6 +1090,14 @@ AC_COMPILE_IFELSE([CLOCK_MONOTONIC_SRC],
 	[AC_MSG_RESULT([no])
 	HAVE_CLOCK_MONOTONIC=])
 GIT_CONF_SUBST([HAVE_CLOCK_MONOTONIC])
+
+#
+# Define HAVE_SYNC_FILE_RANGE=YesPlease if sync_file_range is available.
+GIT_CHECK_FUNC(sync_file_range,
+	[HAVE_SYNC_FILE_RANGE=YesPlease],
+	[HAVE_SYNC_FILE_RANGE])
+GIT_CONF_SUBST([HAVE_SYNC_FILE_RANGE])
+
 #
 # Define NO_SETITIMER if you don't have setitimer.
 GIT_CHECK_FUNC(setitimer,
environment.c +1 -1
index 30fca67e6d6..371a73c1e30 100644
@@ -42,7 +42,7 @@ const char *git_attributes_file;
 const char *git_hooks_path;
 int zlib_compression_level = Z_BEST_SPEED;
 int pack_compression_level = Z_DEFAULT_COMPRESSION;
-int fsync_object_files;
+enum fsync_object_files_mode fsync_object_files;
 size_t packed_git_window_size = DEFAULT_PACKED_GIT_WINDOW_SIZE;
 size_t packed_git_limit = DEFAULT_PACKED_GIT_LIMIT;
 size_t delta_base_cache_limit = 96 * 1024 * 1024;
git-compat-util.h +7 -0
index 7c99eef6612..9daee873782 100644
@@ -1213,6 +1213,13 @@ __attribute__((format (printf, 1, 2))) NORETURN
 void BUG(const char *fmt, ...);
 #endif
 
+enum fsync_action {
+    FSYNC_WRITEOUT_ONLY,
+    FSYNC_HARDWARE_FLUSH
+};
+
+int git_fsync(int fd, enum fsync_action action);
+
 /*
  * Preserves errno, prints a message, but gives no warning for ENOENT.
  * Returns 0 on success, which includes trying to unlink an object that does
object-file.c +11 -1
index 1a3ad558c45..8ea1348f0db 100644
@@ -1932,8 +1932,18 @@ int hash_object_file(const struct git_hash_algo *algo, const void *buf,
 static void close_loose_object(int fd)
 {
 	if (!the_repository->objects->odb->will_destroy) {
-		if (fsync_object_files)
+		switch (fsync_object_files) {
+		case FSYNC_OBJECT_FILES_OFF:
+			break;
+		case FSYNC_OBJECT_FILES_ON:
 			fsync_or_die(fd, "loose object file");
+			break;
+		case FSYNC_OBJECT_FILES_BATCH:
+			fsync_loose_object_bulk_checkin(fd);
+			break;
+		default:
+			BUG("Invalid fsync_object_files mode.");
+		}
 	}
 
 	if (close(fd) != 0)
tmp-objdir.c +0 -2
index 366ffe28511..c26cb5eafee 100644
@@ -279,8 +279,6 @@ int tmp_objdir_migrate(struct tmp_objdir *t)
 	if (!t)
 		return 0;
 
-
-
 	if (t->prev_odb) {
 		if (the_repository->objects->odb->will_destroy)
 			BUG("migrating and ODB that was marked for destruction");
wrapper.c +44 -0

50 lines not on this page · raw message

write-or-die.c +1 -1
index 0b1ec8190b6..cc8291d9794 100644
@@ -57,7 +57,7 @@ void fprintf_or_die(FILE *f, const char *fmt, ...)
 
 void fsync_or_die(int fd, const char *msg)
 {
-	while (fsync(fd) < 0) {
+	while (git_fsync(fd, FSYNC_HARDWARE_FLUSH) < 0) {
 		if (errno != EINTR)
 			die_errno("fsync error on '%s'", msg);
 	}
Signature · 1 line
gitgitgadget
#118 lore raw

PATCH v7 5/9 core.fsyncobjectfiles: add windows support for batch mode

In reply to Neeraj K. Singh via GitGitGadget

From: Neeraj Singh <[email protected]>

This commit adds a win32 implementation for fsync_no_flush that is called git_fsync. The 'NtFlushBuffersFileEx' function being called is available since Windows 8. If the function is not available, we return -1 and Git falls back to doing a full fsync.

The operating system is told to flush data only without a hardware flush primitive. A later full fsync will cause the metadata log to be flushed and then the disk cache to be flushed on NTFS and ReFS. Other filesystems will treat this as a full flush operation.

I added a new file here for this system call so as not to conflict with downstream changes in the git-for-windows repository related to fscache.

---
 compat/mingw.h                      |  3 +++
 compat/win32/flush.c                | 28 ++++++++++++++++++++++++++++
 config.mak.uname                    |  2 ++
 contrib/buildsystems/CMakeLists.txt |  3 ++-
 wrapper.c                           |  4 ++++
 5 files changed, 39 insertions(+), 1 deletion(-)
 create mode 100644 compat/win32/flush.c
compat/mingw.h +3 -0
index c9a52ad64a6..6074a3d3ced 100644
@@ -329,6 +329,9 @@ int mingw_getpagesize(void);
 #define getpagesize mingw_getpagesize
 #endif
 
+int win32_fsync_no_flush(int fd);
+#define fsync_no_flush win32_fsync_no_flush
+
 struct rlimit {
 	unsigned int rlim_cur;
 };
compat/win32/flush.c new file +28 -0
new file mode 100644
index 00000000000..75324c24ee7
@@ -0,0 +1,28 @@
+#include "../../git-compat-util.h"
+#include <winternl.h>
+#include "lazyload.h"
+
+int win32_fsync_no_flush(int fd)
+{
+       IO_STATUS_BLOCK io_status;
+
+#define FLUSH_FLAGS_FILE_DATA_ONLY 1
+
+       DECLARE_PROC_ADDR(ntdll.dll, NTSTATUS, NtFlushBuffersFileEx,
+			 HANDLE FileHandle, ULONG Flags, PVOID Parameters, ULONG ParameterSize,
+			 PIO_STATUS_BLOCK IoStatusBlock);
+
+       if (!INIT_PROC_ADDR(NtFlushBuffersFileEx)) {
+		errno = ENOSYS;
+		return -1;
+       }
+
+       memset(&io_status, 0, sizeof(io_status));
+       if (NtFlushBuffersFileEx((HANDLE)_get_osfhandle(fd), FLUSH_FLAGS_FILE_DATA_ONLY,
+				NULL, 0, &io_status)) {
+		errno = EINVAL;
+		return -1;
+       }
+
+       return 0;
+}
config.mak.uname +2 -0
index e6d482fbcc6..34c93314a50 100644
@@ -451,6 +451,7 @@ endif
 	CFLAGS =
 	BASIC_CFLAGS = -nologo -I. -Icompat/vcbuild/include -DWIN32 -D_CONSOLE -DHAVE_STRING_H -D_CRT_SECURE_NO_WARNINGS -D_CRT_NONSTDC_NO_DEPRECATE
 	COMPAT_OBJS = compat/msvc.o compat/winansi.o \
+		compat/win32/flush.o \
 		compat/win32/path-utils.o \
 		compat/win32/pthread.o compat/win32/syslog.o \
 		compat/win32/trace2_win32_process_info.o \
@@ -626,6 +627,7 @@ ifneq (,$(findstring MINGW,$(uname_S)))
 	COMPAT_CFLAGS += -DSTRIP_EXTENSION=\".exe\"
 	COMPAT_OBJS += compat/mingw.o compat/winansi.o \
 		compat/win32/trace2_win32_process_info.o \
+		compat/win32/flush.o \
 		compat/win32/path-utils.o \
 		compat/win32/pthread.o compat/win32/syslog.o \
 		compat/win32/dirent.o
contrib/buildsystems/CMakeLists.txt +2 -1
index 171b4124afe..b573a5ee122 100644
@@ -261,7 +261,8 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
 				NOGDI OBJECT_CREATION_MODE=1 __USE_MINGW_ANSI_STDIO=0
 				USE_NED_ALLOCATOR OVERRIDE_STRDUP MMAP_PREVENTS_DELETE USE_WIN32_MMAP
 				UNICODE _UNICODE HAVE_WPGMPTR ENSURE_MSYSTEM_IS_SET)
-	list(APPEND compat_SOURCES compat/mingw.c compat/winansi.c compat/win32/path-utils.c
+	list(APPEND compat_SOURCES compat/mingw.c compat/winansi.c
+		compat/win32/flush.c compat/win32/path-utils.c
 		compat/win32/pthread.c compat/win32mmap.c compat/win32/syslog.c
 		compat/win32/trace2_win32_process_info.c compat/win32/dirent.c
 		compat/nedmalloc/nedmalloc.c compat/strdup.c)
wrapper.c +4 -0
index bb4f9f043ce..1a1e2fba9c9 100644
@@ -567,6 +567,10 @@ int git_fsync(int fd, enum fsync_action action)
 						 SYNC_FILE_RANGE_WAIT_AFTER);
 #endif
 
+#ifdef fsync_no_flush
+		return fsync_no_flush(fd);
+#endif
+
 		errno = ENOSYS;
 		return -1;
 
Signature · 1 line
gitgitgadget
#119 lore raw

PATCH v7 6/9 update-index: use the bulk-checkin infrastructure

In reply to Neeraj K. Singh via GitGitGadget

From: Neeraj Singh <[email protected]>

The update-index functionality is used internally by 'git stash push' to setup the internal stashed commit.

This change enables bulk-checkin for update-index infrastructure to speed up adding new objects to the object database by leveraging the pack functionality and the new bulk-fsync functionality.

There is some risk with this change, since under batch fsync, the object files will not be available until the update-index is entirely complete. This usage is unlikely, since any tool invoking update-index and expecting to see objects would have to synchronize with the update-index process after passing it a file path.

---
 builtin/update-index.c | 6 ++++++
 1 file changed, 6 insertions(+)
builtin/update-index.c +6 -0
index 187203e8bb5..dc7368bb1ee 100644
@@ -5,6 +5,7 @@
  */
 #define USE_THE_INDEX_COMPATIBILITY_MACROS
 #include "cache.h"
+#include "bulk-checkin.h"
 #include "config.h"
 #include "lockfile.h"
 #include "quote.h"
@@ -1088,6 +1089,9 @@ int cmd_update_index(int argc, const char **argv, const char *prefix)
 
 	the_index.updated_skipworktree = 1;
 
+	/* we might be adding many objects to the object database */
+	plug_bulk_checkin();
+
 	/*
 	 * Custom copy of parse_options() because we want to handle
 	 * filename arguments as they come.
@@ -1168,6 +1172,8 @@ int cmd_update_index(int argc, const char **argv, const char *prefix)
 		strbuf_release(&buf);
 	}
 
+	/* by now we must have added all of the new objects */
+	unplug_bulk_checkin();
 	if (split_index > 0) {
 		if (git_config_get_split_index() == 0)
 			warning(_("core.splitIndex is set to false; "
Signature · 1 line
gitgitgadget
#120 lore raw

PATCH v7 7/9 unpack-objects: use the bulk-checkin infrastructure

In reply to Neeraj K. Singh via GitGitGadget

From: Neeraj Singh <[email protected]>

The unpack-objects functionality is used by fetch, push, and fast-import to turn the transfered data into object database entries when there are fewer objects than the 'unpacklimit' setting.

By enabling bulk-checkin when unpacking objects, we can take advantage of batched fsyncs.

---
 builtin/unpack-objects.c | 3 +++
 1 file changed, 3 insertions(+)
builtin/unpack-objects.c +3 -0
index 4a9466295ba..51eb4f7b531 100644
@@ -1,5 +1,6 @@
 #include "builtin.h"
 #include "cache.h"
+#include "bulk-checkin.h"
 #include "config.h"
 #include "object-store.h"
 #include "object.h"
@@ -503,10 +504,12 @@ static void unpack_all(void)
 	if (!quiet)
 		progress = start_progress(_("Unpacking objects"), nr_objects);
 	CALLOC_ARRAY(obj_list, nr_objects);
+	plug_bulk_checkin();
 	for (i = 0; i < nr_objects; i++) {
 		unpack_one(i);
 		display_progress(progress, i + 1);
 	}
+	unplug_bulk_checkin();
 	stop_progress(&progress);
 
 	if (delta_list)
Signature · 1 line
gitgitgadget
#121 lore raw

PATCH v7 8/9 core.fsyncobjectfiles: tests for batch mode

In reply to Neeraj K. Singh via GitGitGadget

From: Neeraj Singh <[email protected]>

Add test cases to exercise batch mode for:
* 'git add'
* 'git stash'
* 'git update-index'
* 'git unpack-objects'

These tests ensure that the added data winds up in the object database.

In this change we introduce a new test helper lib-unique-files.sh. The goal of this library is to create a tree of files that have different oids from any other files that may have been created in the current test repo. This helps us avoid missing validation of an object being added due to it already being in the repo.

---
 t/lib-unique-files.sh  | 36 ++++++++++++++++++++++++++++++++++++
 t/t3700-add.sh         | 20 ++++++++++++++++++++
 t/t3903-stash.sh       | 14 ++++++++++++++
 t/t5300-pack-object.sh | 30 +++++++++++++++++++-----------
 4 files changed, 89 insertions(+), 11 deletions(-)
 create mode 100644 t/lib-unique-files.sh
t/lib-unique-files.sh new file +36 -0
new file mode 100644
index 00000000000..a7de4ca8512
@@ -0,0 +1,36 @@
+# Helper to create files with unique contents
+
+
+# Create multiple files with unique contents. Takes the number of
+# directories, the number of files in each directory, and the base
+# directory.
+#
+# test_create_unique_files 2 3 my_dir -- Creates 2 directories with 3 files
+#					 each in my_dir, all with unique
+#					 contents.
+
+test_create_unique_files() {
+	test "$#" -ne 3 && BUG "3 param"
+
+	local dirs=$1
+	local files=$2
+	local basedir=$3
+	local counter=0
+	test_tick
+	local basedata=$test_tick
+
+
+	rm -rf $basedir
+
+	for i in $(test_seq $dirs)
+	do
+		local dir=$basedir/dir$i
+
+		mkdir -p "$dir"
+		for j in $(test_seq $files)
+		do
+			counter=$((counter + 1))
+			echo "$basedata.$counter"  >"$dir/file$j.txt"
+		done
+	done
+}
t/t3700-add.sh +20 -0
index 4086e1ebbc9..36049a53ff7 100755
@@ -7,6 +7,8 @@ test_description='Test of git add, including the -- option.'
 
 . ./test-lib.sh
 
+. $TEST_DIRECTORY/lib-unique-files.sh
+
 # Test the file mode "$1" of the file "$2" in the index.
 test_mode_in_index () {
 	case "$(git ls-files -s "$2")" in
@@ -33,6 +35,24 @@ test_expect_success \
     'Test that "git add -- -q" works' \
     'touch -- -q && git add -- -q'
 
+test_expect_success 'git add: core.fsyncobjectfiles=batch' "
+	test_create_unique_files 2 4 fsync-files &&
+	git -c core.fsyncobjectfiles=batch add -- ./fsync-files/ &&
+	rm -f fsynced_files &&
+	git ls-files --stage fsync-files/ > fsynced_files &&
+	test_line_count = 8 fsynced_files &&
+	awk -- '{print \$2}' fsynced_files | xargs -n1 git cat-file -e
+"
+
+test_expect_success 'git update-index: core.fsyncobjectfiles=batch' "
+	test_create_unique_files 2 4 fsync-files2 &&
+	find fsync-files2 ! -type d -print | xargs git -c core.fsyncobjectfiles=batch update-index --add -- &&
+	rm -f fsynced_files2 &&
+	git ls-files --stage fsync-files2/ > fsynced_files2 &&
+	test_line_count = 8 fsynced_files2 &&
+	awk -- '{print \$2}' fsynced_files2 | xargs -n1 git cat-file -e
+"
+
 test_expect_success \
 	'git add: Test that executable bit is not used if core.filemode=0' \
 	'git config core.filemode 0 &&
t/t3903-stash.sh +14 -0
index 873aa56e359..2fc819e5584 100755
@@ -9,6 +9,7 @@ GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
 export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
 
 . ./test-lib.sh
+. $TEST_DIRECTORY/lib-unique-files.sh
 
 diff_cmp () {
 	for i in "$1" "$2"
@@ -1293,6 +1294,19 @@ test_expect_success 'stash handles skip-worktree entries nicely' '
 	git rev-parse --verify refs/stash:A.t
 '
 
+test_expect_success 'stash with core.fsyncobjectfiles=batch' "
+	test_create_unique_files 2 4 fsync-files &&
+	git -c core.fsyncobjectfiles=batch stash push -u -- ./fsync-files/ &&
+	rm -f fsynced_files &&
+
+	# The files were untracked, so use the third parent,
+	# which contains the untracked files
+	git ls-tree -r stash^3 -- ./fsync-files/ > fsynced_files &&
+	test_line_count = 8 fsynced_files &&
+	awk -- '{print \$3}' fsynced_files | xargs -n1 git cat-file -e
+"
+
+
 test_expect_success 'stash -c stash.useBuiltin=false warning ' '
 	expected="stash.useBuiltin support has been removed" &&
 
t/t5300-pack-object.sh +19 -11
index e13a8842075..38663dc1393 100755
@@ -162,23 +162,23 @@ test_expect_success 'pack-objects with bogus arguments' '
 
 check_unpack () {
 	test_when_finished "rm -rf git2" &&
-	git init --bare git2 &&
-	git -C git2 unpack-objects -n <"$1".pack &&
-	git -C git2 unpack-objects <"$1".pack &&
-	(cd .git && find objects -type f -print) |
-	while read path
-	do
-		cmp git2/$path .git/$path || {
-			echo $path differs.
-			return 1
-		}
-	done
+	git $2 init --bare git2 &&
+	(
+		git $2 -C git2 unpack-objects -n <"$1".pack &&
+		git $2 -C git2 unpack-objects <"$1".pack &&
+		git $2 -C git2 cat-file --batch-check="%(objectname)"
+	) <obj-list >current &&
+	cmp obj-list current
 }
 
 test_expect_success 'unpack without delta' '
 	check_unpack test-1-${packname_1}
 '
 
+test_expect_success 'unpack without delta (core.fsyncobjectfiles=batch)' '
+	check_unpack test-1-${packname_1} "-c core.fsyncobjectfiles=batch"
+'
+
 test_expect_success 'pack with REF_DELTA' '
 	packname_2=$(git pack-objects --progress test-2 <obj-list 2>stderr) &&
 	check_deltas stderr -gt 0
@@ -188,6 +188,10 @@ test_expect_success 'unpack with REF_DELTA' '
 	check_unpack test-2-${packname_2}
 '
 
+test_expect_success 'unpack with REF_DELTA (core.fsyncobjectfiles=batch)' '
+       check_unpack test-2-${packname_2} "-c core.fsyncobjectfiles=batch"
+'
+
 test_expect_success 'pack with OFS_DELTA' '
 	packname_3=$(git pack-objects --progress --delta-base-offset test-3 \
 			<obj-list 2>stderr) &&
@@ -198,6 +202,10 @@ test_expect_success 'unpack with OFS_DELTA' '
 	check_unpack test-3-${packname_3}
 '
 
+test_expect_success 'unpack with OFS_DELTA (core.fsyncobjectfiles=batch)' '
+       check_unpack test-3-${packname_3} "-c core.fsyncobjectfiles=batch"
+'
+
 test_expect_success 'compare delta flavors' '
 	perl -e '\''
 		defined($_ = -s $_) or die for @ARGV;
Signature · 1 line
gitgitgadget
#122 lore raw

PATCH v7 9/9 core.fsyncobjectfiles: performance tests for add and stash

In reply to Neeraj K. Singh via GitGitGadget

From: Neeraj Singh <[email protected]>

Add a basic performance test for "git add" and "git stash" of a lot of new objects with various fsync settings.

---
 t/perf/p3700-add.sh   | 43 ++++++++++++++++++++++++++++++++++++++++
 t/perf/p3900-stash.sh | 46 +++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 89 insertions(+)
 create mode 100755 t/perf/p3700-add.sh
 create mode 100755 t/perf/p3900-stash.sh
t/perf/p3700-add.sh new file +43 -0
new file mode 100755
index 00000000000..e93c08a2e70
@@ -0,0 +1,43 @@
+#!/bin/sh
+#
+# This test measures the performance of adding new files to the object database
+# and index. The test was originally added to measure the effect of the
+# core.fsyncObjectFiles=batch mode, which is why we are testing different values
+# of that setting explicitly and creating a lot of unique objects.
+
+test_description="Tests performance of add"
+
+. ./perf-lib.sh
+
+. $TEST_DIRECTORY/lib-unique-files.sh
+
+test_perf_default_repo
+test_checkout_worktree
+
+dir_count=10
+files_per_dir=50
+total_files=$((dir_count * files_per_dir))
+
+# We need to create the files each time we run the perf test, but
+# we do not want to measure the cost of creating the files, so run
+# the tet once.
+if test "${GIT_PERF_REPEAT_COUNT-1}" -ne 1
+then
+	echo "warning: Setting GIT_PERF_REPEAT_COUNT=1" >&2
+	GIT_PERF_REPEAT_COUNT=1
+fi
+
+for m in false true batch
+do
+	test_expect_success "create the files for core.fsyncObjectFiles=$m" '
+		git reset --hard &&
+		# create files across directories
+		test_create_unique_files $dir_count $files_per_dir files
+	'
+
+	test_perf "add $total_files files (core.fsyncObjectFiles=$m)" "
+		git -c core.fsyncobjectfiles=$m add files
+	"
+done
+
+test_done
t/perf/p3900-stash.sh new file +46 -0

46 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#123 lore raw

v8 cover Implement a batched fsync option for core.fsyncObjectFiles

In reply to Neeraj K. Singh via GitGitGadget

Thanks to everyone for review so far!

This series shares the base tmp-objdir patches with my merged version of Elijah Newren's remerge-diff series at:
https://github.com/neerajsi-msft/git/tree/neerajsi/remerge-diff.

The patch is now at version 8: changes since v7:

* Dropped the tmp-objdir patch to avoid renaming in a quarantine/temporary objdir, as suggested by Jeff King. This wasn't a good idea because we don't really know that there's only a single reader/writer. Avoiding the rename was a relatively minor perf optimization so it's okay to drop.

* Added disable_ref_updates logic (as a flag on the odb) which is set when we're in a quarantine or when a tmp objdir is active. I believe this roughly follows the strategy suggested by Jeff King.

The patch is now at version 7: changes since v6:

* Rebased onto current upstream master

* Separate the tmp-objdir changes and move to the beginning of the series so that Elijah Newren's similar changes can be merged.

* Use some of Elijah's implementation for replacing the primary ODB. I was doing some unnecessarily complex copying for no good reason.

* Make the tmp objdir code use a name beginning with tmp_ and having a operation-specific prefix.

* Add git-prune support for removing a stale object directory.

v5 was a bit of a dud, with some issues that I only noticed after submitting. v6 changes:

* re-add Windows support
* fix minor formatting issues
* reset git author and commit dates which got messed up

Changes since v4, all in response to review feedback from Ævar Arnfjörð Bjarmason:

* Update core.fsyncobjectfiles documentation to specify 'loose' objects and to add a statement about not fsyncing parent directories.

* I still don't want to make any promises on behalf of the Linux FS developers in the documentation. However, according to [v4.1] and my understanding of how XFS journals are documented to work, it looks like recent versions of Linux running on XFS should be as safe as Windows or macOS in 'batch' mode. I don't know about ext4, since it's not clear to me when metadata updates are made visible to the journal.

* Rewrite the core batched fsync change to use the tmp-objdir lib. As Ævar pointed out, this lets us access the added loose objects immediately, rather than only after unplugging the bulk checkin. This is a hard requirement in unpack-objects for resolving OBJ_REF_DELTA packed objects.

* As a preparatory patch, the object-file code now doesn't do a rename if it's in a tmp objdir (as determined by the quarantine environment variable).

* I added support to the tmp-objdir lib to replace the 'main' writable odb.

* Instead of using a lockfile for the final full fsync, we now use a new dummy temp file. Doing that makes the below unpack-objects change easier.

* Add bulk-checkin support to unpack-objects, which is used in fetch and push. In addition to making those operations faster, it allows us to directly compare performance of packfiles against loose objects. Please see [v4.2] for a measurement of 'git push' to a local upstream with different numbers of unique new files.

* Rename FSYNC_OBJECT_FILES_MODE to fsync_object_files_mode.

* Remove comment with link to NtFlushBuffersFileEx documentation.

* Make t/lib-unique-files.sh a bit cleaner. We are still creating unique contents, but now this uses test_tick, so it should be deterministic from run to run.

* Ensure there are tests for all of the modified commands. Make the unpack-objects tests validate that the unpacked objects are really available in the ODB.

References for v4: [v4.1]
https://lore.kernel.org/linux-fsdevel/[email protected]/#t

[v4.2]
https://docs.google.com/spreadsheets/d/1uxMBkEXFFnQ1Y3lXKqcKpw6Mq44BzhpCAcPex14T-QQ/edit#gid=1898936117

Changes since v3:

* Fix core.fsyncobjectfiles option parsing as suggested by Junio: We now accept no value to mean "true" and we require 'batch' to be lowercase.

* Leave the default fsync mode as 'false'. Git for windows can change its default when this series makes it over to that fork.

* Use a switch statement in git_fsync, as suggested by Junio.

* Add regression test cases for core.fsyncobjectfiles=batch. This should keep the batch functionality basically working in upstream git even if few users adopt batch mode initially. I expect git-for-windows will provide a good baking area for the new mode.

Neeraj Singh (9):
tmp-objdir: new API for creating temporary writable databases
tmp-objdir: disable ref updates when replacing the primary odb
bulk-checkin: rename 'state' variable and separate 'plugged' boolean core.fsyncobjectfiles: batched disk flushes core.fsyncobjectfiles: add windows support for batch mode
update-index: use the bulk-checkin infrastructure
unpack-objects: use the bulk-checkin infrastructure core.fsyncobjectfiles: tests for batch mode core.fsyncobjectfiles: performance tests for add and stash

 Documentation/config/core.txt       | 29 ++++++++--
 Makefile                            |  6 ++
 builtin/prune.c                     | 22 +++++--
 builtin/receive-pack.c              |  2 +-
 builtin/unpack-objects.c            |  3 +
 builtin/update-index.c              |  6 ++
 bulk-checkin.c                      | 90 +++++++++++++++++++++++++----
 bulk-checkin.h                      |  2 +
 cache.h                             |  8 ++-
 compat/mingw.h                      |  3 +
 compat/win32/flush.c                | 28 +++++++++
 config.c                            |  7 ++-
 config.mak.uname                    |  3 +
 configure.ac                        |  8 +++
 contrib/buildsystems/CMakeLists.txt |  3 +-
 environment.c                       |  6 +-
 git-compat-util.h                   |  7 +++
 object-file.c                       | 60 ++++++++++++++++++-
 object-store.h                      | 26 +++++++++
 object.c                            |  2 +-
 refs.c                              |  2 +-
 repository.c                        |  2 +
 repository.h                        |  1 +
 t/lib-unique-files.sh               | 36 ++++++++++++
 t/perf/p3700-add.sh                 | 43 ++++++++++++++
 t/perf/p3900-stash.sh               | 46 +++++++++++++++
 t/t3700-add.sh                      | 20 +++++++
 t/t3903-stash.sh                    | 14 +++++
 t/t5300-pack-object.sh              | 30 ++++++----
 tmp-objdir.c                        | 30 +++++++++-
 tmp-objdir.h                        | 14 ++++-
 wrapper.c                           | 48 +++++++++++++++
 write-or-die.c                      |  2 +-
 33 files changed, 562 insertions(+), 47 deletions(-)
 create mode 100644 compat/win32/flush.c
 create mode 100644 t/lib-unique-files.sh
 create mode 100755 t/perf/p3700-add.sh
 create mode 100755 t/perf/p3900-stash.sh

base-commit: cefe983a320c03d7843ac78e73bd513a27806845
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1076%2Fneerajsi-msft%2Fneerajsi%2Fbulk-fsync-object-files-v8
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1076/neerajsi-msft/neerajsi/bulk-fsync-object-files-v8
Pull-Request: https://github.com/git/git/pull/1076

Range-diff vs v7:

2: 6ce72a709a1 ! 1: f03797fd80d tmp-objdir: new API for creating temporary writable databases

     @@ Metadata
       ## Commit message ##
          tmp-objdir: new API for creating temporary writable databases
      
     -    This patch is based on work by Elijah Newren. Any bugs however are my
     -    own.
     -
          The tmp_objdir API provides the ability to create temporary object
          directories, but was designed with the goal of having subprocesses
          access these object stores, followed by the main process migrating
     @@ Commit message
          make sure that they have a name starting with tmp_ and containing an
          operation-specific name.
      
     +    Based-on-patch-by: Elijah Newren <[email protected]>
     +
          Signed-off-by: Neeraj Singh <[email protected]>
      
       ## builtin/prune.c ##
     @@ object-file.c: void add_to_alternates_memory(const char *reference)
      +	 */
      +	new_odb = xcalloc(1, sizeof(*new_odb));
      +	new_odb->path = xstrdup(dir);
     -+	new_odb->is_temp = 1;
      +	new_odb->will_destroy = will_destroy;
      +	new_odb->next = the_repository->objects->odb;
      +	the_repository->objects->odb = new_odb;
     @@ object-file.c: int hash_object_file(const struct git_hash_algo *algo, const void
      
       ## object-store.h ##
      @@ object-store.h: struct object_directory {
     - 	 * This is a temporary object store, so there is no need to
     - 	 * create new objects via rename.
     - 	 */
     --	int is_temp;
     -+	int is_temp : 8;
     -+
     + 	uint32_t loose_objects_subdir_seen[8]; /* 256 bits */
     + 	struct oidtree *loose_objects_cache;
     + 
      +	/*
      +	 * This object store is ephemeral, so there is no need to fsync.
      +	 */
     -+	int will_destroy : 8;
     - 
     ++	int will_destroy;
     ++
       	/*
       	 * Path to the alternative object store. If this is a relative path,
     + 	 * it is relative to the current working directory.
      @@ object-store.h: void add_to_alternates_file(const char *dir);
        */
       void add_to_alternates_memory(const char *dir);
     @@ tmp-objdir.c: int tmp_objdir_migrate(struct tmp_objdir *t)
       	if (!t)
       		return 0;
       
     -+
     -+
      +	if (t->prev_odb) {
      +		if (the_repository->objects->odb->will_destroy)
     -+			BUG("migrating and ODB that was marked for destruction");
     ++			BUG("migrating an ODB that was marked for destruction");
      +		restore_primary_odb(t->prev_odb, t->path.buf);
      +		t->prev_odb = NULL;
      +	}

1: 6e65f68fd6d ! 2: bc085137340 object-file.c: do not rename in a temp odb
@@ Metadata
Author: Neeraj Singh <[email protected]>

## Commit message ##
- object-file.c: do not rename in a temp odb
+ tmp-objdir: disable ref updates when replacing the primary odb

- If a temporary ODB is active, as determined by GIT_QUARANTINE_PATH
- being set, create object files with their final names. This avoids
- an extra rename beyond what is needed to merge the temporary ODB in
- tmp_objdir_migrate.
+ When creating a subprocess with a temporary ODB, we set the
+ GIT_QUARANTINE_ENVIRONMENT env var to tell child Git processes not
+ to update refs, since the tmp-objdir may go away.

- Creating an object file with the expected final name should be okay
- since the git process writing to the temporary object store is the
- only writer, and it only invokes write_loose_object/create_object_file
- after checking that the object doesn't exist.
+ Introduce a similar mechanism for in-process temporary ODBs when
+ we call tmp_objdir_replace_primary_odb. Now both mechanisms set
+ the disable_ref_updates flag on the odb, which is queried by
+ the ref_transaction_prepare function.
+
+ Note: This change adds an assumption that the state of
+ the_repository is relevant for any ref transaction that might
+ be initiated. Unwinding this assumption should be straightforward
+ by saving the relevant repository to query in the transaction or
+ the ref_store.
+
+ Peff's test case was invoking ref updates via the cachetextconv
+ setting. That particular code silently does nothing when a ref
+ update is forbidden. See the call to notes_cache_put in
+ fill_textconv where errors are ignored.
+
+ Reported-by: Jeff King <[email protected]>

@@ environment.c: void setup_git_env(const char *git_dir)
args.index_file = getenv_safe(&to_free, INDEX_ENVIRONMENT); args.alternate_db = getenv_safe(&to_free, ALTERNATE_DB_ENVIRONMENT);
+ if (getenv(GIT_QUARANTINE_ENVIRONMENT)) {
-+ args.object_dir_is_temp = 1;
++ args.disable_ref_updates = 1;
+ }
+
repo_set_gitdir(the_repository, git_dir, &args);
@@ environment.c: void setup_git_env(const char *git_dir)

## object-file.c ##
-@@ object-file.c: static void write_object_file_prepare(const struct git_hash_algo *algo,
- }
-
- /*
-- * Move the just written object into its final resting place.
-+ * Move the just written object into its final resting place,
-+ * unless it is already there, as indicated by an empty string for
-+ * tmpfile.
- */
- int finalize_object_file(const char *tmpfile, const char *filename)
- {
- int ret = 0;
-
-+ if (!*tmpfile)
-+ goto out;
+@@ object-file.c: struct object_directory *set_temporary_primary_odb(const char *dir, int will_des
+ */
+ new_odb = xcalloc(1, sizeof(*new_odb));
+ new_odb->path = xstrdup(dir);
+
- if (object_creation_mode == OBJECT_CREATION_USES_RENAMES)
- goto try_rename;
- else if (link(tmpfile, filename))
-@@ object-file.c: static inline int directory_size(const char *filename)
- }
-
- /*
-- * This creates a temporary file in the same directory as the final
-- * 'filename'
-+ * This creates a loose object file for the specified object id.
-+ * If we're working in a temporary object directory, the file is
-+ * created with its final filename, otherwise it is created with
-+ * a temporary name and renamed by finalize_object_file.
-+ * If no rename is required, an empty string is returned in tmp.
- *
- * We want to avoid cross-directory filename renames, because those
- * can have problems on various filesystems (FAT, NFS, Coda).
- */
--static int create_tmpfile(struct strbuf *tmp, const char *filename)
-+static int create_objfile(const struct object_id *oid, struct strbuf *tmp,
-+ struct strbuf *filename)
- {
-- int fd, dirlen = directory_size(filename);
-+ int fd, dirlen, is_retrying = 0;
-+ const char *object_name;
-+ static const int object_mode = 0444;
-
-+ loose_object_path(the_repository, filename, oid);
-+ dirlen = directory_size(filename->buf);
-+
-+retry_create:
- strbuf_reset(tmp);
-- strbuf_add(tmp, filename, dirlen);
-- strbuf_addstr(tmp, "tmp_obj_XXXXXX");
-- fd = git_mkstemp_mode(tmp->buf, 0444);
-- if (fd < 0 && dirlen && errno == ENOENT) {
-+ if (!the_repository->objects->odb->is_temp) {
-+ strbuf_add(tmp, filename->buf, dirlen);
-+ object_name = "tmp_obj_XXXXXX";
-+ strbuf_addstr(tmp, object_name);
-+ fd = git_mkstemp_mode(tmp->buf, object_mode);
-+ } else {
-+ fd = open(filename->buf, O_CREAT | O_EXCL | O_RDWR, object_mode);
-+ }
-+
-+ if (fd < 0 && dirlen && errno == ENOENT && !is_retrying) {
- /*
- * Make sure the directory exists; note that the contents
- * of the buffer are undefined after mkstemp returns an
-@@ object-file.c: static int create_tmpfile(struct strbuf *tmp, const char *filename)
- * scratch.
- */
- strbuf_reset(tmp);
-- strbuf_add(tmp, filename, dirlen - 1);
-+ strbuf_add(tmp, filename->buf, dirlen - 1);
- if (mkdir(tmp->buf, 0777) && errno != EEXIST)
- return -1;
- if (adjust_shared_perm(tmp->buf))
- return -1;
-
- /* Try again */
-- strbuf_addstr(tmp, "/tmp_obj_XXXXXX");
-- fd = git_mkstemp_mode(tmp->buf, 0444);
-+ is_retrying = 1;
-+ goto retry_create;
- }
- return fd;
- }
-@@ object-file.c: static int write_loose_object(const struct object_id *oid, char *hdr,
- static struct strbuf tmp_file = STRBUF_INIT;
- static struct strbuf filename = STRBUF_INIT;
-
-- loose_object_path(the_repository, &filename, oid);
--
-- fd = create_tmpfile(&tmp_file, filename.buf);
-+ fd = create_objfile(oid, &tmp_file, &filename);
- if (fd < 0) {
- if (errno == EACCES)
- return error(_("insufficient permission for adding an object to repository database %s"), get_object_directory());
- else
-- return error_errno(_("unable to create temporary file"));
-+ return error_errno(_("unable to create object file"));
- }
-
- /* Set it up */
++ /*
++ * Disable ref updates while a temporary odb is active, since
++ * the objects in the database may roll back.
++ */
++ new_odb->disable_ref_updates = 1;
+ new_odb->will_destroy = will_destroy;
+ new_odb->next = the_repository->objects->odb;
+ the_repository->objects->odb = new_odb;

## object-store.h ##
@@ object-store.h: struct object_directory {
@@ object-store.h: struct object_directory {
struct oidtree *loose_objects_cache;

+ /*
-+ * This is a temporary object store, so there is no need to
-+ * create new objects via rename.
++ * This is a temporary object store created by the tmp_objdir
++ * facility. Disable ref updates since the objects in the store
++ * might be discarded on rollback.
+ */
-+ int is_temp;
++ unsigned int disable_ref_updates : 1;
+
+ /*
+ * This object store is ephemeral, so there is no need to fsync.
+ */
+- int will_destroy;
++ unsigned int will_destroy : 1;
+
/*
* Path to the alternative object store. If this is a relative path,
- * it is relative to the current working directory.
+
+ ## refs.c ##
+@@ refs.c: int ref_transaction_prepare(struct ref_transaction *transaction,
+ break;
+ }
+
+- if (getenv(GIT_QUARANTINE_ENVIRONMENT)) {
++ if (the_repository->objects->odb->disable_ref_updates) {
+ strbuf_addstr(err,
+ _("ref updates forbidden inside quarantine environment"));
+ return -1;

## repository.c ##
@@ repository.c: void repo_set_gitdir(struct repository *repo,
expand_base_dir(&repo->objects->odb->path, o->object_dir,
repo->commondir, "objects");

-+ repo->objects->odb->is_temp = o->object_dir_is_temp;
++ repo->objects->odb->disable_ref_updates = o->disable_ref_updates;
+
free(repo->objects->alternate_db); repo->objects->alternate_db = xstrdup_or_null(o->alternate_db);
@@ repository.h: struct set_gitdir_args {
const char *graft_file;
const char *index_file;
const char *alternate_db;
-+ int object_dir_is_temp;
++ int disable_ref_updates;
};

void repo_set_gitdir(struct repository *repo, const char *root,

  3:  c272f8776fa =  3:  9335646ed91 bulk-checkin: rename 'state' variable and separate 'plugged' boolean
  4:  55556bb3e90 !  4:  b9d3d874432 core.fsyncobjectfiles: batched disk flushes
     @@ bulk-checkin.c: static int deflate_to_pack(struct bulk_checkin_state *state,
      +	 */
      +	if (bulk_checkin_plugged &&
      +	    git_fsync(fd, FSYNC_WRITEOUT_ONLY) >= 0) {
     -+		assert(the_repository->objects->odb->is_temp);
      +		if (!needs_batch_fsync)
      +			needs_batch_fsync = 1;
      +	} else {
     @@ bulk-checkin.c: int index_bulk_checkin(struct object_id *oid,
       	assert(!bulk_checkin_plugged);
      +
      +	/*
     -+	 * Create a temporary object directory if the current
     -+	 * object directory is not already temporary.
     ++	 * A temporary object directory is used to hold the files
     ++	 * while they are not fsynced.
      +	 */
     -+	if (fsync_object_files == FSYNC_OBJECT_FILES_BATCH &&
     -+	    !the_repository->objects->odb->is_temp) {
     ++	if (fsync_object_files == FSYNC_OBJECT_FILES_BATCH) {
      +		bulk_fsync_objdir = tmp_objdir_create("bulk-fsync");
      +		if (!bulk_fsync_objdir)
      +			die(_("Could not create temporary object directory for core.fsyncobjectfiles=batch"));
     @@ object-file.c: int hash_object_file(const struct git_hash_algo *algo, const void
       
       	if (close(fd) != 0)
      
     - ## tmp-objdir.c ##
     -@@ tmp-objdir.c: int tmp_objdir_migrate(struct tmp_objdir *t)
     - 	if (!t)
     - 		return 0;
     - 
     --
     --
     - 	if (t->prev_odb) {
     - 		if (the_repository->objects->odb->will_destroy)
     - 			BUG("migrating and ODB that was marked for destruction");
     -
       ## wrapper.c ##
      @@ wrapper.c: int xmkstemp_mode(char *filename_template, int mode)
       	return fd;
  5:  6c33e79d6f0 =  5:  8df32eaaa9a core.fsyncobjectfiles: add windows support for batch mode
  6:  09dbff1004e =  6:  15767270984 update-index: use the bulk-checkin infrastructure
  7:  1eced9f9f9a =  7:  e88bab809a2 unpack-objects: use the bulk-checkin infrastructure
  8:  7aaa08d5f5f =  8:  811d6d31509 core.fsyncobjectfiles: tests for batch mode
  9:  ff286fb461a =  9:  f4fa20f591e core.fsyncobjectfiles: performance tests for add and stash
Signature · 1 line
gitgitgadget
#124 lore raw

PATCH v8 1/9 tmp-objdir: new API for creating temporary writable databases

From: Neeraj Singh <[email protected]>

The tmp_objdir API provides the ability to create temporary object directories, but was designed with the goal of having subprocesses access these object stores, followed by the main process migrating objects from it to the main object store or just deleting it. The subprocesses would view it as their primary datastore and write to it.

Here we add the tmp_objdir_replace_primary_odb function that replaces the current process's writable "main" object directory with the specified one. The previous main object directory is restored in either tmp_objdir_migrate or tmp_objdir_destroy.

For the --remerge-diff usecase, add a new will_destroy flag in struct object_database to mark ephemeral object databases that do not require fsync durability.

Add 'git prune' support for removing temporary object databases, and make sure that they have a name starting with tmp_ and containing an operation-specific name.

---
 builtin/prune.c        | 22 +++++++++++++++++----
 builtin/receive-pack.c |  2 +-
 object-file.c          | 44 ++++++++++++++++++++++++++++++++++++++++--
 object-store.h         | 19 ++++++++++++++++++
 object.c               |  2 +-
 tmp-objdir.c           | 30 +++++++++++++++++++++++++---
 tmp-objdir.h           | 14 +++++++++++---
 7 files changed, 119 insertions(+), 14 deletions(-)
builtin/prune.c +18 -4
index 02c6ab7cbaa..9c72ecf5a58 100644
@@ -18,6 +18,7 @@ static int show_only;
 static int verbose;
 static timestamp_t expire;
 static int show_progress = -1;
+static struct strbuf remove_dir_buf = STRBUF_INIT;
 
 static int prune_tmp_file(const char *fullpath)
 {
@@ -26,10 +27,19 @@ static int prune_tmp_file(const char *fullpath)
 		return error("Could not stat '%s'", fullpath);
 	if (st.st_mtime > expire)
 		return 0;
-	if (show_only || verbose)
-		printf("Removing stale temporary file %s\n", fullpath);
-	if (!show_only)
-		unlink_or_warn(fullpath);
+	if (S_ISDIR(st.st_mode)) {
+		if (show_only || verbose)
+			printf("Removing stale temporary directory %s\n", fullpath);
+		if (!show_only) {
+			strbuf_addstr(&remove_dir_buf, fullpath);
+			remove_dir_recursively(&remove_dir_buf, 0);
+		}
+	} else {
+		if (show_only || verbose)
+			printf("Removing stale temporary file %s\n", fullpath);
+		if (!show_only)
+			unlink_or_warn(fullpath);
+	}
 	return 0;
 }
 
@@ -97,6 +107,9 @@ static int prune_cruft(const char *basename, const char *path, void *data)
 
 static int prune_subdir(unsigned int nr, const char *path, void *data)
 {
+	if (verbose)
+		printf("Removing directory %s\n", path);
+
 	if (!show_only)
 		rmdir(path);
 	return 0;
@@ -185,5 +198,6 @@ int cmd_prune(int argc, const char **argv, const char *prefix)
 		prune_shallow(show_only ? PRUNE_SHOW_ONLY : 0);
 	}
 
+	strbuf_release(&remove_dir_buf);
 	return 0;
 }
builtin/receive-pack.c +1 -1
index 48960a9575b..418a42ca069 100644
@@ -2208,7 +2208,7 @@ static const char *unpack(int err_fd, struct shallow_info *si)
 		strvec_push(&child.args, alt_shallow_file);
 	}
 
-	tmp_objdir = tmp_objdir_create();
+	tmp_objdir = tmp_objdir_create("incoming");
 	if (!tmp_objdir) {
 		if (err_fd > 0)
 			close(err_fd);
object-file.c +42 -2

56 lines not on this page · raw message

object-store.h +19 -0
index c5130d8baea..74b1b5872a6 100644
@@ -27,6 +27,11 @@ struct object_directory {
 	uint32_t loose_objects_subdir_seen[8]; /* 256 bits */
 	struct oidtree *loose_objects_cache;
 
+	/*
+	 * This object store is ephemeral, so there is no need to fsync.
+	 */
+	int will_destroy;
+
 	/*
 	 * Path to the alternative object store. If this is a relative path,
 	 * it is relative to the current working directory.
@@ -58,6 +63,17 @@ void add_to_alternates_file(const char *dir);
  */
 void add_to_alternates_memory(const char *dir);
 
+/*
+ * Replace the current writable object directory with the specified temporary
+ * object directory; returns the former primary object directory.
+ */
+struct object_directory *set_temporary_primary_odb(const char *dir, int will_destroy);
+
+/*
+ * Restore a previous ODB replaced by set_temporary_main_odb.
+ */
+void restore_primary_odb(struct object_directory *restore_odb, const char *old_path);
+
 /*
  * Populate and return the loose object cache array corresponding to the
  * given object ID.
@@ -68,6 +84,9 @@ struct oidtree *odb_loose_cache(struct object_directory *odb,
 /* Empty the loose object cache for the specified object directory. */
 void odb_clear_loose_cache(struct object_directory *odb);
 
+/* Clear and free the specified object directory */
+void free_object_directory(struct object_directory *odb);
+
 struct packed_git {
 	struct hashmap_entry packmap_ent;
 	struct packed_git *next;
object.c +1 -1
index 4e85955a941..98635bc4043 100644
@@ -513,7 +513,7 @@ struct raw_object_store *raw_object_store_new(void)
 	return o;
 }
 
-static void free_object_directory(struct object_directory *odb)
+void free_object_directory(struct object_directory *odb)
 {
 	free(odb->path);
 	odb_clear_loose_cache(odb);
tmp-objdir.c +27 -3
index b8d880e3626..45d42a7bcf0 100644
@@ -11,6 +11,7 @@
 struct tmp_objdir {
 	struct strbuf path;
 	struct strvec env;
+	struct object_directory *prev_odb;
 };
 
 /*
@@ -38,6 +39,9 @@ static int tmp_objdir_destroy_1(struct tmp_objdir *t, int on_signal)
 	if (t == the_tmp_objdir)
 		the_tmp_objdir = NULL;
 
+	if (!on_signal && t->prev_odb)
+		restore_primary_odb(t->prev_odb, t->path.buf);
+
 	/*
 	 * This may use malloc via strbuf_grow(), but we should
 	 * have pre-grown t->path sufficiently so that this
@@ -52,6 +56,7 @@ static int tmp_objdir_destroy_1(struct tmp_objdir *t, int on_signal)
 	 */
 	if (!on_signal)
 		tmp_objdir_free(t);
+
 	return err;
 }
 
@@ -121,7 +126,7 @@ static int setup_tmp_objdir(const char *root)
 	return ret;
 }
 
-struct tmp_objdir *tmp_objdir_create(void)
+struct tmp_objdir *tmp_objdir_create(const char *prefix)
 {
 	static int installed_handlers;
 	struct tmp_objdir *t;
@@ -129,11 +134,16 @@ struct tmp_objdir *tmp_objdir_create(void)
 	if (the_tmp_objdir)
 		BUG("only one tmp_objdir can be used at a time");
 
-	t = xmalloc(sizeof(*t));
+	t = xcalloc(1, sizeof(*t));
 	strbuf_init(&t->path, 0);
 	strvec_init(&t->env);
 
-	strbuf_addf(&t->path, "%s/incoming-XXXXXX", get_object_directory());
+	/*
+	 * Use a string starting with tmp_ so that the builtin/prune.c code
+	 * can recognize any stale objdirs left behind by a crash and delete
+	 * them.
+	 */
+	strbuf_addf(&t->path, "%s/tmp_objdir-%s-XXXXXX", get_object_directory(), prefix);
 
 	/*
 	 * Grow the strbuf beyond any filename we expect to be placed in it.
@@ -269,6 +279,13 @@ int tmp_objdir_migrate(struct tmp_objdir *t)
 	if (!t)
 		return 0;
 
+	if (t->prev_odb) {
+		if (the_repository->objects->odb->will_destroy)
+			BUG("migrating an ODB that was marked for destruction");
+		restore_primary_odb(t->prev_odb, t->path.buf);
+		t->prev_odb = NULL;
+	}
+
 	strbuf_addbuf(&src, &t->path);
 	strbuf_addstr(&dst, get_object_directory());
 
@@ -292,3 +309,10 @@ void tmp_objdir_add_as_alternate(const struct tmp_objdir *t)
 {
 	add_to_alternates_memory(t->path.buf);
 }
+
+void tmp_objdir_replace_primary_odb(struct tmp_objdir *t, int will_destroy)
+{
+	if (t->prev_odb)
+		BUG("the primary object database is already replaced");
+	t->prev_odb = set_temporary_primary_odb(t->path.buf, will_destroy);
+}
tmp-objdir.h +11 -3
index b1e45b4c75d..75754cbfba6 100644
@@ -10,7 +10,7 @@
  *
  * Example:
  *
- *	struct tmp_objdir *t = tmp_objdir_create();
+ *	struct tmp_objdir *t = tmp_objdir_create("incoming");
  *	if (!run_command_v_opt_cd_env(cmd, 0, NULL, tmp_objdir_env(t)) &&
  *	    !tmp_objdir_migrate(t))
  *		printf("success!\n");
@@ -22,9 +22,10 @@
 struct tmp_objdir;
 
 /*
- * Create a new temporary object directory; returns NULL on failure.
+ * Create a new temporary object directory with the specified prefix;
+ * returns NULL on failure.
  */
-struct tmp_objdir *tmp_objdir_create(void);
+struct tmp_objdir *tmp_objdir_create(const char *prefix);
 
 /*
  * Return a list of environment strings, suitable for use with
@@ -51,4 +52,11 @@ int tmp_objdir_destroy(struct tmp_objdir *);
  */
 void tmp_objdir_add_as_alternate(const struct tmp_objdir *);
 
+/*
+ * Replaces the main object store in the current process with the temporary
+ * object directory and makes the former main object store an alternate.
+ * If will_destroy is nonzero, the object directory may not be migrated.
+ */
+void tmp_objdir_replace_primary_odb(struct tmp_objdir *, int will_destroy);
+
 #endif /* TMP_OBJDIR_H */
Signature · 1 line
gitgitgadget
#125 lore raw

PATCH v8 2/9 tmp-objdir: disable ref updates when replacing the primary odb

In reply to Neeraj K. Singh via GitGitGadget

From: Neeraj Singh <[email protected]>

When creating a subprocess with a temporary ODB, we set the GIT_QUARANTINE_ENVIRONMENT env var to tell child Git processes not to update refs, since the tmp-objdir may go away.

Introduce a similar mechanism for in-process temporary ODBs when we call tmp_objdir_replace_primary_odb. Now both mechanisms set the disable_ref_updates flag on the odb, which is queried by the ref_transaction_prepare function.

Note: This change adds an assumption that the state of the_repository is relevant for any ref transaction that might be initiated. Unwinding this assumption should be straightforward by saving the relevant repository to query in the transaction or the ref_store.

Peff's test case was invoking ref updates via the cachetextconv setting. That particular code silently does nothing when a ref update is forbidden. See the call to notes_cache_put in fill_textconv where errors are ignored.

---
 environment.c  | 4 ++++
 object-file.c  | 6 ++++++
 object-store.h | 9 ++++++++-
 refs.c         | 2 +-
 repository.c   | 2 ++
 repository.h   | 1 +
 6 files changed, 22 insertions(+), 2 deletions(-)
environment.c +4 -0
index b4ba4fa22db..46ec5072c05 100644
@@ -176,6 +176,10 @@ void setup_git_env(const char *git_dir)
 	args.graft_file = getenv_safe(&to_free, GRAFT_ENVIRONMENT);
 	args.index_file = getenv_safe(&to_free, INDEX_ENVIRONMENT);
 	args.alternate_db = getenv_safe(&to_free, ALTERNATE_DB_ENVIRONMENT);
+	if (getenv(GIT_QUARANTINE_ENVIRONMENT)) {
+		args.disable_ref_updates = 1;
+	}
+
 	repo_set_gitdir(the_repository, git_dir, &args);
 	strvec_clear(&to_free);
 
object-file.c +6 -0
index 990381abee5..f16441afb93 100644
@@ -767,6 +767,12 @@ struct object_directory *set_temporary_primary_odb(const char *dir, int will_des
 	 */
 	new_odb = xcalloc(1, sizeof(*new_odb));
 	new_odb->path = xstrdup(dir);
+
+	/*
+	 * Disable ref updates while a temporary odb is active, since
+	 * the objects in the database may roll back.
+	 */
+	new_odb->disable_ref_updates = 1;
 	new_odb->will_destroy = will_destroy;
 	new_odb->next = the_repository->objects->odb;
 	the_repository->objects->odb = new_odb;
object-store.h +8 -1
index 74b1b5872a6..bd53bdf2f2e 100644
@@ -27,10 +27,17 @@ struct object_directory {
 	uint32_t loose_objects_subdir_seen[8]; /* 256 bits */
 	struct oidtree *loose_objects_cache;
 
+	/*
+	 * This is a temporary object store created by the tmp_objdir
+	 * facility. Disable ref updates since the objects in the store
+	 * might be discarded on rollback.
+	 */
+	unsigned int disable_ref_updates : 1;
+
 	/*
 	 * This object store is ephemeral, so there is no need to fsync.
 	 */
-	int will_destroy;
+	unsigned int will_destroy : 1;
 
 	/*
 	 * Path to the alternative object store. If this is a relative path,
refs.c +1 -1
index 8b9f7c3a80a..7c182607dcf 100644
@@ -2126,7 +2126,7 @@ int ref_transaction_prepare(struct ref_transaction *transaction,
 		break;
 	}
 
-	if (getenv(GIT_QUARANTINE_ENVIRONMENT)) {
+	if (the_repository->objects->odb->disable_ref_updates) {
 		strbuf_addstr(err,
 			      _("ref updates forbidden inside quarantine environment"));
 		return -1;
repository.c +2 -0
index 710a3b4bf87..18e0526da01 100644
@@ -80,6 +80,8 @@ void repo_set_gitdir(struct repository *repo,
 	expand_base_dir(&repo->objects->odb->path, o->object_dir,
 			repo->commondir, "objects");
 
+	repo->objects->odb->disable_ref_updates = o->disable_ref_updates;
+
 	free(repo->objects->alternate_db);
 	repo->objects->alternate_db = xstrdup_or_null(o->alternate_db);
 	expand_base_dir(&repo->graft_file, o->graft_file,
repository.h +1 -0
index 3740c93bc0f..77316367d99 100644
@@ -162,6 +162,7 @@ struct set_gitdir_args {
 	const char *graft_file;
 	const char *index_file;
 	const char *alternate_db;
+	int disable_ref_updates;
 };
 
 void repo_set_gitdir(struct repository *repo, const char *root,
Signature · 1 line
gitgitgadget
#126 lore raw

PATCH v8 3/9 bulk-checkin: rename 'state' variable and separate 'plugged' boolean

In reply to Neeraj K. Singh via GitGitGadget

From: Neeraj Singh <[email protected]>

Preparation for adding bulk-fsync to the bulk-checkin.c infrastructure.

* Rename 'state' variable to 'bulk_checkin_state', since we will later be adding 'bulk_fsync_state'. This also makes the variable easier to find in the debugger, since the name is more unique.

* Move the 'plugged' data member of 'bulk_checkin_state' into a separate static variable. Doing this avoids resetting the variable in finish_bulk_checkin when zeroing the 'bulk_checkin_state'. As-is, we seem to unintentionally disable the plugging functionality the first time a new packfile must be created due to packfile size limits. While disabling the plugging state only results in suboptimal behavior for the current code, it would be fatal for the bulk-fsync functionality later in this patch series.

---
 bulk-checkin.c | 22 ++++++++++++----------
 1 file changed, 12 insertions(+), 10 deletions(-)
bulk-checkin.c +12 -10
index 8785b2ac806..6ae18401e04 100644
@@ -10,9 +10,9 @@
 #include "packfile.h"
 #include "object-store.h"
 
-static struct bulk_checkin_state {
-	unsigned plugged:1;
+static int bulk_checkin_plugged;
 
+static struct bulk_checkin_state {
 	char *pack_tmp_name;
 	struct hashfile *f;
 	off_t offset;
@@ -21,7 +21,7 @@ static struct bulk_checkin_state {
 	struct pack_idx_entry **written;
 	uint32_t alloc_written;
 	uint32_t nr_written;
-} state;
+} bulk_checkin_state;
 
 static void finish_tmp_packfile(struct strbuf *basename,
 				const char *pack_tmp_name,
@@ -277,21 +277,23 @@ int index_bulk_checkin(struct object_id *oid,
 		       int fd, size_t size, enum object_type type,
 		       const char *path, unsigned flags)
 {
-	int status = deflate_to_pack(&state, oid, fd, size, type,
+	int status = deflate_to_pack(&bulk_checkin_state, oid, fd, size, type,
 				     path, flags);
-	if (!state.plugged)
-		finish_bulk_checkin(&state);
+	if (!bulk_checkin_plugged)
+		finish_bulk_checkin(&bulk_checkin_state);
 	return status;
 }
 
 void plug_bulk_checkin(void)
 {
-	state.plugged = 1;
+	assert(!bulk_checkin_plugged);
+	bulk_checkin_plugged = 1;
 }
 
 void unplug_bulk_checkin(void)
 {
-	state.plugged = 0;
-	if (state.f)
-		finish_bulk_checkin(&state);
+	assert(bulk_checkin_plugged);
+	bulk_checkin_plugged = 0;
+	if (bulk_checkin_state.f)
+		finish_bulk_checkin(&bulk_checkin_state);
 }
Signature · 1 line
gitgitgadget
#127 lore raw

PATCH v8 4/9 core.fsyncobjectfiles: batched disk flushes

In reply to Neeraj K. Singh via GitGitGadget

From: Neeraj Singh <[email protected]>

When adding many objects to a repo with core.fsyncObjectFiles set to true, the cost of fsync'ing each object file can become prohibitive.

One major source of the cost of fsync is the implied flush of the hardware writeback cache within the disk drive. Fortunately, Windows, and macOS offer mechanisms to write data from the filesystem page cache without initiating a hardware flush. Linux has the sync_file_range API, which issues a pagecache writeback request reliably after version 5.2.

This patch introduces a new 'core.fsyncObjectFiles = batch' option that batches up hardware flushes. It hooks into the bulk-checkin plugging and unplugging functionality and takes advantage of tmp-objdir.

When the new mode is enabled do the following for each new object:
1. Create the object in a tmp-objdir.
2. Issue a pagecache writeback request and wait for it to complete.

At the end of the entire transaction when unplugging bulk checkin:
1. Issue an fsync against a dummy file to flush the hardware writeback cache, which should by now have processed the tmp-objdir writes.
2. Rename all of the tmp-objdir files to their final names.
3. When updating the index and/or refs, we assume that Git will issue another fsync internal to that operation. This is not the case today, but may be a good extension to those components.

On a filesystem with a singular journal that is updated during name operations (e.g. create, link, rename, etc), such as NTFS, HFS+, or XFS we would expect the fsync to trigger a journal writeout so that this sequence is enough to ensure that the user's data is durable by the time the git command returns.

This change also updates the macOS code to trigger a real hardware flush via fnctl(fd, F_FULLFSYNC) when fsync_or_die is called. Previously, on macOS there was no guarantee of durability since a simple fsync(2) call does not flush any hardware caches.

_Performance numbers_:

Linux - Hyper-V VM running Kernel 5.11 (Ubuntu 20.04) on a fast SSD. Mac - macOS 11.5.1 running on a Mac mini on a 1TB Apple SSD. Windows - Same host as Linux, a preview version of Windows 11.
This number is from a patch later in the series.

Adding 500 files to the repo with 'git add' Times reported in seconds.

core.fsyncObjectFiles | Linux | Mac   | Windows
----------------------|-------|-------|--------
                false | 0.06  |  0.35 | 0.61
                true  | 1.88  | 11.18 | 2.47
                batch | 0.15  |  0.41 | 1.53
---
 Documentation/config/core.txt | 29 +++++++++++----
 Makefile                      |  6 ++++
 bulk-checkin.c                | 68 +++++++++++++++++++++++++++++++++++
 bulk-checkin.h                |  2 ++
 cache.h                       |  8 ++++-
 config.c                      |  7 +++-
 config.mak.uname              |  1 +
 configure.ac                  |  8 +++++
 environment.c                 |  2 +-
 git-compat-util.h             |  7 ++++
 object-file.c                 | 12 ++++++-
 wrapper.c                     | 44 +++++++++++++++++++++++
 write-or-die.c                |  2 +-
 13 files changed, 185 insertions(+), 11 deletions(-)
Documentation/config/core.txt +23 -6
index c04f62a54a1..200b4d9f06e 100644
@@ -548,12 +548,29 @@ core.whitespace::
   errors. The default tab width is 8. Allowed values are 1 to 63.
 
 core.fsyncObjectFiles::
-	This boolean will enable 'fsync()' when writing object files.
-+
-This is a total waste of time and effort on a filesystem that orders
-data writes properly, but can be useful for filesystems that do not use
-journalling (traditional UNIX filesystems) or that only journal metadata
-and not file contents (OS X's HFS+, or Linux ext3 with "data=writeback").
+	A value indicating the level of effort Git will expend in
+	trying to make objects added to the repo durable in the event
+	of an unclean system shutdown. This setting currently only
+	controls loose objects in the object store, so updates to any
+	refs or the index may not be equally durable.
++
+* `false` allows data to remain in file system caches according to
+  operating system policy, whence it may be lost if the system loses power
+  or crashes.
+* `true` triggers a data integrity flush for each loose object added to the
+  object store. This is the safest setting that is likely to ensure durability
+  across all operating systems and file systems that honor the 'fsync' system
+  call. However, this setting comes with a significant performance cost on
+  common hardware. Git does not currently fsync parent directories for
+  newly-added files, so some filesystems may still allow data to be lost on
+  system crash.
+* `batch` enables an experimental mode that uses interfaces available in some
+  operating systems to write loose object data with a minimal set of FLUSH
+  CACHE (or equivalent) commands sent to the storage controller. If the
+  operating system interfaces are not available, this mode behaves the same as
+  `true`. This mode is expected to be as safe as `true` on macOS for repos
+  stored on HFS+ or APFS filesystems and on Windows for repos stored on NTFS or
+  ReFS.
 
 core.preloadIndex::
 	Enable parallel index preload for operations like 'git diff'
Makefile +6 -0
index a9f9b689f0c..313b3dc7cd6 100644
@@ -406,6 +406,8 @@ all::
 #
 # Define HAVE_CLOCK_MONOTONIC if your platform has CLOCK_MONOTONIC.
 #
+# Define HAVE_SYNC_FILE_RANGE if your platform has sync_file_range.
+#
 # Define NEEDS_LIBRT if your platform requires linking with librt (glibc version
 # before 2.17) for clock_gettime and CLOCK_MONOTONIC.
 #
@@ -1874,6 +1876,10 @@ ifdef HAVE_CLOCK_MONOTONIC
 	BASIC_CFLAGS += -DHAVE_CLOCK_MONOTONIC
 endif
 
+ifdef HAVE_SYNC_FILE_RANGE
+	BASIC_CFLAGS += -DHAVE_SYNC_FILE_RANGE
+endif
+
 ifdef NEEDS_LIBRT
 	EXTLIBS += -lrt
 endif
bulk-checkin.c +68 -0

104 lines not on this page · raw message

bulk-checkin.h +2 -0
index b26f3dc3b74..08f292379b6 100644
@@ -6,6 +6,8 @@
 
 #include "cache.h"
 
+void fsync_loose_object_bulk_checkin(int fd);
+
 int index_bulk_checkin(struct object_id *oid,
 		       int fd, size_t size, enum object_type type,
 		       const char *path, unsigned flags);
cache.h +7 -1
index f6295f3b048..1ed8137b5e6 100644
@@ -984,7 +984,13 @@ void reset_shared_repository(void);
 extern int read_replace_refs;
 extern char *git_replace_ref_base;
 
-extern int fsync_object_files;
+enum fsync_object_files_mode {
+    FSYNC_OBJECT_FILES_OFF,
+    FSYNC_OBJECT_FILES_ON,
+    FSYNC_OBJECT_FILES_BATCH
+};
+
+extern enum fsync_object_files_mode fsync_object_files;
 extern int core_preload_index;
 extern int precomposed_unicode;
 extern int protect_hfs;
config.c +6 -1
index 2edf835262f..8315d020eeb 100644
@@ -1506,7 +1506,12 @@ static int git_default_core_config(const char *var, const char *value, void *cb)
 	}
 
 	if (!strcmp(var, "core.fsyncobjectfiles")) {
-		fsync_object_files = git_config_bool(var, value);
+		if (value && !strcmp(value, "batch"))
+			fsync_object_files = FSYNC_OBJECT_FILES_BATCH;
+		else if (git_config_bool(var, value))
+			fsync_object_files = FSYNC_OBJECT_FILES_ON;
+		else
+			fsync_object_files = FSYNC_OBJECT_FILES_OFF;
 		return 0;
 	}
 
config.mak.uname +1 -0
index 76516aaa9a5..e6d482fbcc6 100644
@@ -53,6 +53,7 @@ ifeq ($(uname_S),Linux)
 	HAVE_CLOCK_MONOTONIC = YesPlease
 	# -lrt is needed for clock_gettime on glibc <= 2.16
 	NEEDS_LIBRT = YesPlease
+	HAVE_SYNC_FILE_RANGE = YesPlease
 	HAVE_GETDELIM = YesPlease
 	SANE_TEXT_GREP=-a
 	FREAD_READS_DIRECTORIES = UnfortunatelyYes
configure.ac +8 -0
index 031e8d3fee8..c711037d625 100644
@@ -1090,6 +1090,14 @@ AC_COMPILE_IFELSE([CLOCK_MONOTONIC_SRC],
 	[AC_MSG_RESULT([no])
 	HAVE_CLOCK_MONOTONIC=])
 GIT_CONF_SUBST([HAVE_CLOCK_MONOTONIC])
+
+#
+# Define HAVE_SYNC_FILE_RANGE=YesPlease if sync_file_range is available.
+GIT_CHECK_FUNC(sync_file_range,
+	[HAVE_SYNC_FILE_RANGE=YesPlease],
+	[HAVE_SYNC_FILE_RANGE])
+GIT_CONF_SUBST([HAVE_SYNC_FILE_RANGE])
+
 #
 # Define NO_SETITIMER if you don't have setitimer.
 GIT_CHECK_FUNC(setitimer,
environment.c +1 -1
index 46ec5072c05..0ace9fa2167 100644
@@ -42,7 +42,7 @@ const char *git_attributes_file;
 const char *git_hooks_path;
 int zlib_compression_level = Z_BEST_SPEED;
 int pack_compression_level = Z_DEFAULT_COMPRESSION;
-int fsync_object_files;
+enum fsync_object_files_mode fsync_object_files;
 size_t packed_git_window_size = DEFAULT_PACKED_GIT_WINDOW_SIZE;
 size_t packed_git_limit = DEFAULT_PACKED_GIT_LIMIT;
 size_t delta_base_cache_limit = 96 * 1024 * 1024;
git-compat-util.h +7 -0
index 7c99eef6612..9daee873782 100644
@@ -1213,6 +1213,13 @@ __attribute__((format (printf, 1, 2))) NORETURN
 void BUG(const char *fmt, ...);
 #endif
 
+enum fsync_action {
+    FSYNC_WRITEOUT_ONLY,
+    FSYNC_HARDWARE_FLUSH
+};
+
+int git_fsync(int fd, enum fsync_action action);
+
 /*
  * Preserves errno, prints a message, but gives no warning for ENOENT.
  * Returns 0 on success, which includes trying to unlink an object that does
object-file.c +11 -1
index f16441afb93..b035d88f309 100644
@@ -1932,8 +1932,18 @@ int hash_object_file(const struct git_hash_algo *algo, const void *buf,
 static void close_loose_object(int fd)
 {
 	if (!the_repository->objects->odb->will_destroy) {
-		if (fsync_object_files)
+		switch (fsync_object_files) {
+		case FSYNC_OBJECT_FILES_OFF:
+			break;
+		case FSYNC_OBJECT_FILES_ON:
 			fsync_or_die(fd, "loose object file");
+			break;
+		case FSYNC_OBJECT_FILES_BATCH:
+			fsync_loose_object_bulk_checkin(fd);
+			break;
+		default:
+			BUG("Invalid fsync_object_files mode.");
+		}
 	}
 
 	if (close(fd) != 0)
wrapper.c +44 -0

50 lines not on this page · raw message

write-or-die.c +1 -1
index 0b1ec8190b6..cc8291d9794 100644
@@ -57,7 +57,7 @@ void fprintf_or_die(FILE *f, const char *fmt, ...)
 
 void fsync_or_die(int fd, const char *msg)
 {
-	while (fsync(fd) < 0) {
+	while (git_fsync(fd, FSYNC_HARDWARE_FLUSH) < 0) {
 		if (errno != EINTR)
 			die_errno("fsync error on '%s'", msg);
 	}
Signature · 1 line
gitgitgadget
#128 lore raw

PATCH v8 5/9 core.fsyncobjectfiles: add windows support for batch mode

In reply to Neeraj K. Singh via GitGitGadget

From: Neeraj Singh <[email protected]>

This commit adds a win32 implementation for fsync_no_flush that is called git_fsync. The 'NtFlushBuffersFileEx' function being called is available since Windows 8. If the function is not available, we return -1 and Git falls back to doing a full fsync.

The operating system is told to flush data only without a hardware flush primitive. A later full fsync will cause the metadata log to be flushed and then the disk cache to be flushed on NTFS and ReFS. Other filesystems will treat this as a full flush operation.

I added a new file here for this system call so as not to conflict with downstream changes in the git-for-windows repository related to fscache.

---
 compat/mingw.h                      |  3 +++
 compat/win32/flush.c                | 28 ++++++++++++++++++++++++++++
 config.mak.uname                    |  2 ++
 contrib/buildsystems/CMakeLists.txt |  3 ++-
 wrapper.c                           |  4 ++++
 5 files changed, 39 insertions(+), 1 deletion(-)
 create mode 100644 compat/win32/flush.c
compat/mingw.h +3 -0
index c9a52ad64a6..6074a3d3ced 100644
@@ -329,6 +329,9 @@ int mingw_getpagesize(void);
 #define getpagesize mingw_getpagesize
 #endif
 
+int win32_fsync_no_flush(int fd);
+#define fsync_no_flush win32_fsync_no_flush
+
 struct rlimit {
 	unsigned int rlim_cur;
 };
compat/win32/flush.c new file +28 -0
new file mode 100644
index 00000000000..75324c24ee7
@@ -0,0 +1,28 @@
+#include "../../git-compat-util.h"
+#include <winternl.h>
+#include "lazyload.h"
+
+int win32_fsync_no_flush(int fd)
+{
+       IO_STATUS_BLOCK io_status;
+
+#define FLUSH_FLAGS_FILE_DATA_ONLY 1
+
+       DECLARE_PROC_ADDR(ntdll.dll, NTSTATUS, NtFlushBuffersFileEx,
+			 HANDLE FileHandle, ULONG Flags, PVOID Parameters, ULONG ParameterSize,
+			 PIO_STATUS_BLOCK IoStatusBlock);
+
+       if (!INIT_PROC_ADDR(NtFlushBuffersFileEx)) {
+		errno = ENOSYS;
+		return -1;
+       }
+
+       memset(&io_status, 0, sizeof(io_status));
+       if (NtFlushBuffersFileEx((HANDLE)_get_osfhandle(fd), FLUSH_FLAGS_FILE_DATA_ONLY,
+				NULL, 0, &io_status)) {
+		errno = EINVAL;
+		return -1;
+       }
+
+       return 0;
+}
config.mak.uname +2 -0
index e6d482fbcc6..34c93314a50 100644
@@ -451,6 +451,7 @@ endif
 	CFLAGS =
 	BASIC_CFLAGS = -nologo -I. -Icompat/vcbuild/include -DWIN32 -D_CONSOLE -DHAVE_STRING_H -D_CRT_SECURE_NO_WARNINGS -D_CRT_NONSTDC_NO_DEPRECATE
 	COMPAT_OBJS = compat/msvc.o compat/winansi.o \
+		compat/win32/flush.o \
 		compat/win32/path-utils.o \
 		compat/win32/pthread.o compat/win32/syslog.o \
 		compat/win32/trace2_win32_process_info.o \
@@ -626,6 +627,7 @@ ifneq (,$(findstring MINGW,$(uname_S)))
 	COMPAT_CFLAGS += -DSTRIP_EXTENSION=\".exe\"
 	COMPAT_OBJS += compat/mingw.o compat/winansi.o \
 		compat/win32/trace2_win32_process_info.o \
+		compat/win32/flush.o \
 		compat/win32/path-utils.o \
 		compat/win32/pthread.o compat/win32/syslog.o \
 		compat/win32/dirent.o
contrib/buildsystems/CMakeLists.txt +2 -1
index 171b4124afe..b573a5ee122 100644
@@ -261,7 +261,8 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
 				NOGDI OBJECT_CREATION_MODE=1 __USE_MINGW_ANSI_STDIO=0
 				USE_NED_ALLOCATOR OVERRIDE_STRDUP MMAP_PREVENTS_DELETE USE_WIN32_MMAP
 				UNICODE _UNICODE HAVE_WPGMPTR ENSURE_MSYSTEM_IS_SET)
-	list(APPEND compat_SOURCES compat/mingw.c compat/winansi.c compat/win32/path-utils.c
+	list(APPEND compat_SOURCES compat/mingw.c compat/winansi.c
+		compat/win32/flush.c compat/win32/path-utils.c
 		compat/win32/pthread.c compat/win32mmap.c compat/win32/syslog.c
 		compat/win32/trace2_win32_process_info.c compat/win32/dirent.c
 		compat/nedmalloc/nedmalloc.c compat/strdup.c)
wrapper.c +4 -0
index bb4f9f043ce..1a1e2fba9c9 100644
@@ -567,6 +567,10 @@ int git_fsync(int fd, enum fsync_action action)
 						 SYNC_FILE_RANGE_WAIT_AFTER);
 #endif
 
+#ifdef fsync_no_flush
+		return fsync_no_flush(fd);
+#endif
+
 		errno = ENOSYS;
 		return -1;
 
Signature · 1 line
gitgitgadget
#129 lore raw

PATCH v8 6/9 update-index: use the bulk-checkin infrastructure

In reply to Neeraj K. Singh via GitGitGadget

From: Neeraj Singh <[email protected]>

The update-index functionality is used internally by 'git stash push' to setup the internal stashed commit.

This change enables bulk-checkin for update-index infrastructure to speed up adding new objects to the object database by leveraging the pack functionality and the new bulk-fsync functionality.

There is some risk with this change, since under batch fsync, the object files will not be available until the update-index is entirely complete. This usage is unlikely, since any tool invoking update-index and expecting to see objects would have to synchronize with the update-index process after passing it a file path.

---
 builtin/update-index.c | 6 ++++++
 1 file changed, 6 insertions(+)
builtin/update-index.c +6 -0
index 187203e8bb5..dc7368bb1ee 100644
@@ -5,6 +5,7 @@
  */
 #define USE_THE_INDEX_COMPATIBILITY_MACROS
 #include "cache.h"
+#include "bulk-checkin.h"
 #include "config.h"
 #include "lockfile.h"
 #include "quote.h"
@@ -1088,6 +1089,9 @@ int cmd_update_index(int argc, const char **argv, const char *prefix)
 
 	the_index.updated_skipworktree = 1;
 
+	/* we might be adding many objects to the object database */
+	plug_bulk_checkin();
+
 	/*
 	 * Custom copy of parse_options() because we want to handle
 	 * filename arguments as they come.
@@ -1168,6 +1172,8 @@ int cmd_update_index(int argc, const char **argv, const char *prefix)
 		strbuf_release(&buf);
 	}
 
+	/* by now we must have added all of the new objects */
+	unplug_bulk_checkin();
 	if (split_index > 0) {
 		if (git_config_get_split_index() == 0)
 			warning(_("core.splitIndex is set to false; "
Signature · 1 line
gitgitgadget
#130 lore raw

PATCH v8 7/9 unpack-objects: use the bulk-checkin infrastructure

In reply to Neeraj K. Singh via GitGitGadget

From: Neeraj Singh <[email protected]>

The unpack-objects functionality is used by fetch, push, and fast-import to turn the transfered data into object database entries when there are fewer objects than the 'unpacklimit' setting.

By enabling bulk-checkin when unpacking objects, we can take advantage of batched fsyncs.

---
 builtin/unpack-objects.c | 3 +++
 1 file changed, 3 insertions(+)
builtin/unpack-objects.c +3 -0
index 4a9466295ba..51eb4f7b531 100644
@@ -1,5 +1,6 @@
 #include "builtin.h"
 #include "cache.h"
+#include "bulk-checkin.h"
 #include "config.h"
 #include "object-store.h"
 #include "object.h"
@@ -503,10 +504,12 @@ static void unpack_all(void)
 	if (!quiet)
 		progress = start_progress(_("Unpacking objects"), nr_objects);
 	CALLOC_ARRAY(obj_list, nr_objects);
+	plug_bulk_checkin();
 	for (i = 0; i < nr_objects; i++) {
 		unpack_one(i);
 		display_progress(progress, i + 1);
 	}
+	unplug_bulk_checkin();
 	stop_progress(&progress);
 
 	if (delta_list)
Signature · 1 line
gitgitgadget
#131 lore raw

PATCH v8 8/9 core.fsyncobjectfiles: tests for batch mode

In reply to Neeraj K. Singh via GitGitGadget

From: Neeraj Singh <[email protected]>

Add test cases to exercise batch mode for:
* 'git add'
* 'git stash'
* 'git update-index'
* 'git unpack-objects'

These tests ensure that the added data winds up in the object database.

In this change we introduce a new test helper lib-unique-files.sh. The goal of this library is to create a tree of files that have different oids from any other files that may have been created in the current test repo. This helps us avoid missing validation of an object being added due to it already being in the repo.

---
 t/lib-unique-files.sh  | 36 ++++++++++++++++++++++++++++++++++++
 t/t3700-add.sh         | 20 ++++++++++++++++++++
 t/t3903-stash.sh       | 14 ++++++++++++++
 t/t5300-pack-object.sh | 30 +++++++++++++++++++-----------
 4 files changed, 89 insertions(+), 11 deletions(-)
 create mode 100644 t/lib-unique-files.sh
t/lib-unique-files.sh new file +36 -0
new file mode 100644
index 00000000000..a7de4ca8512
@@ -0,0 +1,36 @@
+# Helper to create files with unique contents
+
+
+# Create multiple files with unique contents. Takes the number of
+# directories, the number of files in each directory, and the base
+# directory.
+#
+# test_create_unique_files 2 3 my_dir -- Creates 2 directories with 3 files
+#					 each in my_dir, all with unique
+#					 contents.
+
+test_create_unique_files() {
+	test "$#" -ne 3 && BUG "3 param"
+
+	local dirs=$1
+	local files=$2
+	local basedir=$3
+	local counter=0
+	test_tick
+	local basedata=$test_tick
+
+
+	rm -rf $basedir
+
+	for i in $(test_seq $dirs)
+	do
+		local dir=$basedir/dir$i
+
+		mkdir -p "$dir"
+		for j in $(test_seq $files)
+		do
+			counter=$((counter + 1))
+			echo "$basedata.$counter"  >"$dir/file$j.txt"
+		done
+	done
+}
t/t3700-add.sh +20 -0
index 4086e1ebbc9..36049a53ff7 100755
@@ -7,6 +7,8 @@ test_description='Test of git add, including the -- option.'
 
 . ./test-lib.sh
 
+. $TEST_DIRECTORY/lib-unique-files.sh
+
 # Test the file mode "$1" of the file "$2" in the index.
 test_mode_in_index () {
 	case "$(git ls-files -s "$2")" in
@@ -33,6 +35,24 @@ test_expect_success \
     'Test that "git add -- -q" works' \
     'touch -- -q && git add -- -q'
 
+test_expect_success 'git add: core.fsyncobjectfiles=batch' "
+	test_create_unique_files 2 4 fsync-files &&
+	git -c core.fsyncobjectfiles=batch add -- ./fsync-files/ &&
+	rm -f fsynced_files &&
+	git ls-files --stage fsync-files/ > fsynced_files &&
+	test_line_count = 8 fsynced_files &&
+	awk -- '{print \$2}' fsynced_files | xargs -n1 git cat-file -e
+"
+
+test_expect_success 'git update-index: core.fsyncobjectfiles=batch' "
+	test_create_unique_files 2 4 fsync-files2 &&
+	find fsync-files2 ! -type d -print | xargs git -c core.fsyncobjectfiles=batch update-index --add -- &&
+	rm -f fsynced_files2 &&
+	git ls-files --stage fsync-files2/ > fsynced_files2 &&
+	test_line_count = 8 fsynced_files2 &&
+	awk -- '{print \$2}' fsynced_files2 | xargs -n1 git cat-file -e
+"
+
 test_expect_success \
 	'git add: Test that executable bit is not used if core.filemode=0' \
 	'git config core.filemode 0 &&
t/t3903-stash.sh +14 -0
index 873aa56e359..2fc819e5584 100755
@@ -9,6 +9,7 @@ GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
 export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
 
 . ./test-lib.sh
+. $TEST_DIRECTORY/lib-unique-files.sh
 
 diff_cmp () {
 	for i in "$1" "$2"
@@ -1293,6 +1294,19 @@ test_expect_success 'stash handles skip-worktree entries nicely' '
 	git rev-parse --verify refs/stash:A.t
 '
 
+test_expect_success 'stash with core.fsyncobjectfiles=batch' "
+	test_create_unique_files 2 4 fsync-files &&
+	git -c core.fsyncobjectfiles=batch stash push -u -- ./fsync-files/ &&
+	rm -f fsynced_files &&
+
+	# The files were untracked, so use the third parent,
+	# which contains the untracked files
+	git ls-tree -r stash^3 -- ./fsync-files/ > fsynced_files &&
+	test_line_count = 8 fsynced_files &&
+	awk -- '{print \$3}' fsynced_files | xargs -n1 git cat-file -e
+"
+
+
 test_expect_success 'stash -c stash.useBuiltin=false warning ' '
 	expected="stash.useBuiltin support has been removed" &&
 
t/t5300-pack-object.sh +19 -11
index e13a8842075..38663dc1393 100755
@@ -162,23 +162,23 @@ test_expect_success 'pack-objects with bogus arguments' '
 
 check_unpack () {
 	test_when_finished "rm -rf git2" &&
-	git init --bare git2 &&
-	git -C git2 unpack-objects -n <"$1".pack &&
-	git -C git2 unpack-objects <"$1".pack &&
-	(cd .git && find objects -type f -print) |
-	while read path
-	do
-		cmp git2/$path .git/$path || {
-			echo $path differs.
-			return 1
-		}
-	done
+	git $2 init --bare git2 &&
+	(
+		git $2 -C git2 unpack-objects -n <"$1".pack &&
+		git $2 -C git2 unpack-objects <"$1".pack &&
+		git $2 -C git2 cat-file --batch-check="%(objectname)"
+	) <obj-list >current &&
+	cmp obj-list current
 }
 
 test_expect_success 'unpack without delta' '
 	check_unpack test-1-${packname_1}
 '
 
+test_expect_success 'unpack without delta (core.fsyncobjectfiles=batch)' '
+	check_unpack test-1-${packname_1} "-c core.fsyncobjectfiles=batch"
+'
+
 test_expect_success 'pack with REF_DELTA' '
 	packname_2=$(git pack-objects --progress test-2 <obj-list 2>stderr) &&
 	check_deltas stderr -gt 0
@@ -188,6 +188,10 @@ test_expect_success 'unpack with REF_DELTA' '
 	check_unpack test-2-${packname_2}
 '
 
+test_expect_success 'unpack with REF_DELTA (core.fsyncobjectfiles=batch)' '
+       check_unpack test-2-${packname_2} "-c core.fsyncobjectfiles=batch"
+'
+
 test_expect_success 'pack with OFS_DELTA' '
 	packname_3=$(git pack-objects --progress --delta-base-offset test-3 \
 			<obj-list 2>stderr) &&
@@ -198,6 +202,10 @@ test_expect_success 'unpack with OFS_DELTA' '
 	check_unpack test-3-${packname_3}
 '
 
+test_expect_success 'unpack with OFS_DELTA (core.fsyncobjectfiles=batch)' '
+       check_unpack test-3-${packname_3} "-c core.fsyncobjectfiles=batch"
+'
+
 test_expect_success 'compare delta flavors' '
 	perl -e '\''
 		defined($_ = -s $_) or die for @ARGV;
Signature · 1 line
gitgitgadget
#132 lore raw

PATCH v8 9/9 core.fsyncobjectfiles: performance tests for add and stash

In reply to Neeraj K. Singh via GitGitGadget

From: Neeraj Singh <[email protected]>

Add a basic performance test for "git add" and "git stash" of a lot of new objects with various fsync settings.

---
 t/perf/p3700-add.sh   | 43 ++++++++++++++++++++++++++++++++++++++++
 t/perf/p3900-stash.sh | 46 +++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 89 insertions(+)
 create mode 100755 t/perf/p3700-add.sh
 create mode 100755 t/perf/p3900-stash.sh
t/perf/p3700-add.sh new file +43 -0
new file mode 100755
index 00000000000..e93c08a2e70
@@ -0,0 +1,43 @@
+#!/bin/sh
+#
+# This test measures the performance of adding new files to the object database
+# and index. The test was originally added to measure the effect of the
+# core.fsyncObjectFiles=batch mode, which is why we are testing different values
+# of that setting explicitly and creating a lot of unique objects.
+
+test_description="Tests performance of add"
+
+. ./perf-lib.sh
+
+. $TEST_DIRECTORY/lib-unique-files.sh
+
+test_perf_default_repo
+test_checkout_worktree
+
+dir_count=10
+files_per_dir=50
+total_files=$((dir_count * files_per_dir))
+
+# We need to create the files each time we run the perf test, but
+# we do not want to measure the cost of creating the files, so run
+# the tet once.
+if test "${GIT_PERF_REPEAT_COUNT-1}" -ne 1
+then
+	echo "warning: Setting GIT_PERF_REPEAT_COUNT=1" >&2
+	GIT_PERF_REPEAT_COUNT=1
+fi
+
+for m in false true batch
+do
+	test_expect_success "create the files for core.fsyncObjectFiles=$m" '
+		git reset --hard &&
+		# create files across directories
+		test_create_unique_files $dir_count $files_per_dir files
+	'
+
+	test_perf "add $total_files files (core.fsyncObjectFiles=$m)" "
+		git -c core.fsyncobjectfiles=$m add files
+	"
+done
+
+test_done
t/perf/p3900-stash.sh new file +46 -0

46 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#133 lore raw

v9 cover Implement a batched fsync option for core.fsyncObjectFiles

In reply to Neeraj K. Singh via GitGitGadget

Thanks to everyone for review so far!

This series shares the base tmp-objdir patches with my merged version of Elijah Newren's remerge-diff series at:
https://github.com/neerajsi-msft/git/tree/neerajsi/remerge-diff.

Changes between v8 and v9 [1]:

* Rebased onto master at tag v2.34.0
* Fixed git-prune bug when trying to clean up multiple cruft directories.
* Preserve the tmp-objdir around update_relative_gitdir, which is called by setup_work_tree through the chdir_notify mechanism.
* Per [2], I'm leaving the fsyncObjectFiles configuration as is with 'true', 'false', and 'batch'. This makes using old and new versions of git with 'batch' mode a little trickier, but hopefully people will generally be moving forward in versions.

[1] See
https://lore.kernel.org/git/[email protected]/
[2] https://lore.kernel.org/git/[email protected]/

Changes between v7 and v8:

* Dropped the tmp-objdir patch to avoid renaming in a quarantine/temporary objdir, as suggested by Jeff King. This wasn't a good idea because we don't really know that there's only a single reader/writer. Avoiding the rename was a relatively minor perf optimization so it's okay to drop.

* Added disable_ref_updates logic (as a flag on the odb) which is set when we're in a quarantine or when a tmp objdir is active. I believe this roughly follows the strategy suggested by Jeff King.

Neeraj Singh (9):
tmp-objdir: new API for creating temporary writable databases
tmp-objdir: disable ref updates when replacing the primary odb
bulk-checkin: rename 'state' variable and separate 'plugged' boolean core.fsyncobjectfiles: batched disk flushes core.fsyncobjectfiles: add windows support for batch mode
update-index: use the bulk-checkin infrastructure
unpack-objects: use the bulk-checkin infrastructure core.fsyncobjectfiles: tests for batch mode core.fsyncobjectfiles: performance tests for add and stash

 Documentation/config/core.txt       | 29 ++++++++--
 Makefile                            |  6 ++
 builtin/prune.c                     | 23 ++++++--
 builtin/receive-pack.c              |  2 +-
 builtin/unpack-objects.c            |  3 +
 builtin/update-index.c              |  6 ++
 bulk-checkin.c                      | 90 +++++++++++++++++++++++++----
 bulk-checkin.h                      |  2 +
 cache.h                             |  8 ++-
 compat/mingw.h                      |  3 +
 compat/win32/flush.c                | 28 +++++++++
 config.c                            |  7 ++-
 config.mak.uname                    |  3 +
 configure.ac                        |  8 +++
 contrib/buildsystems/CMakeLists.txt |  3 +-
 environment.c                       | 11 +++-
 git-compat-util.h                   |  7 +++
 object-file.c                       | 60 ++++++++++++++++++-
 object-store.h                      | 26 +++++++++
 object.c                            |  2 +-
 refs.c                              |  2 +-
 repository.c                        |  2 +
 repository.h                        |  1 +
 t/lib-unique-files.sh               | 36 ++++++++++++
 t/perf/p3700-add.sh                 | 43 ++++++++++++++
 t/perf/p3900-stash.sh               | 46 +++++++++++++++
 t/t3700-add.sh                      | 20 +++++++
 t/t3903-stash.sh                    | 14 +++++
 t/t5300-pack-object.sh              | 30 ++++++----
 tmp-objdir.c                        | 55 +++++++++++++++++-
 tmp-objdir.h                        | 29 +++++++++-
 wrapper.c                           | 48 +++++++++++++++
 write-or-die.c                      |  2 +-
 33 files changed, 608 insertions(+), 47 deletions(-)
 create mode 100644 compat/win32/flush.c
 create mode 100644 t/lib-unique-files.sh
 create mode 100755 t/perf/p3700-add.sh
 create mode 100755 t/perf/p3900-stash.sh

base-commit: cd3e606211bb1cf8bc57f7d76bab98cc17a150bc
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1076%2Fneerajsi-msft%2Fneerajsi%2Fbulk-fsync-object-files-v9
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1076/neerajsi-msft/neerajsi/bulk-fsync-object-files-v9
Pull-Request: https://github.com/git/git/pull/1076

Range-diff vs v8:

1: f03797fd80d ! 1: 6b27afa60e0 tmp-objdir: new API for creating temporary writable databases

     @@ Commit message
          Based-on-patch-by: Elijah Newren <[email protected]>
      
          Signed-off-by: Neeraj Singh <[email protected]>
     +    Signed-off-by: Junio C Hamano <[email protected]>
      
       ## builtin/prune.c ##
      @@ builtin/prune.c: static int show_only;
     @@ builtin/prune.c: static int prune_tmp_file(const char *fullpath)
      +		if (show_only || verbose)
      +			printf("Removing stale temporary directory %s\n", fullpath);
      +		if (!show_only) {
     ++			strbuf_reset(&remove_dir_buf);
      +			strbuf_addstr(&remove_dir_buf, fullpath);
      +			remove_dir_recursively(&remove_dir_buf, 0);
      +		}
     @@ builtin/receive-pack.c: static const char *unpack(int err_fd, struct shallow_inf
       		if (err_fd > 0)
       			close(err_fd);
      
     + ## environment.c ##
     +@@
     + #include "commit.h"
     + #include "strvec.h"
     + #include "object-store.h"
     ++#include "tmp-objdir.h"
     + #include "chdir-notify.h"
     + #include "shallow.h"
     + 
     +@@ environment.c: static void update_relative_gitdir(const char *name,
     + 				   void *data)
     + {
     + 	char *path = reparent_relative_path(old_cwd, new_cwd, get_git_dir());
     ++	struct tmp_objdir *tmp_objdir = tmp_objdir_unapply_primary_odb();
     + 	trace_printf_key(&trace_setup_key,
     + 			 "setup: move $GIT_DIR to '%s'",
     + 			 path);
     ++
     + 	set_git_dir_1(path);
     ++	if (tmp_objdir)
     ++		tmp_objdir_reapply_primary_odb(tmp_objdir, old_cwd, new_cwd);
     + 	free(path);
     + }
     + 
     +
       ## object-file.c ##
      @@ object-file.c: void add_to_alternates_memory(const char *reference)
       			     '\n', NULL, 0);
     @@ object.c: struct raw_object_store *raw_object_store_new(void)
       	odb_clear_loose_cache(odb);
      
       ## tmp-objdir.c ##
     +@@
     + #include "cache.h"
     + #include "tmp-objdir.h"
     ++#include "chdir-notify.h"
     + #include "dir.h"
     + #include "sigchain.h"
     + #include "string-list.h"
      @@
       struct tmp_objdir {
       	struct strbuf path;
       	struct strvec env;
      +	struct object_directory *prev_odb;
     ++	int will_destroy;
       };
       
       /*
     @@ tmp-objdir.c: void tmp_objdir_add_as_alternate(const struct tmp_objdir *t)
      +	if (t->prev_odb)
      +		BUG("the primary object database is already replaced");
      +	t->prev_odb = set_temporary_primary_odb(t->path.buf, will_destroy);
     ++	t->will_destroy = will_destroy;
     ++}
     ++
     ++struct tmp_objdir *tmp_objdir_unapply_primary_odb(void)
     ++{
     ++	if (!the_tmp_objdir || !the_tmp_objdir->prev_odb)
     ++		return NULL;
     ++
     ++	restore_primary_odb(the_tmp_objdir->prev_odb, the_tmp_objdir->path.buf);
     ++	the_tmp_objdir->prev_odb = NULL;
     ++	return the_tmp_objdir;
     ++}
     ++
     ++void tmp_objdir_reapply_primary_odb(struct tmp_objdir *t, const char *old_cwd,
     ++		const char *new_cwd)
     ++{
     ++	char *path;
     ++
     ++	path = reparent_relative_path(old_cwd, new_cwd, t->path.buf);
     ++	strbuf_reset(&t->path);
     ++	strbuf_addstr(&t->path, path);
     ++	free(path);
     ++	tmp_objdir_replace_primary_odb(t, t->will_destroy);
      +}
      
       ## tmp-objdir.h ##
     @@ tmp-objdir.h: int tmp_objdir_destroy(struct tmp_objdir *);
      + * If will_destroy is nonzero, the object directory may not be migrated.
      + */
      +void tmp_objdir_replace_primary_odb(struct tmp_objdir *, int will_destroy);
     ++
     ++/*
     ++ * If the primary object database was replaced by a temporary object directory,
     ++ * restore it to its original value while keeping the directory contents around.
     ++ * Returns NULL if the primary object database was not replaced.
     ++ */
     ++struct tmp_objdir *tmp_objdir_unapply_primary_odb(void);
     ++
     ++/*
     ++ * Reapplies the former primary temporary object database, after protentially
     ++ * changing its relative path.
     ++ */
     ++void tmp_objdir_reapply_primary_odb(struct tmp_objdir *, const char *old_cwd,
     ++		const char *new_cwd);
     ++
      +
       #endif /* TMP_OBJDIR_H */

2: bc085137340 ! 2: 71817cccfb9 tmp-objdir: disable ref updates when replacing the primary odb
@@ Commit message

+ Signed-off-by: Junio C Hamano <[email protected]>

## environment.c ##
@@ environment.c: void setup_git_env(const char *git_dir)

  3:  9335646ed91 =  3:  8fd1ca4c00a bulk-checkin: rename 'state' variable and separate 'plugged' boolean
  4:  b9d3d874432 =  4:  e1747ce00af core.fsyncobjectfiles: batched disk flushes
  5:  8df32eaaa9a !  5:  951a559874e core.fsyncobjectfiles: add windows support for batch mode
     @@ config.mak.uname: endif
       		compat/win32/path-utils.o \
       		compat/win32/pthread.o compat/win32/syslog.o \
       		compat/win32/trace2_win32_process_info.o \
     -@@ config.mak.uname: ifneq (,$(findstring MINGW,$(uname_S)))
     +@@ config.mak.uname: ifeq ($(uname_S),MINGW)
       	COMPAT_CFLAGS += -DSTRIP_EXTENSION=\".exe\"
       	COMPAT_OBJS += compat/mingw.o compat/winansi.o \
       		compat/win32/trace2_win32_process_info.o \
  6:  15767270984 =  6:  4a40fd4a29a update-index: use the bulk-checkin infrastructure
  7:  e88bab809a2 =  7:  cfc6a347d08 unpack-objects: use the bulk-checkin infrastructure
  8:  811d6d31509 !  8:  270c24827d0 core.fsyncobjectfiles: tests for batch mode
     @@ t/lib-unique-files.sh (new)
      
       ## t/t3700-add.sh ##
      @@ t/t3700-add.sh: test_description='Test of git add, including the -- option.'
     - 
     + TEST_PASSES_SANITIZE_LEAK=true
       . ./test-lib.sh
       
      +. $TEST_DIRECTORY/lib-unique-files.sh

9: f4fa20f591e = 9: 12d99641f4c core.fsyncobjectfiles: performance tests for add and stash

Signature · 1 line
gitgitgadget
#134 lore raw

PATCH v9 1/9 tmp-objdir: new API for creating temporary writable databases

From: Neeraj Singh <[email protected]>

The tmp_objdir API provides the ability to create temporary object directories, but was designed with the goal of having subprocesses access these object stores, followed by the main process migrating objects from it to the main object store or just deleting it. The subprocesses would view it as their primary datastore and write to it.

Here we add the tmp_objdir_replace_primary_odb function that replaces the current process's writable "main" object directory with the specified one. The previous main object directory is restored in either tmp_objdir_migrate or tmp_objdir_destroy.

For the --remerge-diff usecase, add a new will_destroy flag in struct object_database to mark ephemeral object databases that do not require fsync durability.

Add 'git prune' support for removing temporary object databases, and make sure that they have a name starting with tmp_ and containing an operation-specific name.

---
 builtin/prune.c        | 23 +++++++++++++++---
 builtin/receive-pack.c |  2 +-
 environment.c          |  5 ++++
 object-file.c          | 44 +++++++++++++++++++++++++++++++--
 object-store.h         | 19 +++++++++++++++
 object.c               |  2 +-
 tmp-objdir.c           | 55 +++++++++++++++++++++++++++++++++++++++---
 tmp-objdir.h           | 29 +++++++++++++++++++---
 8 files changed, 165 insertions(+), 14 deletions(-)
builtin/prune.c +19 -4
index 485c9a3c56f..a76e6a5f0e8 100644
@@ -18,6 +18,7 @@ static int show_only;
 static int verbose;
 static timestamp_t expire;
 static int show_progress = -1;
+static struct strbuf remove_dir_buf = STRBUF_INIT;
 
 static int prune_tmp_file(const char *fullpath)
 {
@@ -26,10 +27,20 @@ static int prune_tmp_file(const char *fullpath)
 		return error("Could not stat '%s'", fullpath);
 	if (st.st_mtime > expire)
 		return 0;
-	if (show_only || verbose)
-		printf("Removing stale temporary file %s\n", fullpath);
-	if (!show_only)
-		unlink_or_warn(fullpath);
+	if (S_ISDIR(st.st_mode)) {
+		if (show_only || verbose)
+			printf("Removing stale temporary directory %s\n", fullpath);
+		if (!show_only) {
+			strbuf_reset(&remove_dir_buf);
+			strbuf_addstr(&remove_dir_buf, fullpath);
+			remove_dir_recursively(&remove_dir_buf, 0);
+		}
+	} else {
+		if (show_only || verbose)
+			printf("Removing stale temporary file %s\n", fullpath);
+		if (!show_only)
+			unlink_or_warn(fullpath);
+	}
 	return 0;
 }
 
@@ -97,6 +108,9 @@ static int prune_cruft(const char *basename, const char *path, void *data)
 
 static int prune_subdir(unsigned int nr, const char *path, void *data)
 {
+	if (verbose)
+		printf("Removing directory %s\n", path);
+
 	if (!show_only)
 		rmdir(path);
 	return 0;
@@ -184,5 +198,6 @@ int cmd_prune(int argc, const char **argv, const char *prefix)
 		prune_shallow(show_only ? PRUNE_SHOW_ONLY : 0);
 	}
 
+	strbuf_release(&remove_dir_buf);
 	return 0;
 }
builtin/receive-pack.c +1 -1
index 49b846d9605..8815e24cde5 100644
@@ -2213,7 +2213,7 @@ static const char *unpack(int err_fd, struct shallow_info *si)
 		strvec_push(&child.args, alt_shallow_file);
 	}
 
-	tmp_objdir = tmp_objdir_create();
+	tmp_objdir = tmp_objdir_create("incoming");
 	if (!tmp_objdir) {
 		if (err_fd > 0)
 			close(err_fd);
environment.c +5 -0
index 9da7f3c1a19..342400fcaad 100644
@@ -17,6 +17,7 @@
 #include "commit.h"
 #include "strvec.h"
 #include "object-store.h"
+#include "tmp-objdir.h"
 #include "chdir-notify.h"
 #include "shallow.h"
 
@@ -331,10 +332,14 @@ static void update_relative_gitdir(const char *name,
 				   void *data)
 {
 	char *path = reparent_relative_path(old_cwd, new_cwd, get_git_dir());
+	struct tmp_objdir *tmp_objdir = tmp_objdir_unapply_primary_odb();
 	trace_printf_key(&trace_setup_key,
 			 "setup: move $GIT_DIR to '%s'",
 			 path);
+
 	set_git_dir_1(path);
+	if (tmp_objdir)
+		tmp_objdir_reapply_primary_odb(tmp_objdir, old_cwd, new_cwd);
 	free(path);
 }
 
object-file.c +42 -2

56 lines not on this page · raw message

object-store.h +19 -0
index 952efb6a4be..cb173e69392 100644
@@ -27,6 +27,11 @@ struct object_directory {
 	uint32_t loose_objects_subdir_seen[8]; /* 256 bits */
 	struct oidtree *loose_objects_cache;
 
+	/*
+	 * This object store is ephemeral, so there is no need to fsync.
+	 */
+	int will_destroy;
+
 	/*
 	 * Path to the alternative object store. If this is a relative path,
 	 * it is relative to the current working directory.
@@ -58,6 +63,17 @@ void add_to_alternates_file(const char *dir);
  */
 void add_to_alternates_memory(const char *dir);
 
+/*
+ * Replace the current writable object directory with the specified temporary
+ * object directory; returns the former primary object directory.
+ */
+struct object_directory *set_temporary_primary_odb(const char *dir, int will_destroy);
+
+/*
+ * Restore a previous ODB replaced by set_temporary_main_odb.
+ */
+void restore_primary_odb(struct object_directory *restore_odb, const char *old_path);
+
 /*
  * Populate and return the loose object cache array corresponding to the
  * given object ID.
@@ -68,6 +84,9 @@ struct oidtree *odb_loose_cache(struct object_directory *odb,
 /* Empty the loose object cache for the specified object directory. */
 void odb_clear_loose_cache(struct object_directory *odb);
 
+/* Clear and free the specified object directory */
+void free_object_directory(struct object_directory *odb);
+
 struct packed_git {
 	struct hashmap_entry packmap_ent;
 	struct packed_git *next;
object.c +1 -1
index 23a24e678a8..048f96a260e 100644
@@ -513,7 +513,7 @@ struct raw_object_store *raw_object_store_new(void)
 	return o;
 }
 
-static void free_object_directory(struct object_directory *odb)
+void free_object_directory(struct object_directory *odb)
 {
 	free(odb->path);
 	odb_clear_loose_cache(odb);
tmp-objdir.c +52 -3

102 lines not on this page · raw message

tmp-objdir.h +26 -3
index b1e45b4c75d..a3145051f25 100644
@@ -10,7 +10,7 @@
  *
  * Example:
  *
- *	struct tmp_objdir *t = tmp_objdir_create();
+ *	struct tmp_objdir *t = tmp_objdir_create("incoming");
  *	if (!run_command_v_opt_cd_env(cmd, 0, NULL, tmp_objdir_env(t)) &&
  *	    !tmp_objdir_migrate(t))
  *		printf("success!\n");
@@ -22,9 +22,10 @@
 struct tmp_objdir;
 
 /*
- * Create a new temporary object directory; returns NULL on failure.
+ * Create a new temporary object directory with the specified prefix;
+ * returns NULL on failure.
  */
-struct tmp_objdir *tmp_objdir_create(void);
+struct tmp_objdir *tmp_objdir_create(const char *prefix);
 
 /*
  * Return a list of environment strings, suitable for use with
@@ -51,4 +52,26 @@ int tmp_objdir_destroy(struct tmp_objdir *);
  */
 void tmp_objdir_add_as_alternate(const struct tmp_objdir *);
 
+/*
+ * Replaces the main object store in the current process with the temporary
+ * object directory and makes the former main object store an alternate.
+ * If will_destroy is nonzero, the object directory may not be migrated.
+ */
+void tmp_objdir_replace_primary_odb(struct tmp_objdir *, int will_destroy);
+
+/*
+ * If the primary object database was replaced by a temporary object directory,
+ * restore it to its original value while keeping the directory contents around.
+ * Returns NULL if the primary object database was not replaced.
+ */
+struct tmp_objdir *tmp_objdir_unapply_primary_odb(void);
+
+/*
+ * Reapplies the former primary temporary object database, after protentially
+ * changing its relative path.
+ */
+void tmp_objdir_reapply_primary_odb(struct tmp_objdir *, const char *old_cwd,
+		const char *new_cwd);
+
+
 #endif /* TMP_OBJDIR_H */
Signature · 1 line
gitgitgadget
#135 lore raw

Re: [PATCH v9 1/9] tmp-objdir: new API for creating temporary writable databases

Quoted 54 lines · Neeraj Singh via GitGitGadget

From: Neeraj Singh <[email protected]>

The tmp_objdir API provides the ability to create temporary object directories, but was designed with the goal of having subprocesses access these object stores, followed by the main process migrating objects from it to the main object store or just deleting it. The subprocesses would view it as their primary datastore and write to it.

Here we add the tmp_objdir_replace_primary_odb function that replaces the current process's writable "main" object directory with the specified one. The previous main object directory is restored in either tmp_objdir_migrate or tmp_objdir_destroy.

For the --remerge-diff usecase, add a new will_destroy flag in struct object_database to mark ephemeral object databases that do not require fsync durability.

Add 'git prune' support for removing temporary object databases, and make sure that they have a name starting with tmp_ and containing an operation-specific name.

---
 builtin/prune.c        | 23 +++++++++++++++---
 builtin/receive-pack.c |  2 +-
 environment.c          |  5 ++++
 object-file.c          | 44 +++++++++++++++++++++++++++++++--
 object-store.h         | 19 +++++++++++++++
 object.c               |  2 +-
 tmp-objdir.c           | 55 +++++++++++++++++++++++++++++++++++++++---
 tmp-objdir.h           | 29 +++++++++++++++++++---
 8 files changed, 165 insertions(+), 14 deletions(-)
builtin/prune.c +1 -1
index 485c9a3c56f..a76e6a5f0e8 100644
@@ -18,6 +18,7 @@ static int show_only;
 static int verbose;
 static timestamp_t expire;
 static int show_progress = -1;
+static struct strbuf remove_dir_buf = STRBUF_INIT;
 
 static int prune_tmp_file(const char *fullpath)
 {
@@ -26,10 +27,20 @@ static int prune_tmp_file(const char *fullpath)
                return error("Could not stat '%s'", fullpath);
        if (st.st_mtime > expire)
                return 0;
-       if (show_only || verbose)

builtin/prune.c static int prune_tmp_file(const char *fullpath) · line 36

Quoted 10 lines
builtin/prune.c +7 -3
-               printf("Removing stale temporary file %s\n", fullpath);
-       if (!show_only)
-               unlink_or_warn(fullpath);
+       if (S_ISDIR(st.st_mode)) {
+               if (show_only || verbose)
+                       printf("Removing stale temporary directory %s\n", fullpath);
+               if (!show_only) {
+                       strbuf_reset(&remove_dir_buf);
+                       strbuf_addstr(&remove_dir_buf, fullpath);
+                       remove_dir_recursively(&remove_dir_buf, 0);

Why not just define remove_dir_buf here rather than as a global? It'd not only make the code more readable by keeping everything localized, it would have prevented the forgotten strbuf_reset() bug from the earlier round of this patch.

Sure, that'd be an extra memory allocation/free for each directory you hit, which should be negligible compared to the cost of remove_dir_recursively()...and I'm not sure this is performance critical anyway (I don't see why we'd expect more than O(1) cruft temporary directories).

Quoted 11 lines
builtin/prune.c +7 -0
+               }
+       } else {
+               if (show_only || verbose)
+                       printf("Removing stale temporary file %s\n", fullpath);
+               if (!show_only)
+                       unlink_or_warn(fullpath);
+       }
        return 0;
 }
 

builtin/prune.c static int prune_cruft(const char *basename, const char *path, void *data) · line 111

Quoted 4 lines
builtin/prune.c +1 -0
@@ -97,6 +108,9 @@ static int prune_cruft(const char *basename, const char *path, void *data)
 
 static int prune_subdir(unsigned int nr, const char *path, void *data)
 {
+       if (verbose)

Shouldn't this be

    if (show_only || verbose)

?

Quoted 340 lines
builtin/prune.c +3 -0
+               printf("Removing directory %s\n", path);
+
        if (!show_only)
                rmdir(path);
        return 0;
@@ -184,5 +198,6 @@ int cmd_prune(int argc, const char **argv, const char *prefix)
                prune_shallow(show_only ? PRUNE_SHOW_ONLY : 0);
        }
 
+       strbuf_release(&remove_dir_buf);
        return 0;
 }
builtin/receive-pack.c +1 -1
index 49b846d9605..8815e24cde5 100644
@@ -2213,7 +2213,7 @@ static const char *unpack(int err_fd, struct shallow_info *si)
                strvec_push(&child.args, alt_shallow_file);
        }
 
-       tmp_objdir = tmp_objdir_create();
+       tmp_objdir = tmp_objdir_create("incoming");
        if (!tmp_objdir) {
                if (err_fd > 0)
                        close(err_fd);
environment.c +5 -0
index 9da7f3c1a19..342400fcaad 100644
@@ -17,6 +17,7 @@
 #include "commit.h"
 #include "strvec.h"
 #include "object-store.h"
+#include "tmp-objdir.h"
 #include "chdir-notify.h"
 #include "shallow.h"
 
@@ -331,10 +332,14 @@ static void update_relative_gitdir(const char *name,
                                   void *data)
 {
        char *path = reparent_relative_path(old_cwd, new_cwd, get_git_dir());
+       struct tmp_objdir *tmp_objdir = tmp_objdir_unapply_primary_odb();
        trace_printf_key(&trace_setup_key,
                         "setup: move $GIT_DIR to '%s'",
                         path);
+
        set_git_dir_1(path);
+       if (tmp_objdir)
+               tmp_objdir_reapply_primary_odb(tmp_objdir, old_cwd, new_cwd);
        free(path);
 }
 
object-file.c +42 -2
index c3d866a287e..0b6a61aeaff 100644
@@ -683,6 +683,43 @@ void add_to_alternates_memory(const char *reference)
                             '\n', NULL, 0);
 }
 
+struct object_directory *set_temporary_primary_odb(const char *dir, int will_destroy)
+{
+       struct object_directory *new_odb;
+
+       /*
+        * Make sure alternates are initialized, or else our entry may be
+        * overwritten when they are.
+        */
+       prepare_alt_odb(the_repository);
+
+       /*
+        * Make a new primary odb and link the old primary ODB in as an
+        * alternate
+        */
+       new_odb = xcalloc(1, sizeof(*new_odb));
+       new_odb->path = xstrdup(dir);
+       new_odb->will_destroy = will_destroy;
+       new_odb->next = the_repository->objects->odb;
+       the_repository->objects->odb = new_odb;
+       return new_odb->next;
+}
+
+void restore_primary_odb(struct object_directory *restore_odb, const char *old_path)
+{
+       struct object_directory *cur_odb = the_repository->objects->odb;
+
+       if (strcmp(old_path, cur_odb->path))
+               BUG("expected %s as primary object store; found %s",
+                   old_path, cur_odb->path);
+
+       if (cur_odb->next != restore_odb)
+               BUG("we expect the old primary object store to be the first alternate");
+
+       the_repository->objects->odb = restore_odb;
+       free_object_directory(cur_odb);
+}
+
 /*
  * Compute the exact path an alternate is at and returns it. In case of
  * error NULL is returned and the human readable error is added to `err`
@@ -1809,8 +1846,11 @@ int hash_object_file(const struct git_hash_algo *algo, const void *buf,
 /* Finalize a file on disk, and close it. */
 static void close_loose_object(int fd)
 {
-       if (fsync_object_files)
-               fsync_or_die(fd, "loose object file");
+       if (!the_repository->objects->odb->will_destroy) {
+               if (fsync_object_files)
+                       fsync_or_die(fd, "loose object file");
+       }
+
        if (close(fd) != 0)
                die_errno(_("error when closing loose object file"));
 }
object-store.h +19 -0
index 952efb6a4be..cb173e69392 100644
@@ -27,6 +27,11 @@ struct object_directory {
        uint32_t loose_objects_subdir_seen[8]; /* 256 bits */
        struct oidtree *loose_objects_cache;
 
+       /*
+        * This object store is ephemeral, so there is no need to fsync.
+        */
+       int will_destroy;
+
        /*
         * Path to the alternative object store. If this is a relative path,
         * it is relative to the current working directory.
@@ -58,6 +63,17 @@ void add_to_alternates_file(const char *dir);
  */
 void add_to_alternates_memory(const char *dir);
 
+/*
+ * Replace the current writable object directory with the specified temporary
+ * object directory; returns the former primary object directory.
+ */
+struct object_directory *set_temporary_primary_odb(const char *dir, int will_destroy);
+
+/*
+ * Restore a previous ODB replaced by set_temporary_main_odb.
+ */
+void restore_primary_odb(struct object_directory *restore_odb, const char *old_path);
+
 /*
  * Populate and return the loose object cache array corresponding to the
  * given object ID.
@@ -68,6 +84,9 @@ struct oidtree *odb_loose_cache(struct object_directory *odb,
 /* Empty the loose object cache for the specified object directory. */
 void odb_clear_loose_cache(struct object_directory *odb);
 
+/* Clear and free the specified object directory */
+void free_object_directory(struct object_directory *odb);
+
 struct packed_git {
        struct hashmap_entry packmap_ent;
        struct packed_git *next;
object.c +1 -1
index 23a24e678a8..048f96a260e 100644
@@ -513,7 +513,7 @@ struct raw_object_store *raw_object_store_new(void)
        return o;
 }
 
-static void free_object_directory(struct object_directory *odb)
+void free_object_directory(struct object_directory *odb)
 {
        free(odb->path);
        odb_clear_loose_cache(odb);
tmp-objdir.c +52 -3
index b8d880e3626..3d38eeab66b 100644
@@ -1,5 +1,6 @@
 #include "cache.h"
 #include "tmp-objdir.h"
+#include "chdir-notify.h"
 #include "dir.h"
 #include "sigchain.h"
 #include "string-list.h"
@@ -11,6 +12,8 @@
 struct tmp_objdir {
        struct strbuf path;
        struct strvec env;
+       struct object_directory *prev_odb;
+       int will_destroy;
 };
 
 /*
@@ -38,6 +41,9 @@ static int tmp_objdir_destroy_1(struct tmp_objdir *t, int on_signal)
        if (t == the_tmp_objdir)
                the_tmp_objdir = NULL;
 
+       if (!on_signal && t->prev_odb)
+               restore_primary_odb(t->prev_odb, t->path.buf);
+
        /*
         * This may use malloc via strbuf_grow(), but we should
         * have pre-grown t->path sufficiently so that this
@@ -52,6 +58,7 @@ static int tmp_objdir_destroy_1(struct tmp_objdir *t, int on_signal)
         */
        if (!on_signal)
                tmp_objdir_free(t);
+
        return err;
 }
 
@@ -121,7 +128,7 @@ static int setup_tmp_objdir(const char *root)
        return ret;
 }
 
-struct tmp_objdir *tmp_objdir_create(void)
+struct tmp_objdir *tmp_objdir_create(const char *prefix)
 {
        static int installed_handlers;
        struct tmp_objdir *t;
@@ -129,11 +136,16 @@ struct tmp_objdir *tmp_objdir_create(void)
        if (the_tmp_objdir)
                BUG("only one tmp_objdir can be used at a time");
 
-       t = xmalloc(sizeof(*t));
+       t = xcalloc(1, sizeof(*t));
        strbuf_init(&t->path, 0);
        strvec_init(&t->env);
 
-       strbuf_addf(&t->path, "%s/incoming-XXXXXX", get_object_directory());
+       /*
+        * Use a string starting with tmp_ so that the builtin/prune.c code
+        * can recognize any stale objdirs left behind by a crash and delete
+        * them.
+        */
+       strbuf_addf(&t->path, "%s/tmp_objdir-%s-XXXXXX", get_object_directory(), prefix);
 
        /*
         * Grow the strbuf beyond any filename we expect to be placed in it.
@@ -269,6 +281,13 @@ int tmp_objdir_migrate(struct tmp_objdir *t)
        if (!t)
                return 0;
 
+       if (t->prev_odb) {
+               if (the_repository->objects->odb->will_destroy)
+                       BUG("migrating an ODB that was marked for destruction");
+               restore_primary_odb(t->prev_odb, t->path.buf);
+               t->prev_odb = NULL;
+       }
+
        strbuf_addbuf(&src, &t->path);
        strbuf_addstr(&dst, get_object_directory());
 
@@ -292,3 +311,33 @@ void tmp_objdir_add_as_alternate(const struct tmp_objdir *t)
 {
        add_to_alternates_memory(t->path.buf);
 }
+
+void tmp_objdir_replace_primary_odb(struct tmp_objdir *t, int will_destroy)
+{
+       if (t->prev_odb)
+               BUG("the primary object database is already replaced");
+       t->prev_odb = set_temporary_primary_odb(t->path.buf, will_destroy);
+       t->will_destroy = will_destroy;
+}
+
+struct tmp_objdir *tmp_objdir_unapply_primary_odb(void)
+{
+       if (!the_tmp_objdir || !the_tmp_objdir->prev_odb)
+               return NULL;
+
+       restore_primary_odb(the_tmp_objdir->prev_odb, the_tmp_objdir->path.buf);
+       the_tmp_objdir->prev_odb = NULL;
+       return the_tmp_objdir;
+}
+
+void tmp_objdir_reapply_primary_odb(struct tmp_objdir *t, const char *old_cwd,
+               const char *new_cwd)
+{
+       char *path;
+
+       path = reparent_relative_path(old_cwd, new_cwd, t->path.buf);
+       strbuf_reset(&t->path);
+       strbuf_addstr(&t->path, path);
+       free(path);
+       tmp_objdir_replace_primary_odb(t, t->will_destroy);
+}
tmp-objdir.h +26 -4
index b1e45b4c75d..a3145051f25 100644
@@ -10,7 +10,7 @@
  *
  * Example:
  *
- *     struct tmp_objdir *t = tmp_objdir_create();
+ *     struct tmp_objdir *t = tmp_objdir_create("incoming");
  *     if (!run_command_v_opt_cd_env(cmd, 0, NULL, tmp_objdir_env(t)) &&
  *         !tmp_objdir_migrate(t))
  *             printf("success!\n");
@@ -22,9 +22,10 @@
 struct tmp_objdir;
 
 /*
- * Create a new temporary object directory; returns NULL on failure.
+ * Create a new temporary object directory with the specified prefix;
+ * returns NULL on failure.
  */
-struct tmp_objdir *tmp_objdir_create(void);
+struct tmp_objdir *tmp_objdir_create(const char *prefix);
 
 /*
  * Return a list of environment strings, suitable for use with
@@ -51,4 +52,26 @@ int tmp_objdir_destroy(struct tmp_objdir *);
  */
 void tmp_objdir_add_as_alternate(const struct tmp_objdir *);
 
+/*
+ * Replaces the main object store in the current process with the temporary
+ * object directory and makes the former main object store an alternate.
+ * If will_destroy is nonzero, the object directory may not be migrated.
+ */
+void tmp_objdir_replace_primary_odb(struct tmp_objdir *, int will_destroy);
+
+/*
+ * If the primary object database was replaced by a temporary object directory,
+ * restore it to its original value while keeping the directory contents around.
+ * Returns NULL if the primary object database was not replaced.
+ */
+struct tmp_objdir *tmp_objdir_unapply_primary_odb(void);
+
+/*
+ * Reapplies the former primary temporary object database, after protentially
+ * changing its relative path.
+ */
+void tmp_objdir_reapply_primary_odb(struct tmp_objdir *, const char *old_cwd,
+               const char *new_cwd);
+
+
 #endif /* TMP_OBJDIR_H */
--

gitgitgadget

#136 lore raw

Re: [PATCH v9 1/9] tmp-objdir: new API for creating temporary writable databases

Quoted 73 lines · Elijah Newren
Quoted 54 lines · Neeraj Singh via GitGitGadget

From: Neeraj Singh <[email protected]>

The tmp_objdir API provides the ability to create temporary object directories, but was designed with the goal of having subprocesses access these object stores, followed by the main process migrating objects from it to the main object store or just deleting it. The subprocesses would view it as their primary datastore and write to it.

Here we add the tmp_objdir_replace_primary_odb function that replaces the current process's writable "main" object directory with the specified one. The previous main object directory is restored in either tmp_objdir_migrate or tmp_objdir_destroy.

For the --remerge-diff usecase, add a new will_destroy flag in struct object_database to mark ephemeral object databases that do not require fsync durability.

Add 'git prune' support for removing temporary object databases, and make sure that they have a name starting with tmp_ and containing an operation-specific name.

---
 builtin/prune.c        | 23 +++++++++++++++---
 builtin/receive-pack.c |  2 +-
 environment.c          |  5 ++++
 object-file.c          | 44 +++++++++++++++++++++++++++++++--
 object-store.h         | 19 +++++++++++++++
 object.c               |  2 +-
 tmp-objdir.c           | 55 +++++++++++++++++++++++++++++++++++++++---
 tmp-objdir.h           | 29 +++++++++++++++++++---
 8 files changed, 165 insertions(+), 14 deletions(-)
builtin/prune.c +1 -1
index 485c9a3c56f..a76e6a5f0e8 100644
@@ -18,6 +18,7 @@ static int show_only;
 static int verbose;
 static timestamp_t expire;
 static int show_progress = -1;
+static struct strbuf remove_dir_buf = STRBUF_INIT;
 
 static int prune_tmp_file(const char *fullpath)
 {
@@ -26,10 +27,20 @@ static int prune_tmp_file(const char *fullpath)
                return error("Could not stat '%s'", fullpath);
        if (st.st_mtime > expire)
                return 0;
-       if (show_only || verbose)

builtin/prune.c static int prune_tmp_file(const char *fullpath) · line 36

Quoted 10 lines
builtin/prune.c +7 -3
-               printf("Removing stale temporary file %s\n", fullpath);
-       if (!show_only)
-               unlink_or_warn(fullpath);
+       if (S_ISDIR(st.st_mode)) {
+               if (show_only || verbose)
+                       printf("Removing stale temporary directory %s\n", fullpath);
+               if (!show_only) {
+                       strbuf_reset(&remove_dir_buf);
+                       strbuf_addstr(&remove_dir_buf, fullpath);
+                       remove_dir_recursively(&remove_dir_buf, 0);

Why not just define remove_dir_buf here rather than as a global? It'd not only make the code more readable by keeping everything localized, it would have prevented the forgotten strbuf_reset() bug from the earlier round of this patch.

Quoted 5 lines

Sure, that'd be an extra memory allocation/free for each directory you hit, which should be negligible compared to the cost of remove_dir_recursively()...and I'm not sure this is performance critical anyway (I don't see why we'd expect more than O(1) cruft temporary directories).

I'll take this suggestion.

Quoted 12 lines
Quoted 12 lines
builtin/prune.c +7 -0
+               }
+       } else {
+               if (show_only || verbose)
+                       printf("Removing stale temporary file %s\n", fullpath);
+               if (!show_only)
+                       unlink_or_warn(fullpath);
+       }
        return 0;
 }
 
@@ -97,6 +108,9 @@ static int prune_cruft(const char *basename, const char *path, void *data)
 
Quoted 7 lines

builtin/prune.c static int prune_cruft(const char *basename, const char *path, void *data) · line 111

Quoted 3 lines
builtin/prune.c +1 -0
 static int prune_subdir(unsigned int nr, const char *path, void *data)
 {
+       if (verbose)

Shouldn't this be

    if (show_only || verbose)

?

Doing that breaks one of the tests, since we print extra stuff that's unexpected. I think I'm going to just revert this change, since it appears that we call this callback and try to remove the directory even if it's non-empty.

Do you have any comments or thoughts on how we want to allow the user to configure fsync settings?

Thanks,
Neeraj

#137 lore raw

Re: [PATCH v9 1/9] tmp-objdir: new API for creating temporary writable databases

Quoted 103 lines · Neeraj Singh
Quoted 73 lines · Elijah Newren
Quoted 54 lines · Neeraj Singh via GitGitGadget

From: Neeraj Singh <[email protected]>

The tmp_objdir API provides the ability to create temporary object directories, but was designed with the goal of having subprocesses access these object stores, followed by the main process migrating objects from it to the main object store or just deleting it. The subprocesses would view it as their primary datastore and write to it.

Here we add the tmp_objdir_replace_primary_odb function that replaces the current process's writable "main" object directory with the specified one. The previous main object directory is restored in either tmp_objdir_migrate or tmp_objdir_destroy.

For the --remerge-diff usecase, add a new will_destroy flag in struct object_database to mark ephemeral object databases that do not require fsync durability.

Add 'git prune' support for removing temporary object databases, and make sure that they have a name starting with tmp_ and containing an operation-specific name.

---
 builtin/prune.c        | 23 +++++++++++++++---
 builtin/receive-pack.c |  2 +-
 environment.c          |  5 ++++
 object-file.c          | 44 +++++++++++++++++++++++++++++++--
 object-store.h         | 19 +++++++++++++++
 object.c               |  2 +-
 tmp-objdir.c           | 55 +++++++++++++++++++++++++++++++++++++++---
 tmp-objdir.h           | 29 +++++++++++++++++++---
 8 files changed, 165 insertions(+), 14 deletions(-)
builtin/prune.c +1 -1
index 485c9a3c56f..a76e6a5f0e8 100644
@@ -18,6 +18,7 @@ static int show_only;
 static int verbose;
 static timestamp_t expire;
 static int show_progress = -1;
+static struct strbuf remove_dir_buf = STRBUF_INIT;
 
 static int prune_tmp_file(const char *fullpath)
 {
@@ -26,10 +27,20 @@ static int prune_tmp_file(const char *fullpath)
                return error("Could not stat '%s'", fullpath);
        if (st.st_mtime > expire)
                return 0;
-       if (show_only || verbose)

builtin/prune.c static int prune_tmp_file(const char *fullpath) · line 36

Quoted 10 lines
builtin/prune.c +7 -3
-               printf("Removing stale temporary file %s\n", fullpath);
-       if (!show_only)
-               unlink_or_warn(fullpath);
+       if (S_ISDIR(st.st_mode)) {
+               if (show_only || verbose)
+                       printf("Removing stale temporary directory %s\n", fullpath);
+               if (!show_only) {
+                       strbuf_reset(&remove_dir_buf);
+                       strbuf_addstr(&remove_dir_buf, fullpath);
+                       remove_dir_recursively(&remove_dir_buf, 0);

Why not just define remove_dir_buf here rather than as a global? It'd not only make the code more readable by keeping everything localized, it would have prevented the forgotten strbuf_reset() bug from the earlier round of this patch.

Quoted 5 lines

Sure, that'd be an extra memory allocation/free for each directory you hit, which should be negligible compared to the cost of remove_dir_recursively()...and I'm not sure this is performance critical anyway (I don't see why we'd expect more than O(1) cruft temporary directories).

I'll take this suggestion.

Quoted 12 lines
Quoted 12 lines
builtin/prune.c +7 -0
+               }
+       } else {
+               if (show_only || verbose)
+                       printf("Removing stale temporary file %s\n", fullpath);
+               if (!show_only)
+                       unlink_or_warn(fullpath);
+       }
        return 0;
 }
 
@@ -97,6 +108,9 @@ static int prune_cruft(const char *basename, const char *path, void *data)
 
Quoted 7 lines

builtin/prune.c static int prune_cruft(const char *basename, const char *path, void *data) · line 111

Quoted 3 lines
builtin/prune.c +1 -0
 static int prune_subdir(unsigned int nr, const char *path, void *data)
 {
+       if (verbose)

Shouldn't this be

    if (show_only || verbose)

?

Quoted 4 lines

Doing that breaks one of the tests, since we print extra stuff that's unexpected. I think I'm going to just revert this change, since it appears that we call this callback and try to remove the directory even if it's non-empty.

Makes sense.

Quoted 2 lines

Do you have any comments or thoughts on how we want to allow the user to configure fsync settings?

I don't; sorry.

#138 lore raw

PATCH v9 2/9 tmp-objdir: disable ref updates when replacing the primary odb

In reply to Neeraj K. Singh via GitGitGadget

From: Neeraj Singh <[email protected]>

When creating a subprocess with a temporary ODB, we set the GIT_QUARANTINE_ENVIRONMENT env var to tell child Git processes not to update refs, since the tmp-objdir may go away.

Introduce a similar mechanism for in-process temporary ODBs when we call tmp_objdir_replace_primary_odb. Now both mechanisms set the disable_ref_updates flag on the odb, which is queried by the ref_transaction_prepare function.

Note: This change adds an assumption that the state of the_repository is relevant for any ref transaction that might be initiated. Unwinding this assumption should be straightforward by saving the relevant repository to query in the transaction or the ref_store.

Peff's test case was invoking ref updates via the cachetextconv setting. That particular code silently does nothing when a ref update is forbidden. See the call to notes_cache_put in fill_textconv where errors are ignored.

---
 environment.c  | 4 ++++
 object-file.c  | 6 ++++++
 object-store.h | 9 ++++++++-
 refs.c         | 2 +-
 repository.c   | 2 ++
 repository.h   | 1 +
 6 files changed, 22 insertions(+), 2 deletions(-)
environment.c +4 -0
index 342400fcaad..2701dfeeec8 100644
@@ -169,6 +169,10 @@ void setup_git_env(const char *git_dir)
 	args.graft_file = getenv_safe(&to_free, GRAFT_ENVIRONMENT);
 	args.index_file = getenv_safe(&to_free, INDEX_ENVIRONMENT);
 	args.alternate_db = getenv_safe(&to_free, ALTERNATE_DB_ENVIRONMENT);
+	if (getenv(GIT_QUARANTINE_ENVIRONMENT)) {
+		args.disable_ref_updates = 1;
+	}
+
 	repo_set_gitdir(the_repository, git_dir, &args);
 	strvec_clear(&to_free);
 
object-file.c +6 -0
index 0b6a61aeaff..659ef7623ff 100644
@@ -699,6 +699,12 @@ struct object_directory *set_temporary_primary_odb(const char *dir, int will_des
 	 */
 	new_odb = xcalloc(1, sizeof(*new_odb));
 	new_odb->path = xstrdup(dir);
+
+	/*
+	 * Disable ref updates while a temporary odb is active, since
+	 * the objects in the database may roll back.
+	 */
+	new_odb->disable_ref_updates = 1;
 	new_odb->will_destroy = will_destroy;
 	new_odb->next = the_repository->objects->odb;
 	the_repository->objects->odb = new_odb;
object-store.h +8 -1
index cb173e69392..9ae9262c340 100644
@@ -27,10 +27,17 @@ struct object_directory {
 	uint32_t loose_objects_subdir_seen[8]; /* 256 bits */
 	struct oidtree *loose_objects_cache;
 
+	/*
+	 * This is a temporary object store created by the tmp_objdir
+	 * facility. Disable ref updates since the objects in the store
+	 * might be discarded on rollback.
+	 */
+	unsigned int disable_ref_updates : 1;
+
 	/*
 	 * This object store is ephemeral, so there is no need to fsync.
 	 */
-	int will_destroy;
+	unsigned int will_destroy : 1;
 
 	/*
 	 * Path to the alternative object store. If this is a relative path,
refs.c +1 -1
index d7cc0a23a3b..27ec7d1fc64 100644
@@ -2137,7 +2137,7 @@ int ref_transaction_prepare(struct ref_transaction *transaction,
 		break;
 	}
 
-	if (getenv(GIT_QUARANTINE_ENVIRONMENT)) {
+	if (the_repository->objects->odb->disable_ref_updates) {
 		strbuf_addstr(err,
 			      _("ref updates forbidden inside quarantine environment"));
 		return -1;
repository.c +2 -0
index c5b90ba93ea..dce8e35ac20 100644
@@ -80,6 +80,8 @@ void repo_set_gitdir(struct repository *repo,
 	expand_base_dir(&repo->objects->odb->path, o->object_dir,
 			repo->commondir, "objects");
 
+	repo->objects->odb->disable_ref_updates = o->disable_ref_updates;
+
 	free(repo->objects->alternate_db);
 	repo->objects->alternate_db = xstrdup_or_null(o->alternate_db);
 	expand_base_dir(&repo->graft_file, o->graft_file,
repository.h +1 -0
index a057653981c..7c04e99ac5c 100644
@@ -158,6 +158,7 @@ struct set_gitdir_args {
 	const char *graft_file;
 	const char *index_file;
 	const char *alternate_db;
+	int disable_ref_updates;
 };
 
 void repo_set_gitdir(struct repository *repo, const char *root,
Signature · 1 line
gitgitgadget
#139 lore raw

Re: [PATCH v9 2/9] tmp-objdir: disable ref updates when replacing the primary odb

object-store.h struct object_directory { · line 43

Quoted 8 lines · Neeraj Singh via GitGitGadget
object-store.h +1 -1
 	/*
 	 * This object store is ephemeral, so there is no need to fsync.
 	 */
-	int will_destroy;
+	unsigned int will_destroy : 1;
 
 	/*
 	 * Path to the alternative object store. If this is a relative path,

Why add this as an int in the preceding commit and turn it "unsigned :
1" here?

#140 lore raw

Re: [PATCH v9 2/9] tmp-objdir: disable ref updates when replacing the primary odb

Quoted 10 lines · Ævar Arnfjörð Bjarmason
Quoted 6 lines · Neeraj Singh via GitGitGadget
      /*
       * This object store is ephemeral, so there is no need to fsync.
       */
object-store.h +1 -1
-     int will_destroy;
+     unsigned int will_destroy : 1;
Quoted 5 lines
Quoted 2 lines

/*
* Path to the alternative object store. If this is a relative path,

Why add this as an int in the preceding commit and turn it "unsigned :
1" here?

Good catch. I'll fix this. This was an artifact of a previous version where there was another variable here as well.

Thanks,
Neeraj

#141 lore raw

PATCH v9 3/9 bulk-checkin: rename 'state' variable and separate 'plugged' boolean

In reply to Neeraj K. Singh via GitGitGadget

From: Neeraj Singh <[email protected]>

Preparation for adding bulk-fsync to the bulk-checkin.c infrastructure.

* Rename 'state' variable to 'bulk_checkin_state', since we will later be adding 'bulk_fsync_state'. This also makes the variable easier to find in the debugger, since the name is more unique.

* Move the 'plugged' data member of 'bulk_checkin_state' into a separate static variable. Doing this avoids resetting the variable in finish_bulk_checkin when zeroing the 'bulk_checkin_state'. As-is, we seem to unintentionally disable the plugging functionality the first time a new packfile must be created due to packfile size limits. While disabling the plugging state only results in suboptimal behavior for the current code, it would be fatal for the bulk-fsync functionality later in this patch series.

---
 bulk-checkin.c | 22 ++++++++++++----------
 1 file changed, 12 insertions(+), 10 deletions(-)
bulk-checkin.c +12 -10
index 8785b2ac806..6ae18401e04 100644
@@ -10,9 +10,9 @@
 #include "packfile.h"
 #include "object-store.h"
 
-static struct bulk_checkin_state {
-	unsigned plugged:1;
+static int bulk_checkin_plugged;
 
+static struct bulk_checkin_state {
 	char *pack_tmp_name;
 	struct hashfile *f;
 	off_t offset;
@@ -21,7 +21,7 @@ static struct bulk_checkin_state {
 	struct pack_idx_entry **written;
 	uint32_t alloc_written;
 	uint32_t nr_written;
-} state;
+} bulk_checkin_state;
 
 static void finish_tmp_packfile(struct strbuf *basename,
 				const char *pack_tmp_name,
@@ -277,21 +277,23 @@ int index_bulk_checkin(struct object_id *oid,
 		       int fd, size_t size, enum object_type type,
 		       const char *path, unsigned flags)
 {
-	int status = deflate_to_pack(&state, oid, fd, size, type,
+	int status = deflate_to_pack(&bulk_checkin_state, oid, fd, size, type,
 				     path, flags);
-	if (!state.plugged)
-		finish_bulk_checkin(&state);
+	if (!bulk_checkin_plugged)
+		finish_bulk_checkin(&bulk_checkin_state);
 	return status;
 }
 
 void plug_bulk_checkin(void)
 {
-	state.plugged = 1;
+	assert(!bulk_checkin_plugged);
+	bulk_checkin_plugged = 1;
 }
 
 void unplug_bulk_checkin(void)
 {
-	state.plugged = 0;
-	if (state.f)
-		finish_bulk_checkin(&state);
+	assert(bulk_checkin_plugged);
+	bulk_checkin_plugged = 0;
+	if (bulk_checkin_state.f)
+		finish_bulk_checkin(&bulk_checkin_state);
 }
Signature · 1 line
gitgitgadget
#142 lore raw

PATCH v9 4/9 core.fsyncobjectfiles: batched disk flushes

In reply to Neeraj K. Singh via GitGitGadget

From: Neeraj Singh <[email protected]>

When adding many objects to a repo with core.fsyncObjectFiles set to true, the cost of fsync'ing each object file can become prohibitive.

One major source of the cost of fsync is the implied flush of the hardware writeback cache within the disk drive. Fortunately, Windows, and macOS offer mechanisms to write data from the filesystem page cache without initiating a hardware flush. Linux has the sync_file_range API, which issues a pagecache writeback request reliably after version 5.2.

This patch introduces a new 'core.fsyncObjectFiles = batch' option that batches up hardware flushes. It hooks into the bulk-checkin plugging and unplugging functionality and takes advantage of tmp-objdir.

When the new mode is enabled do the following for each new object:
1. Create the object in a tmp-objdir.
2. Issue a pagecache writeback request and wait for it to complete.

At the end of the entire transaction when unplugging bulk checkin:
1. Issue an fsync against a dummy file to flush the hardware writeback cache, which should by now have processed the tmp-objdir writes.
2. Rename all of the tmp-objdir files to their final names.
3. When updating the index and/or refs, we assume that Git will issue another fsync internal to that operation. This is not the case today, but may be a good extension to those components.

On a filesystem with a singular journal that is updated during name operations (e.g. create, link, rename, etc), such as NTFS, HFS+, or XFS we would expect the fsync to trigger a journal writeout so that this sequence is enough to ensure that the user's data is durable by the time the git command returns.

This change also updates the macOS code to trigger a real hardware flush via fnctl(fd, F_FULLFSYNC) when fsync_or_die is called. Previously, on macOS there was no guarantee of durability since a simple fsync(2) call does not flush any hardware caches.

_Performance numbers_:

Linux - Hyper-V VM running Kernel 5.11 (Ubuntu 20.04) on a fast SSD. Mac - macOS 11.5.1 running on a Mac mini on a 1TB Apple SSD. Windows - Same host as Linux, a preview version of Windows 11.
This number is from a patch later in the series.

Adding 500 files to the repo with 'git add' Times reported in seconds.

core.fsyncObjectFiles | Linux | Mac   | Windows
----------------------|-------|-------|--------
                false | 0.06  |  0.35 | 0.61
                true  | 1.88  | 11.18 | 2.47
                batch | 0.15  |  0.41 | 1.53
---
 Documentation/config/core.txt | 29 +++++++++++----
 Makefile                      |  6 ++++
 bulk-checkin.c                | 68 +++++++++++++++++++++++++++++++++++
 bulk-checkin.h                |  2 ++
 cache.h                       |  8 ++++-
 config.c                      |  7 +++-
 config.mak.uname              |  1 +
 configure.ac                  |  8 +++++
 environment.c                 |  2 +-
 git-compat-util.h             |  7 ++++
 object-file.c                 | 12 ++++++-
 wrapper.c                     | 44 +++++++++++++++++++++++
 write-or-die.c                |  2 +-
 13 files changed, 185 insertions(+), 11 deletions(-)
Documentation/config/core.txt +23 -6
index c04f62a54a1..200b4d9f06e 100644
@@ -548,12 +548,29 @@ core.whitespace::
   errors. The default tab width is 8. Allowed values are 1 to 63.
 
 core.fsyncObjectFiles::
-	This boolean will enable 'fsync()' when writing object files.
-+
-This is a total waste of time and effort on a filesystem that orders
-data writes properly, but can be useful for filesystems that do not use
-journalling (traditional UNIX filesystems) or that only journal metadata
-and not file contents (OS X's HFS+, or Linux ext3 with "data=writeback").
+	A value indicating the level of effort Git will expend in
+	trying to make objects added to the repo durable in the event
+	of an unclean system shutdown. This setting currently only
+	controls loose objects in the object store, so updates to any
+	refs or the index may not be equally durable.
++
+* `false` allows data to remain in file system caches according to
+  operating system policy, whence it may be lost if the system loses power
+  or crashes.
+* `true` triggers a data integrity flush for each loose object added to the
+  object store. This is the safest setting that is likely to ensure durability
+  across all operating systems and file systems that honor the 'fsync' system
+  call. However, this setting comes with a significant performance cost on
+  common hardware. Git does not currently fsync parent directories for
+  newly-added files, so some filesystems may still allow data to be lost on
+  system crash.
+* `batch` enables an experimental mode that uses interfaces available in some
+  operating systems to write loose object data with a minimal set of FLUSH
+  CACHE (or equivalent) commands sent to the storage controller. If the
+  operating system interfaces are not available, this mode behaves the same as
+  `true`. This mode is expected to be as safe as `true` on macOS for repos
+  stored on HFS+ or APFS filesystems and on Windows for repos stored on NTFS or
+  ReFS.
 
 core.preloadIndex::
 	Enable parallel index preload for operations like 'git diff'
Makefile +6 -0
index 12be39ac497..241dc322c09 100644
@@ -406,6 +406,8 @@ all::
 #
 # Define HAVE_CLOCK_MONOTONIC if your platform has CLOCK_MONOTONIC.
 #
+# Define HAVE_SYNC_FILE_RANGE if your platform has sync_file_range.
+#
 # Define NEEDS_LIBRT if your platform requires linking with librt (glibc version
 # before 2.17) for clock_gettime and CLOCK_MONOTONIC.
 #
@@ -1884,6 +1886,10 @@ ifdef HAVE_CLOCK_MONOTONIC
 	BASIC_CFLAGS += -DHAVE_CLOCK_MONOTONIC
 endif
 
+ifdef HAVE_SYNC_FILE_RANGE
+	BASIC_CFLAGS += -DHAVE_SYNC_FILE_RANGE
+endif
+
 ifdef NEEDS_LIBRT
 	EXTLIBS += -lrt
 endif
bulk-checkin.c +68 -0

104 lines not on this page · raw message

bulk-checkin.h +2 -0
index b26f3dc3b74..08f292379b6 100644
@@ -6,6 +6,8 @@
 
 #include "cache.h"
 
+void fsync_loose_object_bulk_checkin(int fd);
+
 int index_bulk_checkin(struct object_id *oid,
 		       int fd, size_t size, enum object_type type,
 		       const char *path, unsigned flags);
cache.h +7 -1
index eba12487b99..6d6e6770ecc 100644
@@ -985,7 +985,13 @@ void reset_shared_repository(void);
 extern int read_replace_refs;
 extern char *git_replace_ref_base;
 
-extern int fsync_object_files;
+enum fsync_object_files_mode {
+    FSYNC_OBJECT_FILES_OFF,
+    FSYNC_OBJECT_FILES_ON,
+    FSYNC_OBJECT_FILES_BATCH
+};
+
+extern enum fsync_object_files_mode fsync_object_files;
 extern int core_preload_index;
 extern int precomposed_unicode;
 extern int protect_hfs;
config.c +6 -1
index c5873f3a706..5eb36ecd77a 100644
@@ -1491,7 +1491,12 @@ static int git_default_core_config(const char *var, const char *value, void *cb)
 	}
 
 	if (!strcmp(var, "core.fsyncobjectfiles")) {
-		fsync_object_files = git_config_bool(var, value);
+		if (value && !strcmp(value, "batch"))
+			fsync_object_files = FSYNC_OBJECT_FILES_BATCH;
+		else if (git_config_bool(var, value))
+			fsync_object_files = FSYNC_OBJECT_FILES_ON;
+		else
+			fsync_object_files = FSYNC_OBJECT_FILES_OFF;
 		return 0;
 	}
 
config.mak.uname +1 -0
index 3236a4918a3..5ead1377667 100644
@@ -57,6 +57,7 @@ ifeq ($(uname_S),Linux)
 	HAVE_CLOCK_MONOTONIC = YesPlease
 	# -lrt is needed for clock_gettime on glibc <= 2.16
 	NEEDS_LIBRT = YesPlease
+	HAVE_SYNC_FILE_RANGE = YesPlease
 	HAVE_GETDELIM = YesPlease
 	SANE_TEXT_GREP=-a
 	FREAD_READS_DIRECTORIES = UnfortunatelyYes
configure.ac +8 -0
index 031e8d3fee8..c711037d625 100644
@@ -1090,6 +1090,14 @@ AC_COMPILE_IFELSE([CLOCK_MONOTONIC_SRC],
 	[AC_MSG_RESULT([no])
 	HAVE_CLOCK_MONOTONIC=])
 GIT_CONF_SUBST([HAVE_CLOCK_MONOTONIC])
+
+#
+# Define HAVE_SYNC_FILE_RANGE=YesPlease if sync_file_range is available.
+GIT_CHECK_FUNC(sync_file_range,
+	[HAVE_SYNC_FILE_RANGE=YesPlease],
+	[HAVE_SYNC_FILE_RANGE])
+GIT_CONF_SUBST([HAVE_SYNC_FILE_RANGE])
+
 #
 # Define NO_SETITIMER if you don't have setitimer.
 GIT_CHECK_FUNC(setitimer,
environment.c +1 -1
index 2701dfeeec8..aeafe80235e 100644
@@ -42,7 +42,7 @@ const char *git_attributes_file;
 const char *git_hooks_path;
 int zlib_compression_level = Z_BEST_SPEED;
 int pack_compression_level = Z_DEFAULT_COMPRESSION;
-int fsync_object_files;
+enum fsync_object_files_mode fsync_object_files;
 size_t packed_git_window_size = DEFAULT_PACKED_GIT_WINDOW_SIZE;
 size_t packed_git_limit = DEFAULT_PACKED_GIT_LIMIT;
 size_t delta_base_cache_limit = 96 * 1024 * 1024;
git-compat-util.h +7 -0
index d70ce142861..4defd4ab200 100644
@@ -1214,6 +1214,13 @@ __attribute__((format (printf, 1, 2))) NORETURN
 void BUG(const char *fmt, ...);
 #endif
 
+enum fsync_action {
+    FSYNC_WRITEOUT_ONLY,
+    FSYNC_HARDWARE_FLUSH
+};
+
+int git_fsync(int fd, enum fsync_action action);
+
 /*
  * Preserves errno, prints a message, but gives no warning for ENOENT.
  * Returns 0 on success, which includes trying to unlink an object that does
object-file.c +11 -1
index 659ef7623ff..9d0aac792ae 100644
@@ -1853,8 +1853,18 @@ int hash_object_file(const struct git_hash_algo *algo, const void *buf,
 static void close_loose_object(int fd)
 {
 	if (!the_repository->objects->odb->will_destroy) {
-		if (fsync_object_files)
+		switch (fsync_object_files) {
+		case FSYNC_OBJECT_FILES_OFF:
+			break;
+		case FSYNC_OBJECT_FILES_ON:
 			fsync_or_die(fd, "loose object file");
+			break;
+		case FSYNC_OBJECT_FILES_BATCH:
+			fsync_loose_object_bulk_checkin(fd);
+			break;
+		default:
+			BUG("Invalid fsync_object_files mode.");
+		}
 	}
 
 	if (close(fd) != 0)
wrapper.c +44 -0

50 lines not on this page · raw message

write-or-die.c +1 -1
index 0b1ec8190b6..cc8291d9794 100644
@@ -57,7 +57,7 @@ void fprintf_or_die(FILE *f, const char *fmt, ...)
 
 void fsync_or_die(int fd, const char *msg)
 {
-	while (fsync(fd) < 0) {
+	while (git_fsync(fd, FSYNC_HARDWARE_FLUSH) < 0) {
 		if (errno != EINTR)
 			die_errno("fsync error on '%s'", msg);
 	}
Signature · 1 line
gitgitgadget
#143 lore raw

PATCH v9 5/9 core.fsyncobjectfiles: add windows support for batch mode

In reply to Neeraj K. Singh via GitGitGadget

From: Neeraj Singh <[email protected]>

This commit adds a win32 implementation for fsync_no_flush that is called git_fsync. The 'NtFlushBuffersFileEx' function being called is available since Windows 8. If the function is not available, we return -1 and Git falls back to doing a full fsync.

The operating system is told to flush data only without a hardware flush primitive. A later full fsync will cause the metadata log to be flushed and then the disk cache to be flushed on NTFS and ReFS. Other filesystems will treat this as a full flush operation.

I added a new file here for this system call so as not to conflict with downstream changes in the git-for-windows repository related to fscache.

---
 compat/mingw.h                      |  3 +++
 compat/win32/flush.c                | 28 ++++++++++++++++++++++++++++
 config.mak.uname                    |  2 ++
 contrib/buildsystems/CMakeLists.txt |  3 ++-
 wrapper.c                           |  4 ++++
 5 files changed, 39 insertions(+), 1 deletion(-)
 create mode 100644 compat/win32/flush.c
compat/mingw.h +3 -0
index c9a52ad64a6..6074a3d3ced 100644
@@ -329,6 +329,9 @@ int mingw_getpagesize(void);
 #define getpagesize mingw_getpagesize
 #endif
 
+int win32_fsync_no_flush(int fd);
+#define fsync_no_flush win32_fsync_no_flush
+
 struct rlimit {
 	unsigned int rlim_cur;
 };
compat/win32/flush.c new file +28 -0
new file mode 100644
index 00000000000..75324c24ee7
@@ -0,0 +1,28 @@
+#include "../../git-compat-util.h"
+#include <winternl.h>
+#include "lazyload.h"
+
+int win32_fsync_no_flush(int fd)
+{
+       IO_STATUS_BLOCK io_status;
+
+#define FLUSH_FLAGS_FILE_DATA_ONLY 1
+
+       DECLARE_PROC_ADDR(ntdll.dll, NTSTATUS, NtFlushBuffersFileEx,
+			 HANDLE FileHandle, ULONG Flags, PVOID Parameters, ULONG ParameterSize,
+			 PIO_STATUS_BLOCK IoStatusBlock);
+
+       if (!INIT_PROC_ADDR(NtFlushBuffersFileEx)) {
+		errno = ENOSYS;
+		return -1;
+       }
+
+       memset(&io_status, 0, sizeof(io_status));
+       if (NtFlushBuffersFileEx((HANDLE)_get_osfhandle(fd), FLUSH_FLAGS_FILE_DATA_ONLY,
+				NULL, 0, &io_status)) {
+		errno = EINVAL;
+		return -1;
+       }
+
+       return 0;
+}
config.mak.uname +2 -0
index 5ead1377667..5727fb093ca 100644
@@ -455,6 +455,7 @@ endif
 	CFLAGS =
 	BASIC_CFLAGS = -nologo -I. -Icompat/vcbuild/include -DWIN32 -D_CONSOLE -DHAVE_STRING_H -D_CRT_SECURE_NO_WARNINGS -D_CRT_NONSTDC_NO_DEPRECATE
 	COMPAT_OBJS = compat/msvc.o compat/winansi.o \
+		compat/win32/flush.o \
 		compat/win32/path-utils.o \
 		compat/win32/pthread.o compat/win32/syslog.o \
 		compat/win32/trace2_win32_process_info.o \
@@ -630,6 +631,7 @@ ifeq ($(uname_S),MINGW)
 	COMPAT_CFLAGS += -DSTRIP_EXTENSION=\".exe\"
 	COMPAT_OBJS += compat/mingw.o compat/winansi.o \
 		compat/win32/trace2_win32_process_info.o \
+		compat/win32/flush.o \
 		compat/win32/path-utils.o \
 		compat/win32/pthread.o compat/win32/syslog.o \
 		compat/win32/dirent.o
contrib/buildsystems/CMakeLists.txt +2 -1
index fd1399c440f..ef0c1e4976d 100644
@@ -261,7 +261,8 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
 				NOGDI OBJECT_CREATION_MODE=1 __USE_MINGW_ANSI_STDIO=0
 				USE_NED_ALLOCATOR OVERRIDE_STRDUP MMAP_PREVENTS_DELETE USE_WIN32_MMAP
 				UNICODE _UNICODE HAVE_WPGMPTR ENSURE_MSYSTEM_IS_SET)
-	list(APPEND compat_SOURCES compat/mingw.c compat/winansi.c compat/win32/path-utils.c
+	list(APPEND compat_SOURCES compat/mingw.c compat/winansi.c
+		compat/win32/flush.c compat/win32/path-utils.c
 		compat/win32/pthread.c compat/win32mmap.c compat/win32/syslog.c
 		compat/win32/trace2_win32_process_info.c compat/win32/dirent.c
 		compat/nedmalloc/nedmalloc.c compat/strdup.c)
wrapper.c +4 -0
index 689288d2e31..ece3d2ca106 100644
@@ -573,6 +573,10 @@ int git_fsync(int fd, enum fsync_action action)
 						 SYNC_FILE_RANGE_WAIT_AFTER);
 #endif
 
+#ifdef fsync_no_flush
+		return fsync_no_flush(fd);
+#endif
+
 		errno = ENOSYS;
 		return -1;
 
Signature · 1 line
gitgitgadget
#144 lore raw

PATCH v9 6/9 update-index: use the bulk-checkin infrastructure

In reply to Neeraj K. Singh via GitGitGadget

From: Neeraj Singh <[email protected]>

The update-index functionality is used internally by 'git stash push' to setup the internal stashed commit.

This change enables bulk-checkin for update-index infrastructure to speed up adding new objects to the object database by leveraging the pack functionality and the new bulk-fsync functionality.

There is some risk with this change, since under batch fsync, the object files will not be available until the update-index is entirely complete. This usage is unlikely, since any tool invoking update-index and expecting to see objects would have to synchronize with the update-index process after passing it a file path.

---
 builtin/update-index.c | 6 ++++++
 1 file changed, 6 insertions(+)
builtin/update-index.c +6 -0
index 187203e8bb5..dc7368bb1ee 100644
@@ -5,6 +5,7 @@
  */
 #define USE_THE_INDEX_COMPATIBILITY_MACROS
 #include "cache.h"
+#include "bulk-checkin.h"
 #include "config.h"
 #include "lockfile.h"
 #include "quote.h"
@@ -1088,6 +1089,9 @@ int cmd_update_index(int argc, const char **argv, const char *prefix)
 
 	the_index.updated_skipworktree = 1;
 
+	/* we might be adding many objects to the object database */
+	plug_bulk_checkin();
+
 	/*
 	 * Custom copy of parse_options() because we want to handle
 	 * filename arguments as they come.
@@ -1168,6 +1172,8 @@ int cmd_update_index(int argc, const char **argv, const char *prefix)
 		strbuf_release(&buf);
 	}
 
+	/* by now we must have added all of the new objects */
+	unplug_bulk_checkin();
 	if (split_index > 0) {
 		if (git_config_get_split_index() == 0)
 			warning(_("core.splitIndex is set to false; "
Signature · 1 line
gitgitgadget
#145 lore raw

PATCH v9 7/9 unpack-objects: use the bulk-checkin infrastructure

In reply to Neeraj K. Singh via GitGitGadget

From: Neeraj Singh <[email protected]>

The unpack-objects functionality is used by fetch, push, and fast-import to turn the transfered data into object database entries when there are fewer objects than the 'unpacklimit' setting.

By enabling bulk-checkin when unpacking objects, we can take advantage of batched fsyncs.

---
 builtin/unpack-objects.c | 3 +++
 1 file changed, 3 insertions(+)
builtin/unpack-objects.c +3 -0
index 4a9466295ba..51eb4f7b531 100644
@@ -1,5 +1,6 @@
 #include "builtin.h"
 #include "cache.h"
+#include "bulk-checkin.h"
 #include "config.h"
 #include "object-store.h"
 #include "object.h"
@@ -503,10 +504,12 @@ static void unpack_all(void)
 	if (!quiet)
 		progress = start_progress(_("Unpacking objects"), nr_objects);
 	CALLOC_ARRAY(obj_list, nr_objects);
+	plug_bulk_checkin();
 	for (i = 0; i < nr_objects; i++) {
 		unpack_one(i);
 		display_progress(progress, i + 1);
 	}
+	unplug_bulk_checkin();
 	stop_progress(&progress);
 
 	if (delta_list)
Signature · 1 line
gitgitgadget
#146 lore raw

PATCH v9 8/9 core.fsyncobjectfiles: tests for batch mode

In reply to Neeraj K. Singh via GitGitGadget

From: Neeraj Singh <[email protected]>

Add test cases to exercise batch mode for:
* 'git add'
* 'git stash'
* 'git update-index'
* 'git unpack-objects'

These tests ensure that the added data winds up in the object database.

In this change we introduce a new test helper lib-unique-files.sh. The goal of this library is to create a tree of files that have different oids from any other files that may have been created in the current test repo. This helps us avoid missing validation of an object being added due to it already being in the repo.

---
 t/lib-unique-files.sh  | 36 ++++++++++++++++++++++++++++++++++++
 t/t3700-add.sh         | 20 ++++++++++++++++++++
 t/t3903-stash.sh       | 14 ++++++++++++++
 t/t5300-pack-object.sh | 30 +++++++++++++++++++-----------
 4 files changed, 89 insertions(+), 11 deletions(-)
 create mode 100644 t/lib-unique-files.sh
t/lib-unique-files.sh new file +36 -0
new file mode 100644
index 00000000000..a7de4ca8512
@@ -0,0 +1,36 @@
+# Helper to create files with unique contents
+
+
+# Create multiple files with unique contents. Takes the number of
+# directories, the number of files in each directory, and the base
+# directory.
+#
+# test_create_unique_files 2 3 my_dir -- Creates 2 directories with 3 files
+#					 each in my_dir, all with unique
+#					 contents.
+
+test_create_unique_files() {
+	test "$#" -ne 3 && BUG "3 param"
+
+	local dirs=$1
+	local files=$2
+	local basedir=$3
+	local counter=0
+	test_tick
+	local basedata=$test_tick
+
+
+	rm -rf $basedir
+
+	for i in $(test_seq $dirs)
+	do
+		local dir=$basedir/dir$i
+
+		mkdir -p "$dir"
+		for j in $(test_seq $files)
+		do
+			counter=$((counter + 1))
+			echo "$basedata.$counter"  >"$dir/file$j.txt"
+		done
+	done
+}
t/t3700-add.sh +20 -0
index 283a66955d6..aaecefda159 100755
@@ -8,6 +8,8 @@ test_description='Test of git add, including the -- option.'
 TEST_PASSES_SANITIZE_LEAK=true
 . ./test-lib.sh
 
+. $TEST_DIRECTORY/lib-unique-files.sh
+
 # Test the file mode "$1" of the file "$2" in the index.
 test_mode_in_index () {
 	case "$(git ls-files -s "$2")" in
@@ -34,6 +36,24 @@ test_expect_success \
     'Test that "git add -- -q" works' \
     'touch -- -q && git add -- -q'
 
+test_expect_success 'git add: core.fsyncobjectfiles=batch' "
+	test_create_unique_files 2 4 fsync-files &&
+	git -c core.fsyncobjectfiles=batch add -- ./fsync-files/ &&
+	rm -f fsynced_files &&
+	git ls-files --stage fsync-files/ > fsynced_files &&
+	test_line_count = 8 fsynced_files &&
+	awk -- '{print \$2}' fsynced_files | xargs -n1 git cat-file -e
+"
+
+test_expect_success 'git update-index: core.fsyncobjectfiles=batch' "
+	test_create_unique_files 2 4 fsync-files2 &&
+	find fsync-files2 ! -type d -print | xargs git -c core.fsyncobjectfiles=batch update-index --add -- &&
+	rm -f fsynced_files2 &&
+	git ls-files --stage fsync-files2/ > fsynced_files2 &&
+	test_line_count = 8 fsynced_files2 &&
+	awk -- '{print \$2}' fsynced_files2 | xargs -n1 git cat-file -e
+"
+
 test_expect_success \
 	'git add: Test that executable bit is not used if core.filemode=0' \
 	'git config core.filemode 0 &&
t/t3903-stash.sh +14 -0
index f0a82be9de7..6324b52c874 100755
@@ -9,6 +9,7 @@ GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
 export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
 
 . ./test-lib.sh
+. $TEST_DIRECTORY/lib-unique-files.sh
 
 diff_cmp () {
 	for i in "$1" "$2"
@@ -1293,6 +1294,19 @@ test_expect_success 'stash handles skip-worktree entries nicely' '
 	git rev-parse --verify refs/stash:A.t
 '
 
+test_expect_success 'stash with core.fsyncobjectfiles=batch' "
+	test_create_unique_files 2 4 fsync-files &&
+	git -c core.fsyncobjectfiles=batch stash push -u -- ./fsync-files/ &&
+	rm -f fsynced_files &&
+
+	# The files were untracked, so use the third parent,
+	# which contains the untracked files
+	git ls-tree -r stash^3 -- ./fsync-files/ > fsynced_files &&
+	test_line_count = 8 fsynced_files &&
+	awk -- '{print \$3}' fsynced_files | xargs -n1 git cat-file -e
+"
+
+
 test_expect_success 'stash -c stash.useBuiltin=false warning ' '
 	expected="stash.useBuiltin support has been removed" &&
 
t/t5300-pack-object.sh +19 -11
index e13a8842075..38663dc1393 100755
@@ -162,23 +162,23 @@ test_expect_success 'pack-objects with bogus arguments' '
 
 check_unpack () {
 	test_when_finished "rm -rf git2" &&
-	git init --bare git2 &&
-	git -C git2 unpack-objects -n <"$1".pack &&
-	git -C git2 unpack-objects <"$1".pack &&
-	(cd .git && find objects -type f -print) |
-	while read path
-	do
-		cmp git2/$path .git/$path || {
-			echo $path differs.
-			return 1
-		}
-	done
+	git $2 init --bare git2 &&
+	(
+		git $2 -C git2 unpack-objects -n <"$1".pack &&
+		git $2 -C git2 unpack-objects <"$1".pack &&
+		git $2 -C git2 cat-file --batch-check="%(objectname)"
+	) <obj-list >current &&
+	cmp obj-list current
 }
 
 test_expect_success 'unpack without delta' '
 	check_unpack test-1-${packname_1}
 '
 
+test_expect_success 'unpack without delta (core.fsyncobjectfiles=batch)' '
+	check_unpack test-1-${packname_1} "-c core.fsyncobjectfiles=batch"
+'
+
 test_expect_success 'pack with REF_DELTA' '
 	packname_2=$(git pack-objects --progress test-2 <obj-list 2>stderr) &&
 	check_deltas stderr -gt 0
@@ -188,6 +188,10 @@ test_expect_success 'unpack with REF_DELTA' '
 	check_unpack test-2-${packname_2}
 '
 
+test_expect_success 'unpack with REF_DELTA (core.fsyncobjectfiles=batch)' '
+       check_unpack test-2-${packname_2} "-c core.fsyncobjectfiles=batch"
+'
+
 test_expect_success 'pack with OFS_DELTA' '
 	packname_3=$(git pack-objects --progress --delta-base-offset test-3 \
 			<obj-list 2>stderr) &&
@@ -198,6 +202,10 @@ test_expect_success 'unpack with OFS_DELTA' '
 	check_unpack test-3-${packname_3}
 '
 
+test_expect_success 'unpack with OFS_DELTA (core.fsyncobjectfiles=batch)' '
+       check_unpack test-3-${packname_3} "-c core.fsyncobjectfiles=batch"
+'
+
 test_expect_success 'compare delta flavors' '
 	perl -e '\''
 		defined($_ = -s $_) or die for @ARGV;
Signature · 1 line
gitgitgadget
#147 lore raw

PATCH v9 9/9 core.fsyncobjectfiles: performance tests for add and stash

In reply to Neeraj K. Singh via GitGitGadget

From: Neeraj Singh <[email protected]>

Add a basic performance test for "git add" and "git stash" of a lot of new objects with various fsync settings.

---
 t/perf/p3700-add.sh   | 43 ++++++++++++++++++++++++++++++++++++++++
 t/perf/p3900-stash.sh | 46 +++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 89 insertions(+)
 create mode 100755 t/perf/p3700-add.sh
 create mode 100755 t/perf/p3900-stash.sh
t/perf/p3700-add.sh new file +43 -0
new file mode 100755
index 00000000000..e93c08a2e70
@@ -0,0 +1,43 @@
+#!/bin/sh
+#
+# This test measures the performance of adding new files to the object database
+# and index. The test was originally added to measure the effect of the
+# core.fsyncObjectFiles=batch mode, which is why we are testing different values
+# of that setting explicitly and creating a lot of unique objects.
+
+test_description="Tests performance of add"
+
+. ./perf-lib.sh
+
+. $TEST_DIRECTORY/lib-unique-files.sh
+
+test_perf_default_repo
+test_checkout_worktree
+
+dir_count=10
+files_per_dir=50
+total_files=$((dir_count * files_per_dir))
+
+# We need to create the files each time we run the perf test, but
+# we do not want to measure the cost of creating the files, so run
+# the tet once.
+if test "${GIT_PERF_REPEAT_COUNT-1}" -ne 1
+then
+	echo "warning: Setting GIT_PERF_REPEAT_COUNT=1" >&2
+	GIT_PERF_REPEAT_COUNT=1
+fi
+
+for m in false true batch
+do
+	test_expect_success "create the files for core.fsyncObjectFiles=$m" '
+		git reset --hard &&
+		# create files across directories
+		test_create_unique_files $dir_count $files_per_dir files
+	'
+
+	test_perf "add $total_files files (core.fsyncObjectFiles=$m)" "
+		git -c core.fsyncobjectfiles=$m add files
+	"
+done
+
+test_done
t/perf/p3900-stash.sh new file +46 -0

46 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#148 lore raw

Re: [PATCH v9 0/9] Implement a batched fsync option for core.fsyncObjectFiles

In reply to Neeraj K. Singh via GitGitGadget
Quoted 8 lines · Neeraj K. Singh via GitGitGadget

* Per [2], I'm leaving the fsyncObjectFiles configuration as is with 'true', 'false', and 'batch'. This makes using old and new versions of git with 'batch' mode a little trickier, but hopefully people will generally be moving forward in versions.

[1] See
https://lore.kernel.org/git/[email protected]/
[2] https://lore.kernel.org/git/[email protected]/

I really think leaving that in-place is just being unnecessarily cavalier. There's a lot of mixed-version environments where git is deployed in, and we almost never break the configuration in this way (I think in the past always by mistake).

In this case it's easy to avoid it, and coming up with a less narrow config model[1] seems like a good idea in any case to unify the various outstanding work in this area.

More generally on this series, per the thread ending in [2] I really don't get why we have code like this:

	@@ -503,10 +504,12 @@ static void unpack_all(void)
	 	if (!quiet)
	 		progress = start_progress(_("Unpacking objects"), nr_objects);
	 	CALLOC_ARRAY(obj_list, nr_objects);
	+	plug_bulk_checkin();
	 	for (i = 0; i < nr_objects; i++) {
	 		unpack_one(i);
	 		display_progress(progress, i + 1);
	 	}
	+	unplug_bulk_checkin();
	 	stop_progress(&progress);
	 
	 	if (delta_list)

As opposed to doing an fsync on the last object we're processing. I.e. why do we need the step of intentionally making the objects unavailable in the tmp-objdir, and creating a "cookie" file to sync at the start/end, as opposed to fsyncing on the last file (which we're writing out anyway).

1. https://lore.kernel.org/git/[email protected]/
2. https://lore.kernel.org/git/[email protected]/

#149 lore raw

Re: [PATCH v9 0/9] Implement a batched fsync option for core.fsyncObjectFiles

Quoted 18 lines · Ævar Arnfjörð Bjarmason
Quoted 8 lines · Neeraj K. Singh via GitGitGadget

* Per [2], I'm leaving the fsyncObjectFiles configuration as is with 'true', 'false', and 'batch'. This makes using old and new versions of git with 'batch' mode a little trickier, but hopefully people will generally be moving forward in versions.

[1] See
https://lore.kernel.org/git/[email protected]/
[2] https://lore.kernel.org/git/[email protected]/

I really think leaving that in-place is just being unnecessarily cavalier. There's a lot of mixed-version environments where git is deployed in, and we almost never break the configuration in this way (I think in the past always by mistake).

Quoted 5 lines

In this case it's easy to avoid it, and coming up with a less narrow config model[1] seems like a good idea in any case to unify the various outstanding work in this area.

More generally on this series, per the thread ending in [2] I really

My primary goal in all of these changes is to move git-for-windows over to a default of batch fsync so that it can get closer to other platforms in performance
of 'git add' while still retaining the same level of data integrity. I'm hoping that
most end-users are just sticking to defaults here.

I'm happy to change the configuration schema again if there's a consensus from the Git
community that backwards-compatibility of the configuration is actually important to someone.

Also, if we're doing a deeper rethink of the fsync configuration (as prompted by this work and
Eric Wong's and Patrick Steinhardts work), do we want to retain a mode where we fsync some
parts of the persistent repo data but not others? If we add fsyncing of the index in addition to the refs,
I believe we would have covered all of the critical data structures that would be needed to find the
data that a user has added to the repo if they complete a series of git commands and then experience
a system crash.

Quoted 16 lines

don't get why we have code like this:

        @@ -503,10 +504,12 @@ static void unpack_all(void)
                if (!quiet)
                        progress = start_progress(_("Unpacking objects"), nr_objects);
                CALLOC_ARRAY(obj_list, nr_objects);
        +       plug_bulk_checkin();
                for (i = 0; i < nr_objects; i++) {
                        unpack_one(i);
                        display_progress(progress, i + 1);
                }
        +       unplug_bulk_checkin();
                stop_progress(&progress);

                if (delta_list)
Quoted 8 lines

As opposed to doing an fsync on the last object we're processing. I.e. why do we need the step of intentionally making the objects unavailable in the tmp-objdir, and creating a "cookie" file to sync at the start/end, as opposed to fsyncing on the last file (which we're writing out anyway).

1. https://lore.kernel.org/git/[email protected]/
2. https://lore.kernel.org/git/[email protected]/

It's important to not expose an object's final name until its contents have been fsynced
to disk. We want to ensure that wherever we crash, we won't have a loose object that
Git may later try to open where the filename doesn't match the content hash. I believe it's
okay for a given OID to be missing, since a later command could recreate it, but an object
with a wrong hash looks like it would persist until we do a git-fsck.

I thought about figuring out how to sync the last object rather than some random "cookie" file, but it wasn't clear to me how I'd figure out which object is actually last
from library code in a way that doesn't burden each command with somehow figuring
out its last object and communicating that. The 'cookie' approach seems to lead to a cleaner
interface for callers.

Thanks,
Neeraj

#150 lore raw

Re: [PATCH v9 0/9] Implement a batched fsync option for core.fsyncObjectFiles

Quoted 38 lines · Neeraj Singh
Quoted 18 lines · Ævar Arnfjörð Bjarmason
Quoted 8 lines · Neeraj K. Singh via GitGitGadget

* Per [2], I'm leaving the fsyncObjectFiles configuration as is with 'true', 'false', and 'batch'. This makes using old and new versions of git with 'batch' mode a little trickier, but hopefully people will generally be moving forward in versions.

[1] See
https://lore.kernel.org/git/[email protected]/
[2] https://lore.kernel.org/git/[email protected]/

I really think leaving that in-place is just being unnecessarily cavalier. There's a lot of mixed-version environments where git is deployed in, and we almost never break the configuration in this way (I think in the past always by mistake).

Quoted 5 lines

In this case it's easy to avoid it, and coming up with a less narrow config model[1] seems like a good idea in any case to unify the various outstanding work in this area.

More generally on this series, per the thread ending in [2] I really

My primary goal in all of these changes is to move git-for-windows over to a default of batch fsync so that it can get closer to other platforms in performance
of 'git add' while still retaining the same level of data integrity. I'm hoping that
most end-users are just sticking to defaults here.

I'm happy to change the configuration schema again if there's a consensus from the Git
community that backwards-compatibility of the configuration is actually important to someone.

Quoted 11 lines

Also, if we're doing a deeper rethink of the fsync configuration (as prompted by this work and
Eric Wong's and Patrick Steinhardts work), do we want to retain a mode where we fsync some
parts of the persistent repo data but not others? If we add fsyncing of the index in addition to the refs,
I believe we would have covered all of the critical data structures that would be needed to find the
data that a user has added to the repo if they complete a series of git commands and then experience
a system crash.

Just talking about it is how we'll find consensus, maybe you & Junio would like to keep it as-is. I don't see why we'd expose this bad edge case in configuration handling to users when it's entirely avoidable, and we're still in the design phase.

Quoted 25 lines
Quoted 16 lines

don't get why we have code like this:

        @@ -503,10 +504,12 @@ static void unpack_all(void)
                if (!quiet)
                        progress = start_progress(_("Unpacking objects"), nr_objects);
                CALLOC_ARRAY(obj_list, nr_objects);
        +       plug_bulk_checkin();
                for (i = 0; i < nr_objects; i++) {
                        unpack_one(i);
                        display_progress(progress, i + 1);
                }
        +       unplug_bulk_checkin();
                stop_progress(&progress);

                if (delta_list)
Quoted 8 lines

As opposed to doing an fsync on the last object we're processing. I.e. why do we need the step of intentionally making the objects unavailable in the tmp-objdir, and creating a "cookie" file to sync at the start/end, as opposed to fsyncing on the last file (which we're writing out anyway).

1. https://lore.kernel.org/git/[email protected]/
2. https://lore.kernel.org/git/[email protected]/

Quoted 9 lines

It's important to not expose an object's final name until its contents have been fsynced
to disk. We want to ensure that wherever we crash, we won't have a loose object that
Git may later try to open where the filename doesn't match the content hash. I believe it's
okay for a given OID to be missing, since a later command could recreate it, but an object
with a wrong hash looks like it would persist until we do a git-fsck.

Yes, we handle that rather badly, as I mentioned in some other threads, but not doing the fsync on the last object v.s. a "cookie" file right afterwards seems like a hail-mary at best, no?

Quoted 8 lines

I thought about figuring out how to sync the last object rather than some random "cookie" file, but it wasn't clear to me how I'd figure out which object is actually last
from library code in a way that doesn't burden each command with somehow figuring
out its last object and communicating that. The 'cookie' approach seems to lead to a cleaner
interface for callers.

The above quoted code is looping through nr_objects isn't it? Can't a "do fsync" be passed down to unpack_one() when we process the last loose object?

#151 lore raw

Re: [PATCH v9 0/9] Implement a batched fsync option for core.fsyncObjectFiles

Quoted 54 lines · Ævar Arnfjörð Bjarmason
Quoted 38 lines · Neeraj Singh
Quoted 18 lines · Ævar Arnfjörð Bjarmason
Quoted 8 lines · Neeraj K. Singh via GitGitGadget

* Per [2], I'm leaving the fsyncObjectFiles configuration as is with 'true', 'false', and 'batch'. This makes using old and new versions of git with 'batch' mode a little trickier, but hopefully people will generally be moving forward in versions.

[1] See
https://lore.kernel.org/git/[email protected]/
[2] https://lore.kernel.org/git/[email protected]/

I really think leaving that in-place is just being unnecessarily cavalier. There's a lot of mixed-version environments where git is deployed in, and we almost never break the configuration in this way (I think in the past always by mistake).

Quoted 5 lines

In this case it's easy to avoid it, and coming up with a less narrow config model[1] seems like a good idea in any case to unify the various outstanding work in this area.

More generally on this series, per the thread ending in [2] I really

My primary goal in all of these changes is to move git-for-windows over to a default of batch fsync so that it can get closer to other platforms in performance
of 'git add' while still retaining the same level of data integrity. I'm hoping that
most end-users are just sticking to defaults here.

I'm happy to change the configuration schema again if there's a consensus from the Git
community that backwards-compatibility of the configuration is actually important to someone.

Quoted 11 lines

Also, if we're doing a deeper rethink of the fsync configuration (as prompted by this work and
Eric Wong's and Patrick Steinhardts work), do we want to retain a mode where we fsync some
parts of the persistent repo data but not others? If we add fsyncing of the index in addition to the refs,
I believe we would have covered all of the critical data structures that would be needed to find the
data that a user has added to the repo if they complete a series of git commands and then experience
a system crash.

Quoted 4 lines

Just talking about it is how we'll find consensus, maybe you & Junio would like to keep it as-is. I don't see why we'd expose this bad edge case in configuration handling to users when it's entirely avoidable, and we're still in the design phase.

After trying to figure out an implementation, I have a new proposal, which I've shared on the other thread [1].

[1] https://lore.kernel.org/git/CANQDOdcdhfGtPg0PxpXQA5gQ4x9VknKDKCCi4HEB0Z1xgnjKzg@mail.gmail.com/

Quoted 36 lines
Quoted 26 lines
Quoted 17 lines

don't get why we have code like this:

        @@ -503,10 +504,12 @@ static void unpack_all(void)
                if (!quiet)
                        progress = start_progress(_("Unpacking objects"), nr_objects);
                CALLOC_ARRAY(obj_list, nr_objects);
        +       plug_bulk_checkin();
                for (i = 0; i < nr_objects; i++) {
                        unpack_one(i);
                        display_progress(progress, i + 1);
                }
        +       unplug_bulk_checkin();
                stop_progress(&progress);

                if (delta_list)
Quoted 8 lines

As opposed to doing an fsync on the last object we're processing. I.e. why do we need the step of intentionally making the objects unavailable in the tmp-objdir, and creating a "cookie" file to sync at the start/end, as opposed to fsyncing on the last file (which we're writing out anyway).

1. https://lore.kernel.org/git/[email protected]/
2. https://lore.kernel.org/git/[email protected]/

Quoted 9 lines

It's important to not expose an object's final name until its contents have been fsynced
to disk. We want to ensure that wherever we crash, we won't have a loose object that
Git may later try to open where the filename doesn't match the content hash. I believe it's
okay for a given OID to be missing, since a later command could recreate it, but an object
with a wrong hash looks like it would persist until we do a git-fsck.

Quoted 4 lines

Yes, we handle that rather badly, as I mentioned in some other threads, but not doing the fsync on the last object v.s. a "cookie" file right afterwards seems like a hail-mary at best, no?

I'm not quite grasping what you're saying here. Are you saying that using a dummy
file instead of one of the actual objects is less likely to produce the desired outcome
on actual filesystem implementations?

Quoted 9 lines
Quoted 8 lines

I thought about figuring out how to sync the last object rather than some random "cookie" file, but it wasn't clear to me how I'd figure out which object is actually last
from library code in a way that doesn't burden each command with somehow figuring
out its last object and communicating that. The 'cookie' approach seems to lead to a cleaner
interface for callers.

Quoted 3 lines

The above quoted code is looping through nr_objects isn't it? Can't a "do fsync" be passed down to unpack_one() when we process the last loose object?

Are you proposing that we do something different for unpack_objects versus update_index
and git-add? I was hoping to keep all of the users of the batch fsync functionality equivalent.
For the git-add workflow and update-index, we'd need to track the most recent file so that we
can go back and fsync it. I don't believe that syncing the last object composes well with the existing
implementation of those commands.

Thanks,
Neeraj

#152 lore raw

Re: [PATCH v9 0/9] Implement a batched fsync option for core.fsyncObjectFiles

On Wed, Nov 17 2021, Neeraj Singh wrote:

[Very late reply, sorry]

Quoted 61 lines
Quoted 54 lines · Ævar Arnfjörð Bjarmason
Quoted 38 lines · Neeraj Singh
Quoted 18 lines · Ævar Arnfjörð Bjarmason
Quoted 8 lines · Neeraj K. Singh via GitGitGadget

* Per [2], I'm leaving the fsyncObjectFiles configuration as is with 'true', 'false', and 'batch'. This makes using old and new versions of git with 'batch' mode a little trickier, but hopefully people will generally be moving forward in versions.

[1] See
https://lore.kernel.org/git/[email protected]/
[2] https://lore.kernel.org/git/[email protected]/

I really think leaving that in-place is just being unnecessarily cavalier. There's a lot of mixed-version environments where git is deployed in, and we almost never break the configuration in this way (I think in the past always by mistake).

Quoted 5 lines

In this case it's easy to avoid it, and coming up with a less narrow config model[1] seems like a good idea in any case to unify the various outstanding work in this area.

More generally on this series, per the thread ending in [2] I really

My primary goal in all of these changes is to move git-for-windows over to a default of batch fsync so that it can get closer to other platforms in performance
of 'git add' while still retaining the same level of data integrity. I'm hoping that
most end-users are just sticking to defaults here.

I'm happy to change the configuration schema again if there's a consensus from the Git
community that backwards-compatibility of the configuration is actually important to someone.

Quoted 11 lines

Also, if we're doing a deeper rethink of the fsync configuration (as prompted by this work and
Eric Wong's and Patrick Steinhardts work), do we want to retain a mode where we fsync some
parts of the persistent repo data but not others? If we add fsyncing of the index in addition to the refs,
I believe we would have covered all of the critical data structures that would be needed to find the
data that a user has added to the repo if they complete a series of git commands and then experience
a system crash.

Quoted 4 lines

Just talking about it is how we'll find consensus, maybe you & Junio would like to keep it as-is. I don't see why we'd expose this bad edge case in configuration handling to users when it's entirely avoidable, and we're still in the design phase.

Quoted 4 lines

After trying to figure out an implementation, I have a new proposal, which I've shared on the other thread [1].

[1] https://lore.kernel.org/git/CANQDOdcdhfGtPg0PxpXQA5gQ4x9VknKDKCCi4HEB0Z1xgnjKzg@mail.gmail.com/

This LGTM, or something simpler as Junio points out with his "too fine-grained?" comment as a follow-up. I'm honestly quite apathetic about what we end up with exactly as long as:

1. We get the people who are adding these config settings to talk & see if they make sense in combination.

2. We avoid the trap of hard dying on older versions.

Quoted 41 lines
Quoted 36 lines
Quoted 26 lines
Quoted 17 lines

don't get why we have code like this:

        @@ -503,10 +504,12 @@ static void unpack_all(void)
                if (!quiet)
                        progress = start_progress(_("Unpacking objects"), nr_objects);
                CALLOC_ARRAY(obj_list, nr_objects);
        +       plug_bulk_checkin();
                for (i = 0; i < nr_objects; i++) {
                        unpack_one(i);
                        display_progress(progress, i + 1);
                }
        +       unplug_bulk_checkin();
                stop_progress(&progress);

                if (delta_list)
Quoted 8 lines

As opposed to doing an fsync on the last object we're processing. I.e. why do we need the step of intentionally making the objects unavailable in the tmp-objdir, and creating a "cookie" file to sync at the start/end, as opposed to fsyncing on the last file (which we're writing out anyway).

1. https://lore.kernel.org/git/[email protected]/
2. https://lore.kernel.org/git/[email protected]/

Quoted 9 lines

It's important to not expose an object's final name until its contents have been fsynced
to disk. We want to ensure that wherever we crash, we won't have a loose object that
Git may later try to open where the filename doesn't match the content hash. I believe it's
okay for a given OID to be missing, since a later command could recreate it, but an object
with a wrong hash looks like it would persist until we do a git-fsck.

Quoted 4 lines

Yes, we handle that rather badly, as I mentioned in some other threads, but not doing the fsync on the last object v.s. a "cookie" file right afterwards seems like a hail-mary at best, no?

Quoted 5 lines

I'm not quite grasping what you're saying here. Are you saying that using a dummy
file instead of one of the actual objects is less likely to produce the desired outcome
on actual filesystem implementations?

[...covered below...]

Quoted 13 lines
Quoted 9 lines
Quoted 8 lines

I thought about figuring out how to sync the last object rather than some random "cookie" file, but it wasn't clear to me how I'd figure out which object is actually last
from library code in a way that doesn't burden each command with somehow figuring
out its last object and communicating that. The 'cookie' approach seems to lead to a cleaner
interface for callers.

Quoted 3 lines

The above quoted code is looping through nr_objects isn't it? Can't a "do fsync" be passed down to unpack_one() when we process the last loose object?

Quoted 9 lines

Are you proposing that we do something different for unpack_objects versus update_index
and git-add? I was hoping to keep all of the users of the batch fsync functionality equivalent.
For the git-add workflow and update-index, we'd need to track the most recent file so that we
can go back and fsync it. I don't believe that syncing the last object composes well with the existing
implementation of those commands.

There's probably cases where we need the cookie. I just mean instead of the API being (as seen above in the quoted part), pseudocode:

    # A
    bulk_checkin_start_make_cookie():
    n = 10
    for i in 1..n:
        write_nth(i, fsync: 0);
    bulk_checkin_end_commit_cookie();

To have it be:

    # B
    bulk_checkin_start(do_cookie: 0);
    n = 10
    for i in 1..n:
        write_nth(i, fsync: (i == n));
    bulk_checkin_end();

Or actually, presumably simpler as:

    # C
    all_fsync = bulk_checkin_mode() ? 0 : fsync_turned_on_in_general();
    end_fsync = bulk_checkin_mode() ? 1 : all_fsync;
    n = 10;
    for i in 1..n:
        write_nth(i, fsync: (i == n) ? end_fsync : all_fsync);

I.e. maybe there are cases where you really do need "A", but we're usually (always?) writing out N objects, and we usually know it at the same point where you'd want the plug_bulk_checkin/unplug_bulk_checkin, so just fsyncing the last object/file/ref/whatever means we don't need the whole ceremony of the cookie file.

I don't mind it per-se, but "B" and "C" just seem a lot simpler, particulary since as those examples show we'll presumably want to pass down a "do fsync?" to these in general, and we even usually have a display_progress() in there.

So doesn't just doing "B" or "C" eliminate the need for a cookie entirely?

Another advantage of that is that you'll presumably want such tracking anyway even for the case of "A".

Because as soon as you have say a batch operation of writing X objects and Y refs you'd want to track this anyway. I.e. either only fsync() on the ref write (particularly if there's just the one ref), or on the last ref, or for each ref and no object syncs. So this (like "C", except for the "do_batch" in the "end_fsync" case):

    # D
    do_batch = in_existing_bulk_checkin() ? 1 : 0;
    all_fsync = bulk_checkin_mode() ? 0 : fsync_turned_on_in_general();
    end_fsync = bulk_checkin_mode() ? do_batch : all_fsync;
    n = 10;
    for i in 1..n:
        write_nth(i, fsync: (i == n) ? end_fsync : all_fsync);

I mean, usually we'd want the "all refs", I'm just thinking of a case like "git fast-import" or other known-to-the-user batch operation.

Or, as in the case of my 4bc1fd6e394 (pack-objects: rename .idx files into place after .bitmap files, 2021-09-09) we'd want to know that we're writing all of say *.bitmap, *.rev where we currently fsync() all of them, write *.bitmap, *.rev and *.pack (not sure that one is safe) without fsync(), and then only fsync (or that and in-place move) the *.idx.

#153 lore raw

Re: [PATCH v9 0/9] Implement a batched fsync option for core.fsyncObjectFiles

Quoted 213 lines · Ævar Arnfjörð Bjarmason

On Wed, Nov 17 2021, Neeraj Singh wrote:

[Very late reply, sorry]

Quoted 61 lines
Quoted 54 lines · Ævar Arnfjörð Bjarmason
Quoted 38 lines · Neeraj Singh
Quoted 18 lines · Ævar Arnfjörð Bjarmason
Quoted 8 lines · Neeraj K. Singh via GitGitGadget

* Per [2], I'm leaving the fsyncObjectFiles configuration as is with 'true', 'false', and 'batch'. This makes using old and new versions of git with 'batch' mode a little trickier, but hopefully people will generally be moving forward in versions.

[1] See
https://lore.kernel.org/git/[email protected]/
[2] https://lore.kernel.org/git/[email protected]/

I really think leaving that in-place is just being unnecessarily cavalier. There's a lot of mixed-version environments where git is deployed in, and we almost never break the configuration in this way (I think in the past always by mistake).

Quoted 5 lines

In this case it's easy to avoid it, and coming up with a less narrow config model[1] seems like a good idea in any case to unify the various outstanding work in this area.

More generally on this series, per the thread ending in [2] I really

My primary goal in all of these changes is to move git-for-windows over to a default of batch fsync so that it can get closer to other platforms in performance
of 'git add' while still retaining the same level of data integrity. I'm hoping that
most end-users are just sticking to defaults here.

I'm happy to change the configuration schema again if there's a consensus from the Git
community that backwards-compatibility of the configuration is actually important to someone.

Quoted 11 lines

Also, if we're doing a deeper rethink of the fsync configuration (as prompted by this work and
Eric Wong's and Patrick Steinhardts work), do we want to retain a mode where we fsync some
parts of the persistent repo data but not others? If we add fsyncing of the index in addition to the refs,
I believe we would have covered all of the critical data structures that would be needed to find the
data that a user has added to the repo if they complete a series of git commands and then experience
a system crash.

Quoted 4 lines

Just talking about it is how we'll find consensus, maybe you & Junio would like to keep it as-is. I don't see why we'd expose this bad edge case in configuration handling to users when it's entirely avoidable, and we're still in the design phase.

Quoted 4 lines

After trying to figure out an implementation, I have a new proposal, which I've shared on the other thread [1].

[1] https://lore.kernel.org/git/CANQDOdcdhfGtPg0PxpXQA5gQ4x9VknKDKCCi4HEB0Z1xgnjKzg@mail.gmail.com/

This LGTM, or something simpler as Junio points out with his "too fine-grained?" comment as a follow-up. I'm honestly quite apathetic about what we end up with exactly as long as:

1. We get the people who are adding these config settings to talk & see if they make sense in combination.

2. We avoid the trap of hard dying on older versions.

Quoted 41 lines
Quoted 36 lines
Quoted 26 lines
Quoted 17 lines

don't get why we have code like this:

        @@ -503,10 +504,12 @@ static void unpack_all(void)
                if (!quiet)
                        progress = start_progress(_("Unpacking objects"), nr_objects);
                CALLOC_ARRAY(obj_list, nr_objects);
        +       plug_bulk_checkin();
                for (i = 0; i < nr_objects; i++) {
                        unpack_one(i);
                        display_progress(progress, i + 1);
                }
        +       unplug_bulk_checkin();
                stop_progress(&progress);

                if (delta_list)
Quoted 8 lines

As opposed to doing an fsync on the last object we're processing. I.e. why do we need the step of intentionally making the objects unavailable in the tmp-objdir, and creating a "cookie" file to sync at the start/end, as opposed to fsyncing on the last file (which we're writing out anyway).

1. https://lore.kernel.org/git/[email protected]/
2. https://lore.kernel.org/git/[email protected]/

Quoted 9 lines

It's important to not expose an object's final name until its contents have been fsynced
to disk. We want to ensure that wherever we crash, we won't have a loose object that
Git may later try to open where the filename doesn't match the content hash. I believe it's
okay for a given OID to be missing, since a later command could recreate it, but an object
with a wrong hash looks like it would persist until we do a git-fsck.

Quoted 4 lines

Yes, we handle that rather badly, as I mentioned in some other threads, but not doing the fsync on the last object v.s. a "cookie" file right afterwards seems like a hail-mary at best, no?

Quoted 5 lines

I'm not quite grasping what you're saying here. Are you saying that using a dummy
file instead of one of the actual objects is less likely to produce the desired outcome
on actual filesystem implementations?

[...covered below...]

Quoted 13 lines
Quoted 9 lines
Quoted 8 lines

I thought about figuring out how to sync the last object rather than some random "cookie" file, but it wasn't clear to me how I'd figure out which object is actually last
from library code in a way that doesn't burden each command with somehow figuring
out its last object and communicating that. The 'cookie' approach seems to lead to a cleaner
interface for callers.

Quoted 3 lines

The above quoted code is looping through nr_objects isn't it? Can't a "do fsync" be passed down to unpack_one() when we process the last loose object?

Quoted 9 lines

Are you proposing that we do something different for unpack_objects versus update_index
and git-add? I was hoping to keep all of the users of the batch fsync functionality equivalent.
For the git-add workflow and update-index, we'd need to track the most recent file so that we
can go back and fsync it. I don't believe that syncing the last object composes well with the existing
implementation of those commands.

There's probably cases where we need the cookie. I just mean instead of the API being (as seen above in the quoted part), pseudocode:

    # A
    bulk_checkin_start_make_cookie():
    n = 10
    for i in 1..n:
        write_nth(i, fsync: 0);
    bulk_checkin_end_commit_cookie();

To have it be:

    # B
    bulk_checkin_start(do_cookie: 0);
    n = 10
    for i in 1..n:
        write_nth(i, fsync: (i == n));
    bulk_checkin_end();

Or actually, presumably simpler as:

    # C
    all_fsync = bulk_checkin_mode() ? 0 : fsync_turned_on_in_general();
    end_fsync = bulk_checkin_mode() ? 1 : all_fsync;
    n = 10;
    for i in 1..n:
        write_nth(i, fsync: (i == n) ? end_fsync : all_fsync);

I.e. maybe there are cases where you really do need "A", but we're usually (always?) writing out N objects, and we usually know it at the same point where you'd want the plug_bulk_checkin/unplug_bulk_checkin, so just fsyncing the last object/file/ref/whatever means we don't need the whole ceremony of the cookie file.

I don't mind it per-se, but "B" and "C" just seem a lot simpler, particulary since as those examples show we'll presumably want to pass down a "do fsync?" to these in general, and we even usually have a display_progress() in there.

So doesn't just doing "B" or "C" eliminate the need for a cookie entirely?

Another advantage of that is that you'll presumably want such tracking anyway even for the case of "A".

Because as soon as you have say a batch operation of writing X objects and Y refs you'd want to track this anyway. I.e. either only fsync() on the ref write (particularly if there's just the one ref), or on the last ref, or for each ref and no object syncs. So this (like "C", except for the "do_batch" in the "end_fsync" case):

    # D
    do_batch = in_existing_bulk_checkin() ? 1 : 0;
    all_fsync = bulk_checkin_mode() ? 0 : fsync_turned_on_in_general();
    end_fsync = bulk_checkin_mode() ? do_batch : all_fsync;
    n = 10;
    for i in 1..n:
        write_nth(i, fsync: (i == n) ? end_fsync : all_fsync);

I mean, usually we'd want the "all refs", I'm just thinking of a case like "git fast-import" or other known-to-the-user batch operation.

Quoted 6 lines

Or, as in the case of my 4bc1fd6e394 (pack-objects: rename .idx files into place after .bitmap files, 2021-09-09) we'd want to know that we're writing all of say *.bitmap, *.rev where we currently fsync() all of them, write *.bitmap, *.rev and *.pack (not sure that one is safe) without fsync(), and then only fsync (or that and in-place move) the *.idx.

Replying to an old-ish E-Mail of mine with some more thought that came to mind after[1] (another recently resurrected fsync() thread).

I wonder if there's another twist on the plan outlined in [2] that would be both portable & efficient, i.e. the "slow" POSIX way to write files A..Z is to open/write/close/fsync each one, so we'll trigger a HW flush N times.

And as we've discussed, doing it just on Z will implicitly flush A..Y on common OS's in the wild, which we're taking advantage of here.

But aside from the rename() dance in[2], what do those OS's do if you write A..Z, fsync() the "fd" for Z, and then fsync A..Y (or, presumably equivalently, in reverse order: Y..A).

I'd think they'd be smart enough to know that they already implicitly flushed that data since Z was flushend, and make those fsync()'s a rather cheap noop.

But I don't know, hence the question.

If that's true then perhaps it's a path towards having our cake and eating it too in some cases?

I.e. an FS that would flush A..Y if we flush Z would do so quickly and reliably, whereas a FS that doesn't have such an optimization might be just as slow for all of A..Y, but at least it'll be safe.

1. https://lore.kernel.org/git/[email protected]/
2. https://lore.kernel.org/git/e1747ce00af7ab3170a69955b07d995d5321d6f3.1637020263.git.gitgitgadget@gmail.com/

#154 lore raw

Re: [PATCH v9 0/9] Implement a batched fsync option for core.fsyncObjectFiles

Quoted 25 lines · Ævar Arnfjörð Bjarmason

Replying to an old-ish E-Mail of mine with some more thought that came to mind after[1] (another recently resurrected fsync() thread).

I wonder if there's another twist on the plan outlined in [2] that would be both portable & efficient, i.e. the "slow" POSIX way to write files A..Z is to open/write/close/fsync each one, so we'll trigger a HW flush N times.

And as we've discussed, doing it just on Z will implicitly flush A..Y on common OS's in the wild, which we're taking advantage of here.

But aside from the rename() dance in[2], what do those OS's do if you write A..Z, fsync() the "fd" for Z, and then fsync A..Y (or, presumably equivalently, in reverse order: Y..A).

I'd think they'd be smart enough to know that they already implicitly flushed that data since Z was flushend, and make those fsync()'s a rather cheap noop.

But I don't know, hence the question.

If that's true then perhaps it's a path towards having our cake and eating it too in some cases?

Quoted 6 lines

I.e. an FS that would flush A..Y if we flush Z would do so quickly and reliably, whereas a FS that doesn't have such an optimization might be just as slow for all of A..Y, but at least it'll be safe.

1. https://lore.kernel.org/git/[email protected]/
2. https://lore.kernel.org/git/e1747ce00af7ab3170a69955b07d995d5321d6f3.1637020263.git.gitgitgadget@gmail.com/

The important angle here is that we need some way to indicate to the OS what A..Y is before we fsync on Z. I.e. the OS will cache any writes in memory until some sync-ish operation is done on *that specific file*. Syncing just 'Z' with no sync operations on A..Y doesn't indicate that A..Y would get written out. Apparently the bad old ext3 behavior was similar to what you're proposing where a sync on 'Z' would imply something about independent files.

Here's an interesting paper I recently came across that proposes the interface we'd really want, 'syncv':
https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.924.1168&rep=rep1&type=pdf.

Thanks,
Neeraj

#155 lore raw

Re: [PATCH v9 0/9] Implement a batched fsync option for core.fsyncObjectFiles

Quoted 33 lines · Neeraj Singh
Quoted 25 lines · Ævar Arnfjörð Bjarmason

Replying to an old-ish E-Mail of mine with some more thought that came to mind after[1] (another recently resurrected fsync() thread).

I wonder if there's another twist on the plan outlined in [2] that would be both portable & efficient, i.e. the "slow" POSIX way to write files A..Z is to open/write/close/fsync each one, so we'll trigger a HW flush N times.

And as we've discussed, doing it just on Z will implicitly flush A..Y on common OS's in the wild, which we're taking advantage of here.

But aside from the rename() dance in[2], what do those OS's do if you write A..Z, fsync() the "fd" for Z, and then fsync A..Y (or, presumably equivalently, in reverse order: Y..A).

I'd think they'd be smart enough to know that they already implicitly flushed that data since Z was flushend, and make those fsync()'s a rather cheap noop.

But I don't know, hence the question.

If that's true then perhaps it's a path towards having our cake and eating it too in some cases?

Quoted 6 lines

I.e. an FS that would flush A..Y if we flush Z would do so quickly and reliably, whereas a FS that doesn't have such an optimization might be just as slow for all of A..Y, but at least it'll be safe.

1. https://lore.kernel.org/git/[email protected]/
2. https://lore.kernel.org/git/e1747ce00af7ab3170a69955b07d995d5321d6f3.1637020263.git.gitgitgadget@gmail.com/

Quoted 7 lines

The important angle here is that we need some way to indicate to the OS what A..Y is before we fsync on Z. I.e. the OS will cache any writes in memory until some sync-ish operation is done on *that specific file*. Syncing just 'Z' with no sync operations on A..Y doesn't indicate that A..Y would get written out. Apparently the bad old ext3 behavior was similar to what you're proposing where a sync on 'Z' would imply something about independent files.

It's certainly starting to sound like I'm misunderstanding this whole thing, but just to clarify again I'm talking about the sort of loops mentioned upthread in my [1]. I.e. you have (to copy from that E-Mail):

    bulk_checkin_start_make_cookie():
    n = 10
    for i in 1..n:
        write_nth(i, fsync: 0);
    bulk_checkin_end_commit_cookie();

I.e. we have a "cookie" file in a given dir (where, in this example, we'd also write files A..Z). I.e. we write:

    cookie
    {A..Z}
    cookie

And then only fsync() on the "cookie" at the end, which "flushes" the A..Z updates on some FS's (again, all per my possibly-incorrect understanding).

Which is why I proposed that in many/all cases we could do this, i.e. just the same without the "cookie" file (which AFAICT isn't needed per-se, but was just added to make the API a bit simpler in not needing to modify the relevant loops):

    all_fsync = bulk_checkin_mode() ? 0 : fsync_turned_on_in_general();
    end_fsync = bulk_checkin_mode() ? 1 : all_fsync;
    n = 10;
    for i in 1..n:
        write_nth(i, fsync: (i == n) ? end_fsync : all_fsync);

I.e. we don't pay the cost of the fsync() as we're in the loop, but just for the last file, which "flushes" the rest.

So far all of that's a paraphrasing of existing exchanges, but what I was wondering now in[2] is if we add this to this last example above:

    for i in 1..n-1:
        fsync_nth(i)

Wouldn't those same OS's that are being clever about deferring the syncing of A..Z as a "batch" be clever enough to turn that (re-)syncing into a NOOP?

Of course in this case we'd need to keep the fd's open and be clever about E[MN]FILE (i.e. "Too many open..."), or do an fsync() every Nth for some reasonable Nth, e.g. somewhere in the 2^10..2^12 range.

But *if* this works it seems to me to be something we might be able to enable when "core.fsyncObjectFiles" is configured on those systems.

I.e. the implicit assumption with that configuration was that if we sync N loose objects and then update and fsync the ref that the FS would queue up the ref update after the syncing of the loose objects.

This new "cookie" (or my suggested "fsync last of N") is basically making the same assumption, just with the slight twist that some OSs/FSs are known to behave like that on a per-subdir basis, no?

Quoted 3 lines

Here's an interesting paper I recently came across that proposes the interface we'd really want, 'syncv':
https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.924.1168&rep=rep1&type=pdf.

1. https://lore.kernel.org/git/[email protected]/
2. https://lore.kernel.org/git/[email protected]/

#156 lore raw

Re: [PATCH v9 0/9] Implement a batched fsync option for core.fsyncObjectFiles

Quoted 105 lines · Ævar Arnfjörð Bjarmason
Quoted 33 lines · Neeraj Singh
Quoted 25 lines · Ævar Arnfjörð Bjarmason

Replying to an old-ish E-Mail of mine with some more thought that came to mind after[1] (another recently resurrected fsync() thread).

I wonder if there's another twist on the plan outlined in [2] that would be both portable & efficient, i.e. the "slow" POSIX way to write files A..Z is to open/write/close/fsync each one, so we'll trigger a HW flush N times.

And as we've discussed, doing it just on Z will implicitly flush A..Y on common OS's in the wild, which we're taking advantage of here.

But aside from the rename() dance in[2], what do those OS's do if you write A..Z, fsync() the "fd" for Z, and then fsync A..Y (or, presumably equivalently, in reverse order: Y..A).

I'd think they'd be smart enough to know that they already implicitly flushed that data since Z was flushend, and make those fsync()'s a rather cheap noop.

But I don't know, hence the question.

If that's true then perhaps it's a path towards having our cake and eating it too in some cases?

Quoted 6 lines

I.e. an FS that would flush A..Y if we flush Z would do so quickly and reliably, whereas a FS that doesn't have such an optimization might be just as slow for all of A..Y, but at least it'll be safe.

1. https://lore.kernel.org/git/[email protected]/
2. https://lore.kernel.org/git/e1747ce00af7ab3170a69955b07d995d5321d6f3.1637020263.git.gitgitgadget@gmail.com/

Quoted 7 lines

The important angle here is that we need some way to indicate to the OS what A..Y is before we fsync on Z. I.e. the OS will cache any writes in memory until some sync-ish operation is done on *that specific file*. Syncing just 'Z' with no sync operations on A..Y doesn't indicate that A..Y would get written out. Apparently the bad old ext3 behavior was similar to what you're proposing where a sync on 'Z' would imply something about independent files.

It's certainly starting to sound like I'm misunderstanding this whole thing, but just to clarify again I'm talking about the sort of loops mentioned upthread in my [1]. I.e. you have (to copy from that E-Mail):

    bulk_checkin_start_make_cookie():
    n = 10
    for i in 1..n:
        write_nth(i, fsync: 0);
    bulk_checkin_end_commit_cookie();

I.e. we have a "cookie" file in a given dir (where, in this example, we'd also write files A..Z). I.e. we write:

    cookie
    {A..Z}
    cookie

And then only fsync() on the "cookie" at the end, which "flushes" the A..Z updates on some FS's (again, all per my possibly-incorrect understanding).

Which is why I proposed that in many/all cases we could do this, i.e. just the same without the "cookie" file (which AFAICT isn't needed per-se, but was just added to make the API a bit simpler in not needing to modify the relevant loops):

    all_fsync = bulk_checkin_mode() ? 0 : fsync_turned_on_in_general();
    end_fsync = bulk_checkin_mode() ? 1 : all_fsync;
    n = 10;
    for i in 1..n:
        write_nth(i, fsync: (i == n) ? end_fsync : all_fsync);

I.e. we don't pay the cost of the fsync() as we're in the loop, but just for the last file, which "flushes" the rest.

So far all of that's a paraphrasing of existing exchanges, but what I was wondering now in[2] is if we add this to this last example above:

    for i in 1..n-1:
        fsync_nth(i)

Wouldn't those same OS's that are being clever about deferring the syncing of A..Z as a "batch" be clever enough to turn that (re-)syncing into a NOOP?

Of course in this case we'd need to keep the fd's open and be clever about E[MN]FILE (i.e. "Too many open..."), or do an fsync() every Nth for some reasonable Nth, e.g. somewhere in the 2^10..2^12 range.

But *if* this works it seems to me to be something we might be able to enable when "core.fsyncObjectFiles" is configured on those systems.

I.e. the implicit assumption with that configuration was that if we sync N loose objects and then update and fsync the ref that the FS would queue up the ref update after the syncing of the loose objects.

This new "cookie" (or my suggested "fsync last of N") is basically making the same assumption, just with the slight twist that some OSs/FSs are known to behave like that on a per-subdir basis, no?

Quoted 6 lines
Quoted 3 lines

Here's an interesting paper I recently came across that proposes the interface we'd really want, 'syncv':
https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.924.1168&rep=rep1&type=pdf.

1. https://lore.kernel.org/git/[email protected]/
2. https://lore.kernel.org/git/[email protected]/

On the actual FS implementations in the three common OSes I'm familiar with (macOS, Windows, Linux), each file has its own independent data caching in OS memory. Fsyncing one of them doesn't necessarily imply writing out the OS cache for
any other file. Except, apparently, on ext3 in data=ordered mode, but that FS is no
longer common. On Linux, we use sync_file_range to get the OS to write the in-memory
cache to the storage hardware, which is what makes the data 'available' to fsync.

Now, we could consider an implementation where we call sync_file_range without the
wait flags (i.e. without SYNC_FILE_RANGE_WAIT_BEFORE and SYNC_FILE_RANGE_WAIT_AFTER). Then we could later fsync every file (or batch of files), which might be more efficient if the OS coalesces the disk cache flushes. I expect
that this method is less likely to give us the desired performance on common linux FSes,
however.

The macOS and Windows APIs are defined a bit differently from Linux. In both those OSes,
we're actually calling fsync-equivalent APIs that are defined to write back all the relevant data and
metadata, just without the storage cache flush.

So to summarize:
1. We need to do write(2) to get the data out of Git and into the OS filesystem cache.
2. We need some API (macOS-fsync, Windows-NtFlushBuffersFileEx, Linux-sync_file_range)
to transfer the data per-file to the storage controller, but
without flushing the storage controller.
3. We need some api (macOS-F_FULLFSYNC, Windows-NtFlushBuffersFile, Linux-fsync) to push the storage controller cache to durable media. This only
needs to be done once
at the end to push out the data made available in step (2).

Thanks,
Neeraj

#157 lore raw

RE: [PATCH v9 0/9] Implement a batched fsync option for core.fsyncObjectFiles

Quoted 143 lines · Neeraj Singh
Quoted 5 lines · Ævar Arnfjörð Bjarmason
Quoted 1 line · Neeraj Singh

On Wed, Mar 9, 2022 at 3:10 PM Ævar Arnfjörð Bjarmason

Quoted 95 lines · <[email protected]> wrote:
Quoted 35 lines
Quoted 29 lines

Replying to an old-ish E-Mail of mine with some more thought that came to mind after[1] (another recently resurrected fsync() thread).

I wonder if there's another twist on the plan outlined in [2] that would be both portable & efficient, i.e. the "slow" POSIX way to write files A..Z is to open/write/close/fsync each one, so we'll trigger a HW flush N times.

And as we've discussed, doing it just on Z will implicitly flush A..Y on common OS's in the wild, which we're taking advantage of here.

But aside from the rename() dance in[2], what do those OS's do if you write A..Z, fsync() the "fd" for Z, and then fsync A..Y (or, presumably equivalently, in reverse order: Y..A).

I'd think they'd be smart enough to know that they already implicitly flushed that data since Z was flushend, and make those fsync()'s a rather cheap noop.

But I don't know, hence the question.

If that's true then perhaps it's a path towards having our cake and eating it too in some cases?

I.e. an FS that would flush A..Y if we flush Z would do so quickly and reliably, whereas a FS that doesn't have such an optimization might be just as slow for all of A..Y, but at least it'll be safe.

Quoted 5 lines

1.
https://lore.kernel.org/git/[email protected]
.com/ 2.
https://lore.kernel.org/git/e1747ce00af7ab3170a69955b07d995d5321d6f
[email protected]/

Quoted 7 lines

The important angle here is that we need some way to indicate to the OS what A..Y is before we fsync on Z. I.e. the OS will cache any writes in memory until some sync-ish operation is done on *that specific file*. Syncing just 'Z' with no sync operations on A..Y doesn't indicate that A..Y would get written out. Apparently the bad old ext3 behavior was similar to what you're proposing where a sync on 'Z' would imply something about independent files.

It's certainly starting to sound like I'm misunderstanding this whole thing, but just to clarify again I'm talking about the sort of loops mentioned upthread in my [1]. I.e. you have (to copy from that E-Mail):

    bulk_checkin_start_make_cookie():
    n = 10
    for i in 1..n:
        write_nth(i, fsync: 0);
    bulk_checkin_end_commit_cookie();

I.e. we have a "cookie" file in a given dir (where, in this example, we'd also write files A..Z). I.e. we write:

    cookie
    {A..Z}
    cookie

And then only fsync() on the "cookie" at the end, which "flushes" the A..Z updates on some FS's (again, all per my possibly-incorrect understanding).

Which is why I proposed that in many/all cases we could do this, i.e. just the same without the "cookie" file (which AFAICT isn't needed per-se, but was just added to make the API a bit simpler in not needing to modify the relevant loops):

    all_fsync = bulk_checkin_mode() ? 0 : fsync_turned_on_in_general();
    end_fsync = bulk_checkin_mode() ? 1 : all_fsync;
    n = 10;
    for i in 1..n:
        write_nth(i, fsync: (i == n) ? end_fsync : all_fsync);

I.e. we don't pay the cost of the fsync() as we're in the loop, but just for the last file, which "flushes" the rest.

So far all of that's a paraphrasing of existing exchanges, but what I was wondering now in[2] is if we add this to this last example above:

    for i in 1..n-1:
        fsync_nth(i)

Wouldn't those same OS's that are being clever about deferring the syncing of A..Z as a "batch" be clever enough to turn that (re-)syncing into a NOOP?

Of course in this case we'd need to keep the fd's open and be clever about E[MN]FILE (i.e. "Too many open..."), or do an fsync() every Nth for some reasonable Nth, e.g. somewhere in the 2^10..2^12 range.

But *if* this works it seems to me to be something we might be able to enable when "core.fsyncObjectFiles" is configured on those systems.

Quoted 11 lines

I.e. the implicit assumption with that configuration was that if we sync N loose objects and then update and fsync the ref that the FS would queue up the ref update after the syncing of the loose objects.

This new "cookie" (or my suggested "fsync last of N") is basically making the same assumption, just with the slight twist that some OSs/FSs are known to behave like that on a per-subdir basis, no?

Quoted 3 lines

Here's an interesting paper I recently came across that proposes the interface we'd really want, 'syncv':

https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.924.1168&rep=rep1
&type=pdf.

Quoted 6 lines

1.
https://lore.kernel.org/git/[email protected]
m/ 2.
https://lore.kernel.org/git/[email protected]
m/

On the actual FS implementations in the three common OSes I'm familiar with (macOS, Windows, Linux), each file has its own independent data caching in OS memory. Fsyncing one of them doesn't necessarily imply writing out the OS cache for any other file. Except, apparently, on ext3 in data=ordered mode, but that FS is no longer common. On Linux, we use sync_file_range to get the OS to write the in-memory cache to the storage hardware, which is what makes the data 'available' to fsync.

Now, we could consider an implementation where we call sync_file_range without the wait flags (i.e. without SYNC_FILE_RANGE_WAIT_BEFORE and SYNC_FILE_RANGE_WAIT_AFTER). Then we could later fsync every file (or batch of files), which might be more efficient if the OS coalesces the disk cache flushes. I expect that this method is less likely to give us the desired performance on common linux FSes, however.

The macOS and Windows APIs are defined a bit differently from Linux. In both those OSes,
we're actually calling fsync-equivalent APIs that are defined to write back all the relevant data and metadata, just without the storage cache flush.

Quoted 12 lines

So to summarize:
1. We need to do write(2) to get the data out of Git and into the OS filesystem cache.
2. We need some API (macOS-fsync, Windows-NtFlushBuffersFileEx, Linux-sync_file_range)
to transfer the data per-file to the storage controller, but without flushing the
storage controller.
3. We need some api (macOS-F_FULLFSYNC, Windows-NtFlushBuffersFile, Linux- fsync)
to push the storage controller cache to durable media. This only needs to be
done once
at the end to push out the data made available in step (2).

While this might not be a surprise, on some platforms fsync is a thread-blocking operation. When the OS has kernel threads, fsync can potentially cause multiple processes (if implemented that way) to block, particularly where an fd is shared across threads (and thus processes), which may end up causing a deadlock. We might need to keep an eye out for this type of situation in the future and at least try to test for it. I cannot actually see a situation where this would occur in git, but that does not mean it is impossible. Food for thought. --Randall

#158 lore raw

Re: [PATCH v9 0/9] Implement a batched fsync option for core.fsyncObjectFiles

Quoted 2 lines · On Thu, Mar 10, 2022 at 10:08 AM <[email protected]> wrote:

While this might not be a surprise, on some platforms fsync is a thread-blocking operation. When the OS has kernel threads, fsync can potentially cause multiple processes (if implemented that way) to block, particularly where an fd is shared across threads (and thus processes), which may end up causing a deadlock. We might need to keep an eye out for this type of situation in the future and at least try to test for it. I cannot actually see a situation where this would occur in git, but that does not mean it is impossible. Food for thought. --Randall

fsync is expected to block the calling thread until the underlying data is durable. Unless the OS somehow depends on the git process to make progress before fsync can complete, there should be no deadlock, since there would be no cycle in the waiting graph. This could be a problem for FUSE implementations that are backed by Git, but they already have to deal with that possiblity today and this patch series doesn't change anything.

#159 lore raw

RE: [PATCH v9 0/9] Implement a batched fsync option for core.fsyncObjectFiles

Quoted 8 lines · Neeraj Singh
Quoted 1 line · On Thu, Mar 10, 2022 at 10:08 AM <[email protected]> wrote:

While this might not be a surprise, on some platforms fsync is a thread-blocking

operation. When the OS has kernel threads, fsync can potentially cause multiple processes (if implemented that way) to block, particularly where an fd is shared across threads (and thus processes), which may end up causing a deadlock. We might need to keep an eye out for this type of situation in the future and at least try to test for it. I cannot actually see a situation where this would occur in git, but that does not mean it is impossible. Food for thought.

Quoted 8 lines
Quoted 1 line

--Randall

fsync is expected to block the calling thread until the underlying data is durable. Unless the OS somehow depends on the git process to make progress before fsync can complete, there should be no deadlock, since there would be no cycle in the waiting graph. This could be a problem for FUSE implementations that are backed by Git, but they already have to deal with that possiblity today and this patch series doesn't change anything.

That assumption is based on a specific threading model. In cooperative user-thread models, fsync is process-blocking. While fsync, by spec is required to block the thread, there are no limitations on blocking everything else. In some systems, an fsync can block the entire file system. Just pointing that out.