Skip to content

fix(scan): bound clones with a timeout + rolling worker pool, surface skipped repos#91

Merged
fabio-gos-sonarsource merged 3 commits into
mainfrom
fix/clone-timeout-and-rolling-worker-pool
Jun 30, 2026
Merged

fix(scan): bound clones with a timeout + rolling worker pool, surface skipped repos#91
fabio-gos-sonarsource merged 3 commits into
mainfrom
fix/clone-timeout-and-rolling-worker-pool

Conversation

@fabio-gos-sonarsource

@fabio-gos-sonarsource fabio-gos-sonarsource commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Problem

  1. No clone timeout. gogit.Getrepos used go-git PlainClone with no deadline, so a stalled TCP connection to the git server (VPN drop, proxy, server under load) hung the cloning goroutine forever. It also swallowed clone errors — it logged them but still returned a valid path with a nil error, so a failed clone was treated as an empty-but-successful repo.
  2. Batch barrier in the worker pool. AnalyseReposList launched fixed batches of Workers and waited for all of a batch to finish before starting the next. One slow/hung repo therefore stalled up to Workers-1 idle workers and froze overall progress.

A GitHub Enterprise (and any platform) org-wide scan could freeze for 30+ minutes on a single repository, showing a stuck progress bar and repeated Waiting for workers.... Two independent root causes, both in the shared cross-platform analysis path:

Together: a single bad repo could hang an entire org scan with no recovery and no record.

Fix

  • Clone timeoutgogit.Getrepos now takes a timeout and uses PlainCloneContext. On failure it removes the partial clone and returns the error; a deadline surfaces as an explicit clone timed out after <d> message.
  • CloneTimeout setting (minutes; default 15, 0 disables) threaded through RepoParams → goloc.Params → gogit. Added to config_sample.json, the webui platform defaults, and the advanced-options form.
  • Rolling worker poolAnalyseReposList rewritten as a semaphore-bounded pool + WaitGroup. Same max concurrency as before, but every finished repo frees its slot immediately, so one slow repo can no longer block the others (no more batch barrier / Waiting for workers...).
  • Skipped repositories surfaced — repos that can't be analyzed (clone timeout/failure or analysis error) are recorded per run and persisted to Results/config/analysis_skipped.json, then shown in:
    • the ResultsAll web page (new "Skipped Repositories" section + GET /api/skipped-repositories)
    • the GlobalReport PDF (new "Skipped Repositories" section)

All of this is platform-agnostic — GitHub/GHE, GitLab, Bitbucket Cloud, Bitbucket DC, and Azure all flow through the same performRepoAnalysis / gogit.Getrepos path, so the fix applies to every platform.

Scope note

The "Skipped Repositories" report lists analysis-phase failures (the actionable ones tied to this bug). Intentional pre-analysis skips (archived/empty/excluded) remain summarized as counts, as before.

Tests

  • pkg/gogit: clone-timeout returns an error and leaves no temp clone behind; existing clone tests updated for the new signature.
  • golc (tag): getCloneTimeout defaulting/disable logic; concurrency-safe skipRecorder.
  • pkg/utils: skipped-repos save/load roundtrip, empty-clears-stale, missing-file; PDF section renders (empty / one / long-list-with-truncation) without panic.
  • Full CI-parity run green: go test ./pkg/..., -tags=golc, -tags=resultsall; both binaries build with -trimpath.

An org-wide scan could freeze indefinitely on a single repository. Two
root causes, both platform-agnostic (GitHub/GHE, GitLab, Bitbucket
Cloud/DC, Azure all share this path):

1. gogit.Getrepos used go-git PlainClone with no deadline, so a stalled
   connection to the git server hung the worker forever. It also
   swallowed clone errors (logged, then returned a valid path with nil
   error), so a failed clone looked like an empty-but-successful repo.

2. AnalyseReposList ran fixed batches with a barrier: the next batch
   could not start until the slowest repo in the current batch finished,
   so one slow/hung repo stalled up to Workers-1 idle workers and froze
   overall progress ("Waiting for workers...").

Changes:
- gogit.Getrepos now takes a timeout and uses PlainCloneContext; on
  failure it removes the partial clone and returns the error (timeouts
  become an explicit "clone timed out" message).
- New CloneTimeout setting (minutes, default 15, 0 = disabled) threaded
  through RepoParams -> goloc.Params -> gogit. Added to config_sample.json,
  the webui platform defaults, and the advanced-options form.
- AnalyseReposList rewritten as a semaphore-bounded rolling worker pool
  with a WaitGroup; same max concurrency as before, no batch barrier, so
  one slow repo can no longer block the others.
- Skipped repositories (clone timeout/failure or analysis error) are now
  recorded per run and persisted to Results/config/analysis_skipped.json,
  then surfaced in the ResultsAll web page (+ /api/skipped-repositories)
  and the GlobalReport PDF.
