Skip to content

pr-2214/thomasbachem/rerere-gc-lock-v1

tagged this 02 Sep 08:31
From: Thomas Bachem <mail@thomasbachem.com>

Since 2.54 unscheduled maintenance uses the "geometric" strategy, so
the "git maintenance run --auto --detach" behind every "git commit"
runs "git rerere gc" in the background whenever rr-cache has an entry.
That includes the "git commit" the sequencer runs for a resolved pick
on "git rebase --continue".

rerere_gc() takes MERGE_RR.lock through setup_rerere(), which uses
LOCK_DIE_ON_ERROR, and so does the sequencer's repo_rerere() at the
next conflict a few milliseconds later. Whichever comes second dies.
When it is the rebase, it dies in do_pick_commit() with the index
written but before make_patch() writes rebase-merge/{message,patch,
stopped-sha}, and every later "git rebase --continue" refuses with
"you have staged changes in your working tree". When it is the "git
commit" of a later continue, that one dies in its post-commit
repo_rerere() after the commit was made. Before 2.54 the same
collision needed an auto gc to actually run, since gc runs
"rerere gc" at its end.

A rebase with two conflicts in a row shows it. The filler makes the
pick slower than the ~5 ms the background task needs to take the
lock, and keeps the lock held for about 0.4 s. It hit 6 of 6 runs
here on 2.55.0, and a test suite driving rebases on toy repositories
with a single rr-cache entry hit it in both runs that were traced:

    git init -q -b main r && cd r
    git config rerere.enabled true
    git config maintenance.auto false
    mkdir pad && seq 20000 | (cd pad && split -l 1 -a 5)
    echo base >f && git add -A && git commit -qm base
    git checkout -q -b topic
    echo b >f && git commit -qam B
    echo c >f && git commit -qam C
    git checkout -q main
    echo a >f && git commit -qam A
    git repack -adq
    seq 20000 | awk '{printf ".git/rr-cache/%040x\n", $1}' \
        | xargs mkdir -p
    for d in .git/rr-cache/*/; do echo x >$d/preimage; done
    git config --unset maintenance.auto
    git checkout -q topic
    git rebase main
    echo ab >f && git add f
    GIT_EDITOR=true git rebase --continue

The second continue dies with "Unable to create '.git/MERGE_RR.lock':
File exists" while the gc spawned by its own commit holds the lock,
and after resolving C every further continue refuses. Maintenance
stays off during the setup so that no repack is pending: a repack due
at that commit runs ahead of rerere-gc in the task list and would
spend the window.

The gc needs the lock: it removes every rr-cache directory it finds
empty, and a rerere that has just created its directory but not yet
written the preimage looks exactly like that. So keep the lock and fix
both orders. When the gc finds the lock busy, let it warn and do
nothing this time, the way "maintenance run" treats its own lock, so a
manual "git rerere gc" sees the warning and the maintenance task and
"git gc" see a clean exit. When the gc holds the lock, let every other
caller wait it out instead of dying at once, for rerere.lockTimeout
milliseconds with the semantics of core.packedRefsTimeout: 1000 by
default, 0 for the old behaviour, -1 for an unbounded wait. Walking a
20000-entry rr-cache takes about 0.4 s here.

That rebase now completes. The tests cover the gc under a held lock,
directly and through the maintenance task, a merge that waits a lock
out within a five second rerere.lockTimeout, and one that fails at
once with a timeout of 0.

Assisted-by: Claude Fable 5.1
Signed-off-by: Thomas Bachem <mail@thomasbachem.com>

Submitted-As: https://lore.kernel.org/git/pull.2214.git.1788337897490.gitgitgadget@gmail.com
Assets 2
Loading