git/list[1] front-page[2] threads[3] people[4] search[5] about
 

Re: [PATCH 1/8] t: fix races caused by background maintenance

From
SHStefan Haller <lists@haller-berlin.de>
Date
Aug 10, 2026, 10:45 UTC
Message-ID
<4f6a96ac-d993-4872-b3c4-30d899f61ca9@haller-berlin.de>
In-Reply-To
<anmNX-WVohAyjEcc@pks.im>
On 10.08.26 10:35, Patrick Steinhardt wrote:
Show 11 quoted lines
> On Mon, Aug 10, 2026 at 09:37:01AM +0200, Stefan Haller wrote:
>
>> On 10.08.26 07:20, Patrick Steinhardt wrote:
>>
>> I had cases where the commit fails with
>>
>>   error: invalid object 100644 50d5612... for 'file09.txt'
>>   error: Error building trees
> 
> That's a bug then that we ought to fix. Do you maybe have a reproducer
> for this?

I had my friendly AI agent dig into this more, and below is what it came up with. Disclaimer: I only skimmed it, and didn't check it for accuracy; I'm way too unfamiliar with any of this stuff. I hope this is of some use.

----------
Subject: [BUG] geometric-repack --auto fires on tiny repos, and the
 resulting background repacks race concurrent commands

Since v2.54.0, `git commit` in an ordinary small repository can leave a detached `git repack` running behind it, and it does so far more often than intended: the auto condition that is documented as "at least 100 loose objects" is in practice satisfied by a repository containing four objects. The background repacks that result break concurrent git commands at a rate of roughly one in a thousand commits in my testing.

There are two separate issues here. The first is a plain bug; the second is its fallout.

1. The geometric-repack auto condition triggers ~256x too eagerly
----------------------------------------------------------------
Documentation/config/maintenance.adoc says:
    maintenance.geometric-repack.auto::
            [...] a positive value implies the command should run either
            when there are packfiles that need to be merged together to
            retain the geometric progression, or when there are at least
            this many loose objects that would be written into a new
            packfile. The default value is 100.
Reproducer, on a repository with four objects (v2.54.0 and v2.55.0):
    git init repro && cd repro
    git config maintenance.auto false   # so that the setup itself does
                                        # not fork any maintenance
    echo 263 >a.txt                     # blob 175b6c5d...
    echo 410 >b.txt                     # blob 17e344e7...
    git add . && git commit -m initial
    $ find .git/objects -type f
    .git/objects/17/5b6c5dfd7f9bf6e2b2c4e2dcf3e2341298575d
    .git/objects/17/e344e7c08441fa81d5b56c21008dc0feeeaa20
    .git/objects/70/a9a72c46fc85c654e626f54e77c4946950da52
    .git/objects/7f/174478a536fc641c735e6b6e1a1944c868ba0c
    $ git maintenance run --auto --no-detach
    $ ls .git/objects/pack/
    multi-pack-index
    pack-f826a2dd....idx
    pack-f826a2dd....pack
    pack-f826a2dd....rev

(only the two blob IDs are fixed; the tree and commit depend on the commit timestamp.) Committing only a.txt -- three objects, one of them in objects/17 -- does not trigger the repack. Adding the second blob whose object ID starts with "17" does. That is the whole condition.

