Skip to content

fix(serve): self-heal orphaned git index.lock that wedges checkouts#425

Merged
tinder-maxwellelliott merged 2 commits into
masterfrom
claude/discover-session-issue-8f0489
Jul 11, 2026
Merged

fix(serve): self-heal orphaned git index.lock that wedges checkouts#425
tinder-maxwellelliott merged 2 commits into
masterfrom
claude/discover-session-issue-8f0489

Conversation

@tinder-maxwellelliott

Copy link
Copy Markdown
Collaborator

Problem

A production bazel-diff serve instance wedged on git and had to be restarted (a run of identical checkout failures over several minutes, then a fresh clone). The fatal error:

git checkout --force <sha> failed (exit 128)
fatal: Unable to create '/data/workspace/.git/index.lock': File exists.
... a git process may have crashed in this repository earlier: remove the file manually to continue.

An orphaned .git/index.lock permanently wedges the service. Every cache-missing request runs git checkout --force via process(..., destroyForcibly = true). When the compute thread is force-cancelled — a --requestTimeout firing future.cancel(true), JVM shutdown via computeExecutor.shutdownNow(), or an OOM kill — the git subprocess is SIGKILL'd mid-checkout. Git can't clean up index.lock on 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 — gc never touches index.lock.

Fix (GitClient.kt)

  1. Self-heal the orphaned lock. checkout catches the failure and — because HashService serializes all index-mutating git ops, so a lock present at checkout time is provably orphaned, not live — clears a stale <git-dir>/index.lock and retries once. This is exactly the recovery git's own message advises. A genuine failure (bad revision, no stale lock) still propagates unchanged.
  2. Disable auto-gc on checkouts (-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 stale index.lock (the wedged state) and asserts checkout self-heals.
  • checkoutStillThrowsOnGenuinelyBadRevision — ensures the recovery path doesn't swallow real failures.

Reproduced the exact exit 128 failure 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 --requestTimeout is 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

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>
@tinder-maxwellelliott tinder-maxwellelliott marked this pull request as ready for review July 10, 2026 23:58
@tinder-maxwellelliott tinder-maxwellelliott merged commit e1b851a into master Jul 11, 2026
15 checks passed
@tinder-maxwellelliott tinder-maxwellelliott deleted the claude/discover-session-issue-8f0489 branch July 11, 2026 02:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant