Parallel steps invoking git collide on index.lock, with none of the tolerance the stash path got #1139
Unanswered
k0pernikus
asked this question in
Troubleshooting and Bug Reports
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
hk schedules steps in parallel whose
fixcommands write the git index, and they collide on.git/index.lock. The contention is hk's own — no external process, no stale lock, nothing planted.Two things follow, and the second matters even if you disagree with the first:
added a bounded wait because a transient
index.lockwas judged worth surviving there. A stepcommand gets none.
Grouppergit-invoking step serialises them and the collision disappears.
Reproduction
Steps with disjoint globs, each invoking git — here directly, so the call is visible in the config
itself:
Each step sleeps a random 10–149 ms before its git call, so nothing synchronises them; the overlap comes
from scheduling. Staging the triggers and running
hk run pre-commit:hk exits 128, git's own code from the child process — so this is not the stash path, and
run_git_stashis not involved.Repository, seven scenarios, one CI job each:
k0pernikus/hk-partial-commit-index-lock-repro
git update-indexgit addGroupeachByte-for-byte identical steps in the last two rows — same script, same random delays — differing only in
the grouping. The hit rate tracks core count, which is what a scheduling race should look like.
The same repository pins the behaviours that are already correct, so this is not confused with them: a
lock released inside the 775 ms window recovers, a lock nothing ever releases still aborts (deliberate,
per #1060), and a config with only one git-invoking step showed no collisions in 8 runs — the race
needs two or more.
Why I think the contention is hk's to avoid
declared file, so two steps that both write it look independent — yet for a git hook runner it is the
resource steps are most likely to share.
stage = "…"goes throughadd()(
src/git.rs:1487) behind hk'sRepomutex and never races. The guarantee hk maintains internallysimply stops at the process boundary.
git stash pushprecisely because a brief
index.lockis survivable. The identical contention in a step command getsnothing:
add()has no retry, and a step's own git has none either, so an overlap of a fewmilliseconds is fatal.
That last point is the crux, and it needs no inference about what any command does. I am not suggesting
you parse command strings —
git logwrites nothing, a wrapper hides the call, a script conceals itentirely. Tolerating the contention requires knowing none of that.
What would help
Any of these would help, most useful first:
field would avoid guessing which ones those are.
index.lock, on fix(stash): wait for transient index locks #1060's reasoning.does not fit: "Another git process seems to be running" is true, but it is another hk step in the same
run; "a git process may have crashed … remove the file manually" suggests deleting a healthy lock. So
the investigation starts with editor integrations and filesystem quirks instead of with the config.
Detecting that needs no inference either — only recognising git's index-lock message after the fact.
From the docs on
<GROUP>(source):
The green run above puts one writer in each group. It works, at the cost of a group per writer and full
serialisation rather than just of the git call.
Happy to send a PR for whichever of these you want, in whatever shape you prefer.
Environment
stash = "git",jobsdefaultRelated
RFCs on stash/error recovery
All reactions