[repository-quality] π― Repository Quality Improvement Report β Goroutine Lifecycle Management & Structured Concurrency Adoption Gap #47797
Closed
Replies: 1 comment
|
This discussion was automatically closed because it expired on 2026-07-25T13:25:54.974Z.
|
0 replies
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.
Analysis Date: 2026-07-24
Focus Area: Goroutine Lifecycle Management & Structured Concurrency Adoption Gap
Strategy Type: Custom
Custom Area: Yes β goroutine lifecycle is a repository-specific pattern gap; the codebase already uses
errgroupandconc/poolcorrectly in 2 files but 5 fire-and-forget goroutines lack structured join/cancellation patternsExecutive Summary
The repository launches 7 production goroutines across 6 files. Two files (
pkg/cli/audit.goandpkg/cli/mcp_inspect_inspector.go) usegolang.org/x/sync/errgroupfor structured concurrency, andpkg/cli/logs_run_processor.gousessourcegraph/conc/poolwith bounded parallelism and automatic panic recovery. However, four other goroutine launch sites βupdate_check.go,compile_update_check.go,docker_images.go, andbootstrap_profile_github_app.goβ use ad-hoc fire-and-forget patterns with inconsistent lifecycle guarantees.The most critical gap is in
docker_images.go:StartDockerImagePulllaunches a goroutine with no join point whatsoever. The caller has no mechanism to know when the download finishes or whether it failed, and the goroutine can outlive the process context, deleting map entries via deferred cleanup afterpullStatehas been garbage-collected. Similarly,CheckForUpdatesAsyncinupdate_check.gospawns a goroutine and then does aselect{time.After(100ms)}as a best-effort delay β a fire-and-forget pattern that leaks when contexts are cancelled and has no way to signal completion.With
sourcegraph/concalready ingo.modanderrgroupalready imported by 3 packages, adopting consistent structured concurrency primitives across all goroutine sites would eliminate goroutine leak risk, simplify testing, and standardize the codebase pattern.Full Analysis Report
Focus Area: Goroutine Lifecycle Management & Structured Concurrency Adoption Gap
Current State Assessment
Production goroutine inventory:
pkg/cli/update_check.go:260CheckForUpdatesAsyncgo func()+select{time.After}pkg/cli/compile_update_check.go:67StartCompileUpdateCheckgo func()+ buffered chanwaitForCompileUpdateNotificationpkg/cli/docker_images.go:155StartDockerImagePullgo func()+ map deletepkg/cli/bootstrap_profile_github_app.go:211go func()+ select on resultChShutdownpkg/cli/bootstrap_profile_helpers.go:356go func() { _ = cmd.Wait() }pkg/console/spinner.go:144go func()+sync.WaitGroupwg.Wait()in Stop()pkg/cli/mcp_inspect_inspector.go:47errgroupg.Wait()Metrics Collected:
go func()goroutinesaudit.go,mcp_inspect_inspector.go,mcp_inspect_mcp.go)logs_run_processor.go)recover()guardFindings
Strengths
pkg/cli/logs_run_processor.gousesconc/pool.NewWithResultswithWithContextandWithMaxGoroutinesβ an exemplary bounded-concurrency patternpkg/cli/audit.gouseserrgroup.WithContextthroughout with structured launch helperspkg/console/spinner.gousessync.WaitGroupcorrectly with deferredwg.Doneandwg.Wait()inStop()pkg/cli/compile_update_check.gouses a buffered channel withdefer close()to signal goroutine completionAreas for Improvement
docker_images.go:StartDockerImagePullβ goroutine has no join point. The downloading map is mutated from the goroutine afterStartDockerImagePullreturns, and callers cannot block on completion or observe failureupdate_check.go:CheckForUpdatesAsyncβ fire-and-forget with aselect{time.After(100ms)}heuristic. The goroutine is detached from the caller; no test can deterministically verify it rango.uber.org/goleakor similar goroutine leak detector in tests, meaning goroutine leaks are undetectable in the test suitebootstrap_profile_github_app.goβ dual separateresultCh/errChchannels could be replaced with a singleerrgrouppattern for clarityDetailed Analysis
StartDockerImagePull(docker_images.go:130-220):This function starts a goroutine that retries
docker pullup to 3 times with exponential backoff. It setspullState.downloading[image] = truebefore launching the goroutine and defersdelete(pullState.downloading, image)inside the goroutine. There is no mechanism for callers to await completion, observe errors, or guarantee thatpullState.downloadingis clean after a test. The function returnstrueimmediately, making error-path testing impossible without race conditions.CheckForUpdatesAsync(update_check.go:257-294):Spawns a goroutine and then does a
select { case <-time.After(100ms); case <-ctx.Done() }β this makes the caller wait up to 100ms, which is a time.Sleep anti-pattern in disguise. The goroutine itself is fully detached after that window. In tests, the goroutine may run after the test function returns, causing data races on shared loggers.bootstrap_profile_github_app.go:205-236(server goroutine):Uses separate
resultCh chan *resultanderrCh chan error(1-buffered each) instead of the idiomaticerrgrouporconcpattern. The dual-channel select is correct but verbose;errgroupwould collapse this into a singleg.Wait()with error propagation.π€ Tasks for Copilot Agent
NOTE TO PLANNER AGENT: Split the following tasks into individual work items.
Improvement Tasks
Task 1: Add Join Point to
StartDockerImagePullvia Done ChannelPriority: High
Estimated Effort: Medium
Focus Area: Goroutine Lifecycle / docker_images.go
Description:
StartDockerImagePullinpkg/cli/docker_images.golaunches a goroutine with no join point, making it impossible to await download completion or observe errors. Refactor it to return a<-chan error(closed on completion) so callers can optionally block. UpdateCheckAndPrepareDockerImagesto await all outstanding downloads before returning. Add a test that verifiesStartDockerImagePullgoroutine completes cleanly.Acceptance Criteria:
StartDockerImagePullreturns<-chan errorthat closes (nil error) on success or sends an error on failureCheckAndPrepareDockerImagesblocks on all outstanding downloads before returningCode Region:
pkg/cli/docker_images.goTask 2: Replace Fire-and-Forget +
time.AfterinCheckForUpdatesAsyncPriority: High
Estimated Effort: Small
Focus Area: Goroutine Lifecycle / update_check.go
Description:
CheckForUpdatesAsyncinpkg/cli/update_check.gospawns a detached goroutine and then doesselect{time.After(100ms)}β a fire-and-forget pattern that is hard to test and can cause goroutine leaks. Refactor to return afunc()join function (same pattern asStartCompileUpdateCheckincompile_update_check.go), so the caller can optionally wait for the result before exiting. The goroutine should write to a buffered channel; the join function reads with a short timeout.Acceptance Criteria:
CheckForUpdatesAsyncis renamed or replaced by aStartUpdateCheck(ctx) func()returning a join functioncompileUpdateCheckTimeout(or a newupdateCheckNoWaitconstant) for a resulttime.Aftersleep inside the function body β the timeout is only in the join functionmain.go/compilecommand updatedCode Region:
pkg/cli/update_check.go,cmd/gh-aw/main.goTask 3: Refactor
bootstrap_profile_github_app.goDual-Channel toerrgroupPriority: Medium
Estimated Effort: Small
Focus Area: Goroutine Lifecycle / bootstrap_profile_github_app.go
Description:
createBootstrapGitHubAppinpkg/cli/bootstrap_profile_github_app.gouses separateresultCh chan *bootstrapCreatedGitHubAppanderrCh chan errorchannels plus a manualselect. Replace the dual-channel pattern witherrgroup.WithContextso the result is captured via a closure variable, matching the established project pattern fromaudit.goandmcp_inspect_inspector.go.Acceptance Criteria:
resultChanderrChremoved in favour of a sharedvar result *bootstrapCreatedGitHubAppcaptured by closureg.Go(func() error { return server.Serve(listener) })(or equivalent)bootstrapGitHubAppFlowChannelsstruct removed if it only existed to hold the two channelscontext.WithTimeouton the errgroup contextCode Region:
pkg/cli/bootstrap_profile_github_app.goTask 4: Add Goroutine Leak Detection to Test Suite
Priority: Medium
Estimated Effort: Medium
Focus Area: Testing / goroutine lifecycle
Description: The test suite has zero goroutine leak detection.
go.uber.org/goleakcan be added as a test-only dependency and called viagoleak.VerifyTestMain(m)in package test main functions forpkg/cli. This would catch any goroutine leak introduced by the fire-and-forget patterns identified above and prevent regressions.Acceptance Criteria:
go.uber.org/goleakadded togo.modas a test dependencyTestMaininpkg/cli(or a newpkg/cli/main_test.go) callsgoleak.VerifyTestMain(m)goleak.IgnoreTopFunctionCode Region:
pkg/cli/,go.modRun the tests. For any goroutine that is legitimately fire-and-forget (e.g. the browser launcher goroutine in
bootstrap_profile_helpers.go), addgoleak.IgnoreTopFunction("os/exec.(*Cmd).Wait")or equivalent. Fix any goroutine leaks revealed by the detector. Runmake fmtandmake test-unit.π Historical Context
Previous Focus Areas (last 5)
π― Recommendations
Immediate Actions (This Week)
StartDockerImagePullto return a join channel β Priority: HighCheckForUpdatesAsyncto matchStartCompileUpdateCheckpattern β Priority: HighShort-term Actions (This Month)
bootstrap_profile_github_app.godual-channel to errgroup β Priority: Mediumgoleak.VerifyTestMaintopkg/clitests β Priority: MediumLong-term Actions (This Quarter)
pkg/cli/doc.goβ Priority: Lowπ Success Metrics
pkg/cli)Next Steps
References:
All reactions