Skip to content

fix(miner): serialize per-repo base clone to prevent concurrent git races - #6917

Merged
loopover-orb[bot] merged 1 commit into
JSONbored:mainfrom
e11734937-beep:fix/6762-serialize-repo-clone
Jul 17, 2026
Merged

fix(miner): serialize per-repo base clone to prevent concurrent git races#6917
loopover-orb[bot] merged 1 commit into
JSONbored:mainfrom
e11734937-beep:fix/6762-serialize-repo-clone

Conversation

@e11734937-beep

Copy link
Copy Markdown
Contributor

Problem

ensureRepoCloned (packages/loopover-miner/lib/repo-clone.js) resolves one deterministic path per repoFullName and mutates it in place (git fetch origin, git checkout <base>, git reset --hard origin/<base>) with no locking. worktree-allocator.js's acquire() only caps the total active-slot count (maxConcurrency), never per-repoFullName exclusivity, so two attempts for the same repo can both hold a slot and call ensureRepoCloned concurrently — a genuine git-level race that corrupts the index/HEAD/refs or trips .git/index.lock.

Fix

Add a per-repoPath in-process async mutex. repoCloneLocks maps a resolved repoPath to the tail of its in-flight promise chain, so same-repo ensureRepoCloned calls run strictly one after another while different repoPaths stay fully parallel. The public ensureRepoCloned resolves the same repoPath the git step uses as the mutex key and delegates the git work to an internal ensureRepoClonedUnlocked. The lock is released in a finally on both success and failure, so a failing attempt can neither reject a waiter nor wedge the queue; the map entry is dropped once the chain drains to keep it bounded.

Tests

Added a #6762 regression suite:

  • two concurrent same-repo calls serialize (no interleaved git ops; strict start-to-end ordering)
  • different repos still run in parallel
  • the lock is released when a call throws, so a later same-repo call still proceeds
  • env-resolved base dir keys the lock when no cloneBaseDir is given
  • fetch/checkout/reset stderr passthrough plus fallback branches

Full branch coverage on every changed line.

Closes #6762

…aces

ensureRepoCloned resolves one deterministic path per repoFullName and
mutates it in place (git fetch/checkout/reset --hard) with no locking,
while worktree-allocator only caps the total active-slot count and never
enforces per-repo exclusivity. Two attempts for the same repo could
therefore run these git commands against the shared base clone
concurrently, corrupting the index/HEAD/refs or tripping .git/index.lock.

Add a per-repoPath in-process async mutex: a Map of repoPath to the tail
of an in-flight promise chain serializes same-repo ensureRepoCloned calls
while different repoPaths still run in parallel. The lock is released on
both success and failure (finally), so one failing attempt can neither
reject a waiter nor wedge the queue, and the entry is dropped once the
chain drains.

Closes JSONbored#6762
@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.71%. Comparing base (68b8694) to head (05a7e90).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #6917   +/-   ##
=======================================
  Coverage   93.70%   93.71%           
=======================================
  Files         685      685           
  Lines       68336    68348   +12     
  Branches    18724    18724           
=======================================
+ Hits        64037    64049   +12     
  Misses       3302     3302           
  Partials      997      997           
Flag Coverage Δ
shard-1 43.59% <8.33%> (-0.10%) ⬇️
shard-2 36.80% <8.33%> (+0.02%) ⬆️
shard-3 32.59% <8.33%> (-0.06%) ⬇️
shard-4 34.41% <100.00%> (-0.33%) ⬇️
shard-5 31.49% <8.33%> (+0.29%) ⬆️
shard-6 46.03% <8.33%> (+0.27%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
packages/loopover-miner/lib/repo-clone.js 100.00% <100.00%> (ø)

@loopover-orb loopover-orb Bot added the gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. label Jul 17, 2026
@loopover-orb

loopover-orb Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Tip

✅ LoopOver review result - approve/merge recommended

Review updated: 2026-07-17 13:22:52 UTC

2 files · 1 AI reviewer · no blockers · readiness 86/100 · CI green · clean

✅ Suggested Action - Approve/Merge

  • safe to merge

Review summary
This adds a per-repoPath async mutex (promise-chain based) around the existing ensureRepoCloned git mutation logic, renaming the original function to ensureRepoClonedUnlocked and wrapping it with withRepoCloneLock keyed on the resolved repoPath. The implementation is correct: the tail promise swallows both resolve/reject so a failing attempt still releases the lock in the finally block, and the map entry is deleted only if it still points to the same tail (avoiding a race where a newer call's entry gets deleted by an older one's cleanup). Tests directly exercise same-repo serialization, cross-repo parallelism, lock release on throw, and the env-resolved base dir path, which is solid coverage of the new logic.

Nits — 3 non-blocking
  • The map-key check `if (repoCloneLocks.get(repoPath) === tail) repoCloneLocks.delete(repoPath)` is correct but subtle — a one-line comment explaining why the identity check matters (avoiding deleting a newer waiter's entry) would help future readers, per repo-clone.js:88.
  • The `No concurrency guard on the shared per-repo base clone lets concurrent attempts race git fetch/checkout/reset --hard #6762` reference in the code comment (flagged in the external brief as a 'magic number') is an issue reference, not a magic number needing a named constant — that flag can be disregarded.
  • Consider whether `withRepoCloneLock` should live in a small shared utility if other per-key serialization needs arise elsewhere in the miner package, though at one call site duplicating isn't yet warranted.

Decision drivers

  • ✅ Code review — No blockers (1 reviewer)
  • ✅ Gate result — Passing (No configured blocker found.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #6762
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ❌ 8/20 High review scope from cached public metadata (1 linked issue).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 60 registered-repo PR(s), 35 merged, 2 issue(s).
Contributor context ✅ Confirmed Gittensor contributor e11734937-beep; Gittensor profile; 60 PR(s), 2 issue(s).
Improvement ✅ Minor risk: clean · value: minor · LLM: moderate
Linked issue satisfaction

Addressed
The diff adds a per-repoPath in-process async mutex (repoCloneLocks/withRepoCloneLock) that serializes ensureRepoCloned calls targeting the same resolved repo path while leaving different repos parallel, directly satisfying the issue's first requirement, and includes a regression test with concurrent same-repo calls asserting non-interleaved git ops as required by the deliverables.

Review context
  • Author: e11734937-beep
  • Role context: outside_contributor
  • Public audience mode: oss maintainer
  • Lane context: Repository is configured for direct PR review.
  • Public profile languages: JavaScript
  • Official Gittensor activity: 60 PR(s), 2 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Start here: Add a concise scope and risk note.
  • Then work through the remaining 1 step in the Signals table above.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
🧪 Chat with LoopOver

Ask LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.

  • @loopover ask &lt;question&gt; answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat &lt;question&gt; answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

Full command reference: https://loopover.ai/docs/loopover-commands

🧪 Experimental — new and may change.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.

  • Re-run LoopOver review

@loopover-orb loopover-orb Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LoopOver approves — the gate is satisfied and CI is green.

@loopover-orb
loopover-orb Bot merged commit 2e2f588 into JSONbored:main Jul 17, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No concurrency guard on the shared per-repo base clone lets concurrent attempts race git fetch/checkout/reset --hard

1 participant