fix(serve): self-heal orphaned git index.lock that wedges checkouts#425
Merged
tinder-maxwellelliott merged 2 commits intoJul 11, 2026
Merged
Conversation
A `git checkout` that is force-killed mid-flight -- a --requestTimeout
cancellation (BazelDiffServer forcibly destroys the subprocess),
JVM shutdown, or an OOM kill -- leaves `<git-dir>/index.lock` behind:
git removes the lock on a clean exit but cannot on SIGKILL. Because
nothing ever clears it, every later checkout fails with
fatal: Unable to create '.../.git/index.lock': File exists. (exit 128)
so a single killed checkout wedges the query service until the workspace
is rebuilt (observed in production as a run of identical checkout failures
followed by a pod restart + fresh clone).
Since HashService serializes all workspace-mutating git operations, no
live in-process checkout can be holding the lock, so a lock present when
a checkout runs is provably orphaned. `checkout` now clears a stale
`index.lock` and retries once -- exactly the recovery git's own error
message advises -- while a genuine failure (bad revision, no stale lock)
still propagates unchanged.
Also pass `-c gc.auto=0` on checkouts: the service checks out on nearly
every cache-missing request, and letting each one fork a background
`git gc --auto` ("Auto packing the repository in the background") spawns
unsynchronized repacks that contend with the next checkout, slowing it
and making a mid-checkout kill (and the orphaned lock above) more likely.
Fetches still trigger gc, keeping fetch-created loose objects packed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A production
bazel-diff serveinstance wedged on git and had to be restarted (a run of identical checkout failures over several minutes, then a fresh clone). The fatal error:An orphaned
.git/index.lockpermanently wedges the service. Every cache-missing request runsgit checkout --forceviaprocess(..., destroyForcibly = true). When the compute thread is force-cancelled — a--requestTimeoutfiringfuture.cancel(true), JVM shutdown viacomputeExecutor.shutdownNow(), or an OOM kill — the git subprocess is SIGKILL'd mid-checkout. Git can't clean upindex.lockon SIGKILL, so it's orphaned, and since nothing ever clears it, every subsequent checkout fails until the workspace is rebuilt.The "Auto packing the repository in background" lines in the same window are a correlated symptom, not the cause —
gcnever touchesindex.lock.Fix (
GitClient.kt)checkoutcatches the failure and — becauseHashServiceserializes all index-mutating git ops, so a lock present at checkout time is provably orphaned, not live — clears a stale<git-dir>/index.lockand retries once. This is exactly the recovery git's own message advises. A genuine failure (bad revision, no stale lock) still propagates unchanged.-c gc.auto=0). The service checks out constantly; letting each one fork a background repack contends with the next checkout, slowing it and making a mid-checkout kill more likely (and it's the source of the log noise). Fetches still trigger gc, so loose objects stay packed.Tests (
GitClientTest.kt)checkoutClearsOrphanedIndexLockAndSucceeds— plants a staleindex.lock(the wedged state) and asserts checkout self-heals.checkoutStillThrowsOnGenuinelyBadRevision— ensures the recovery path doesn't swallow real failures.Reproduced the exact
exit 128failure from a planted stale lock and confirmed heal-and-retry recovers. Server test suite (GitClientTest,HashServiceTest,ImpactedTargetsServiceTest,BazelDiffServerTest) passes; ktfmt clean.Reviewer note
This stops the permanent wedge — each request now self-heals the lock. If
--requestTimeoutis set below the real checkout time on a very large workspace, checkouts can still be killed and re-orphan the lock per-request (now recovered each time rather than wedging); the underlying remedy there is a timeout sized to the workspace.🤖 Generated with Claude Code