geometric_repack_auto_condition() (builtin/gc.c) passes its threshold to too_many_loose_objects(), which does not count loose objects: it counts the entries of .git/objects/17 and scales by 256. In v2.54.0:

    int auto_threshold = DIV_ROUND_UP(limit, 256);
    [...]
            if (++num_loose > auto_threshold) {
and equivalently after the rewrite in v2.55.0:
    /*
     * This is weird, but stems from legacy behaviour: [...]
     */
    int auto_threshold = DIV_ROUND_UP(limit, 256) * 256;
    [...]
    return loose_count > auto_threshold;

with loose_count coming from ODB_COUNT_OBJECTS_APPROXIMATE, i.e. the same one-directory estimate. Either way, any limit <= 256 collapses to "two or more objects share the objects/17 directory".

That estimator is fine for gc.auto, whose default of 6700 needs 27 entries in that directory -- a number you only reach with thousands of objects, and whose documentation says "approximately". It falls apart for a threshold below 256, where the smallest representable estimate step exceeds the threshold itself and a single fanout collision decides the outcome. For a repository with n objects the condition is satisfied with probability ~1-(1-p)^n-np(1-p)^(n-1), p=1/256: about 5% at 90 objects, and much higher for repositories that accumulate objects over time.

By contrast maintenance.loose-objects.auto, documented in the same terms, is implemented as an exact count in loose_object_auto_condition().

Since 452b12c2e0 (builtin/maintenance: use "geometric" strategy by default, 2026-02-24) this condition governs what `git maintenance run --auto` does, and run_auto_maintenance() invokes that from commit, am, fetch, merge and rebase, with --detach unless gc.autoDetach says otherwise. So the practical effect is: once any two objects in a repository collide in objects/17 -- and the condition only becomes easier to satisfy as the repository grows -- every subsequent commit forks a detached repack.

Measured on a fixture repository built by 40 commits of "write a file, git add, git commit": a background repack fired in 4 of 25 runs (the prediction from the 40 nondeterministic commit object IDs alone is 1-(255/256)^40 = 14%).

2. The resulting background repacks break concurrent commands
-------------------------------------------------------------

`git repack -d` installs the new pack, removes the redundant ones and then calls prune_packed_objects(), which unlinks the loose copies of objects that are now packed and rmdir()s the fanout directories it empties. Doing that concurrently with unrelated git processes in the same repository is new exposure: before v2.54.0 the same repositories never reached the gc.auto threshold and no such repack ever ran.

Stress test: 24 concurrent workers, each repeatedly creating a fresh repository and running 40 iterations of "write a file, git add, git commit", with maintenance.geometric-repack.auto=-1 so that the condition fires on every commit rather than in ~15% of repositories. Nothing else touches the repositories; the only concurrency is git's own detached auto-maintenance.

109280 commits produced 97 failing commands, i.e. one per ~1100 commits. Message lines across those 97 failures (one failure often emits two of them, e.g. a temporary-file error followed by a fatal):

      71  error: packfile .../pack-....pack index unavailable
      50  fatal: failed to write commit object
      37  error: unable to create temporary file: Invalid argument
      23  error: unable to create temporary file: No such file or directory
      10  error: unable to index file '...'
      10  fatal: adding files failed
At least the ENOENT variant has a straightforward explanation:
  - The writer, in create_tmpfile() (object-file.c), calls
    mkstemp(".git/objects/XX/tmp_obj_XXXXXX").  On ENOENT it mkdir()s
    the fanout directory and retries -- exactly once.
  - The repack, in prune_subdir() (prune-packed.c), rmdir()s each fanout
    directory after pruning its contents.

An rmdir() landing between the writer's mkdir() and its retried mkstemp() defeats the single retry, and the command aborts. The writer retries a bounded number of times against interference that is not bounded.

(For anyone hitting this meanwhile: setting maintenance.auto=false makes it go away, since it stops the fork at the source. We have done that in lazygit's test configuration.)

3. Where this came from
-----------------------

lazygit's integration tests started failing intermittently on CI, always and only in the job running the newest git (2.54.0 on GitHub Actions ubuntu-latest), never in the jobs pinned to 2.32.0/2.38.2/2.44.0, and always during test fixture setup, which is a plain loop of "write a file, git add, git commit":

    error: invalid object 100644 50d561270fcfcdc9afc85f6136f937c529accaaa for 'file09.txt'
    error: invalid object 100644 50d561270fcfcdc9afc85f6136f937c529accaaa for 'file09.txt'
    error: Error building trees

(the first message really is printed twice), i.e. the cache-tree existence check in update_one() (cache-tree.c) failing to find a blob that a previous iteration had successfully committed. The hash is the correct blob for the file's content, so `git add` had staged it fine.

Those fixture repositories are exactly the case in section 1: they contain one object in objects/17 by construction, so a single commit object hashing into the same directory tips them over, and every following commit forks a repack. The affected tests were, without exception, the ones whose fixtures build six or more commits.

I could not reproduce this specific failure outside Linux, and I have not isolated the window that produces it. A fresh process is robust (7200 lookups against a repack loop, no misses), and so is a `git commit` whose repack completes mid-flight -- the flag the cache-tree check passes, ODB_HAS_OBJECT_RECHECK_PACKED, makes a missing lookup reprepare the pack list and retry, and that recovery does work in everything I could construct deliberately. So I am reporting this as the symptom that led to the above rather than as a diagnosed bug, in case it is familiar to someone.

One observation from reading that retry path, which may or may not be related: packfile_store_reprepare() refreshes the pack list but never reloads the multi-pack-index, because prepare_multi_pack_index_one() (midx.c) returns early when one is already loaded. A long-lived process therefore keeps a midx that may reference packs a concurrent repack has since deleted, for its entire lifetime.

Environment -----------

Original failures: git 2.54.0, GitHub Actions ubuntu-latest. Reproducers and measurements above: git 2.55.0 (built from source), macOS 26, APFS. Code references are to v2.54.0 unless stated; I checked that the behaviour is unchanged in v2.55.0.

Thanks, Stefan

Previous: Patrick SteinhardtNext: Patrick Steinhardt
Message 8 of 38 in “builtin/maintenance: use "geometric" strategy by default”
  1. 0/8 builtin/maintenance: use "geometric" strategy by defaultPatrick Steinhardt, Feb 20, 2026
  2. 1/8 t: fix races caused by background maintenancePatrick Steinhardt, Feb 20, 2026
  3. Justin ToblerFeb 23, 2026
  4. Stefan HallerAug 10, 2026
  5. Patrick SteinhardtAug 10, 2026
  6. Stefan HallerAug 10, 2026
  7. Patrick SteinhardtAug 10, 2026
  8. Stefan HallerAug 10, 2026
  9. Patrick SteinhardtAug 10, 2026
  10. Stefan HallerAug 10, 2026
  11. 2/8 t: disable maintenance where we verify object database structurePatrick Steinhardt, Feb 20, 2026
  12. Justin ToblerFeb 23, 2026
  13. 3/8 t34xx: don't expire reflogs where it mattersPatrick Steinhardt, Feb 20, 2026
  14. Derrick StoleeFeb 23, 2026
  15. Justin ToblerFeb 23, 2026
  16. 4/8 t5400: explicitly use "gc" strategyPatrick Steinhardt, Feb 20, 2026
  17. 5/8 t5510: explicitly use "gc" strategyPatrick Steinhardt, Feb 20, 2026
  18. 6/8 t6500: explicitly use "gc" strategyPatrick Steinhardt, Feb 20, 2026
  19. 7/8 t7900: prepare for switch of the default strategyPatrick Steinhardt, Feb 20, 2026
  20. 8/8 builtin/maintenance: use "geometric" strategy by defaultPatrick Steinhardt, Feb 20, 2026
  21. Derrick StoleeFeb 23, 2026
  22. Patrick SteinhardtFeb 23, 2026
  23. Justin ToblerFeb 23, 2026
  24. Patrick SteinhardtFeb 24, 2026
  25. Derrick StoleeFeb 23, 2026
  26. 0/8 builtin/maintenance: use "geometric" strategy by defaultPatrick Steinhardt, Feb 24, 2026
  27. 1/8 t: fix races caused by background maintenancePatrick Steinhardt, Feb 24, 2026
  28. 2/8 t: disable maintenance where we verify object database structurePatrick Steinhardt, Feb 24, 2026
  29. 3/8 t34xx: don't expire reflogs where it mattersPatrick Steinhardt, Feb 24, 2026
  30. 4/8 t5400: explicitly use "gc" strategyPatrick Steinhardt, Feb 24, 2026
  31. 5/8 t5510: explicitly use "gc" strategyPatrick Steinhardt, Feb 24, 2026
  32. 6/8 t6500: explicitly use "gc" strategyPatrick Steinhardt, Feb 24, 2026
  33. Toon ClaesFeb 25, 2026
  34. 7/8 t7900: prepare for switch of the default strategyPatrick Steinhardt, Feb 24, 2026
  35. 8/8 builtin/maintenance: use "geometric" strategy by defaultPatrick Steinhardt, Feb 24, 2026
  36. Derrick StoleeFeb 24, 2026
  37. Toon ClaesFeb 25, 2026
  38. Justin ToblerFeb 24, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.