- Tests: clone-timeout + no-leak in gogit; getCloneTimeout and the
  concurrency-safe skipRecorder in golc; save/load roundtrip and PDF
  section rendering in utils.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread golc.go Outdated
Comment thread webui.go Outdated
fabio-gos-sonarsource and others added 2 commits June 30, 2026 09:53
Addresses two Gitar review findings on #91:

1. Data race on the shared progress counter. AnalyseReposList passed
   &count (plain int) into every worker goroutine; the rolling pool runs
   them concurrently and each does count++/reads count. Convert the
   counter to atomic.Int64 across analyseRepoFunc / the five analyse*Repo
   funcs / performRepoAnalysis / analyseDirectory, and increment via
   count.Add(1). This also fixes a latent bug where the success log only
   read count (never incremented it), so every repo printed "1" — each
   completed repo now gets a unique sequence number. The same shared
   pattern in AnalyseReposListFile/analyseDirectory is fixed too.
   Adds TestAnalyseReposListConcurrentCountNoRace as a -race regression
   guard over the multithreaded path.

2. Clearing the webui "Clone timeout" field silently disabled the
   deadline: parseInt('')||0 -> 0, which getCloneTimeout reads as "no
   timeout" — re-introducing the exact hang this feature prevents. An
   empty/blank field now falls back to the default (15); an explicit "0"
   still disables the deadline.

Verified: go test -race passes for ./pkg/... and -tags golc (incl. the
new concurrency test); webui and resultsall binaries build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SonarCloud flagged two MAJOR "too many parameters" (go:S107) smells on
the PR's new code: performRepoAnalysis (11 params) and analyseDirectory
(9 params). The parameter counts were unchanged from main — they only
entered the PR's new-code scope because the previous commit retouched
those signature lines for the atomic counter — but the gate blocks the
merge, and they are legitimate readability smells, so fix them properly.

Introduce an analysisOptions struct bundling the exclusions, keyword/
name filters, and report-mode flags. performRepoAnalysis now takes
(params, dest, spin, results, count, opts) = 6 params; analyseDirectory
takes (dir, opts, destDir, count) = 4. The five analyse*Repo callers and
AnalyseReposListFile build the struct; behavior is unchanged.

AnalyseReposListFile itself (8 params) is intentionally left untouched:
it is pre-existing and not in the PR's new-code scope, so editing its
signature would needlessly pull a new S107 into scope.

Verified: go build (webui, resultsall), go vet, and go test -race
-tags golc all pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@fabio-gos-sonarsource
fabio-gos-sonarsource merged commit 5cab78f into main Jun 30, 2026
4 checks passed
@gitar-bot

gitar-bot Bot commented Jun 30, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 2 resolved / 2 findings

Implements a rolling worker pool and repository clone timeouts to prevent scan hangs, while introducing a skipped repository reporting mechanism. A data race on the progress counter and silent deadline disabling were both addressed.

✅ 2 resolved
Bug: Data race on shared count pointer in rolling worker pool

📄 golc.go:377-387 📄 golc.go:701 📄 golc.go:729 📄 golc.go:745 📄 golc.go:754 📄 golc.go:764 📄 golc.go:774
In AnalyseReposList a single count := 1 is declared and its address is passed (via analyseRepoFunc) into every worker goroutine. With the new rolling worker pool, up to concurrency goroutines run performRepoAnalysis simultaneously, and each one does *count++ (golc.go:701,729,745,754,764) and reads *count in the success log line (golc.go:774) without any mutex or atomic. This is an unsynchronized concurrent read/write of the same int — a genuine data race (undefined behavior; go test -race/go build -race on this path would flag it, contradicting the "CI green" claim). Impact is limited to incorrect/torn progress numbers in the log line rather than control flow (the function returns total, not count), but it should be fixed while this concurrency code is being rewritten. Note the same shared-pointer pattern also exists in AnalyseReposListFile/analyseDirectory (golc.go:924). Fix by making the counter atomic (e.g. var count int64 + atomic.AddInt64) or guarding it with a mutex.

Edge Case: Cleared CloneTimeout field silently disables the deadline

📄 webui.go:1863-1864
In webui.go gatherConfig(), cfg.CloneTimeout = ctRaw ? (parseInt(ctRaw.value, 10) || 0) : 15;. If a user clears the "Clone timeout" input (empty string), parseInt('') is NaN, so NaN || 0 yields 0, which getCloneTimeout interprets as "deadline disabled" rather than the intended default of 15. This is the exact hang scenario the PR set out to prevent. Consider treating an empty/blank field as the default (15) instead of 0, reserving 0 for an explicit user-entered 0.

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@fabio-gos-sonarsource
fabio-gos-sonarsource deleted the fix/clone-timeout-and-rolling-worker-pool branch July 8, 2026 11:42
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