Skip to content

Fix responses goroutine leak#874

Merged
ilopezluna merged 4 commits into
mainfrom
fix/responses-goroutine-leak
Apr 20, 2026
Merged

Fix responses goroutine leak#874
ilopezluna merged 4 commits into
mainfrom
fix/responses-goroutine-leak

Conversation

@ilopezluna
Copy link
Copy Markdown
Contributor

This pull request introduces proper resource cleanup for the router and service layers to prevent goroutine leaks, especially when the optional Responses API is enabled. The changes ensure that any background goroutines started by the router (such as those for the responses Store) are properly stopped when the service shuts down. It also adds tests to verify that resource cleanup is performed correctly and integrates a goroutine leak detector into the test suite.

NewRouter now returns a RouterResult that tracks closable resources
(currently the responses Store cleanup goroutine). Service gains a
Close() method that delegates to RouterResult.Close().

Callers (e.g. Docker Desktop / pinata) must call Service.Close() on
shutdown. Without this, the responses Store background goroutine
leaks and triggers goroutine-leak detectors in integration tests.
Ensures the responses Store cleanup goroutine is stopped when the
standalone model-runner process exits.
Add testmain_test.go with goleak.VerifyTestMain to detect goroutine
leaks in the routing package.

Add router_test.go with tests that verify:
- NewRouter with IncludeResponsesAPI + Close does not leak goroutines
- NewRouter without IncludeResponsesAPI + Close is safe (no-op)

Verified that without calling Close, goleak catches the leaked
responses.Store.cleanupLoop goroutine, confirming these tests serve
as a regression guard.
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • Consider making RouterResult.Close idempotent (e.g., by clearing or guarding the closers slice after the first call) so that accidental double-closing does not invoke cleanup functions multiple times.
  • In main.go, you might want to call svc.Close() with defer immediately after successful service creation so that resources are reliably cleaned up on all exit paths, including early returns due to errors.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider making `RouterResult.Close` idempotent (e.g., by clearing or guarding the `closers` slice after the first call) so that accidental double-closing does not invoke cleanup functions multiple times.
- In `main.go`, you might want to call `svc.Close()` with `defer` immediately after successful service creation so that resources are reliably cleaned up on all exit paths, including early returns due to errors.

## Individual Comments

### Comment 1
<location path="pkg/routing/router.go" line_range="20-22" />
<code_context>
+// resources that require cleanup.
+type RouterResult struct {
+	Mux *NormalizedServeMux
+	// closers collects io.Closer values that must be closed when the
+	// router is no longer needed (e.g. the responses Store goroutine).
+	closers []func()
+}
+
</code_context>
<issue_to_address>
**suggestion:** Align the `closers` field type with the comment (io.Closer vs func()).

The field’s behavior and its comment don’t match. Please either change the type to `[]io.Closer` and call `.Close()` in `RouterResult.Close`, or update the comment to describe that this holds arbitrary cleanup functions rather than `io.Closer` instances.

Suggested implementation:

```golang
	"io"

	"github.com/docker/model-runner/pkg/responses"

```

```golang
 // RouterResult is the output of NewRouter, bundling the mux with any
 // resources that require cleanup.
type RouterResult struct {
	Mux *NormalizedServeMux
	// closers collects io.Closer values that must be closed when the
	// router is no longer needed (e.g. the responses Store goroutine).
	closers []io.Closer
}

```

To fully align behavior with the new `[]io.Closer` type:
1. Update `RouterResult.Close` (wherever it is defined) to iterate over `r.closers` and call `Close()` on each element, e.g.:
   - 
   ```go
   func (r *RouterResult) Close() {
       for _, c := range r.closers {
           if c != nil {
               _ = c.Close()
           }
       }
   }
   ```
   Optionally clear the slice afterward (`r.closers = nil`) if the router can be reused.
2. Update all code that constructs a `RouterResult` to append `io.Closer` implementations to `closers` instead of `func()` cleanup callbacks. Any existing `func()`-based cleanup should be wrapped in an adapter that implements `io.Closer` if still needed.
</issue_to_address>

### Comment 2
<location path="pkg/routing/router_test.go" line_range="12" />
<code_context>
+// IncludeResponsesAPI enabled and then calling Close does not leak
+// goroutines. The goleak detector in TestMain will catch any leak.
+func TestNewRouter_WithResponsesAPI_Close(t *testing.T) {
+	log := slog.New(slog.DiscardHandler)
+
+	result := NewRouter(RouterConfig{
</code_context>
<issue_to_address>
**issue (testing):** Tests use `slog.DiscardHandler`, which does not exist in the standard slog API and will prevent the tests from compiling

`log/slog` doesn’t define `DiscardHandler`, so this test won’t compile. To discard logs while keeping a valid logger, you can do something like `slog.New(slog.NewTextHandler(io.Discard, nil))` (or JSON handler). Any handler that writes to `io.Discard` is sufficient.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread pkg/routing/router.go Outdated
Comment thread pkg/routing/router_test.go
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request implements a resource cleanup mechanism to prevent goroutine leaks by introducing a RouterResult struct and Close methods for the router and service. Feedback highlights the need for idempotency in the Close methods to prevent panics on multiple calls and the addition of nil checks to avoid potential pointer dereferences in main.go and service.go.

Comment thread main.go Outdated
Comment thread pkg/routing/router.go
Comment thread pkg/routing/service.go
- RouterResult.Close: nil out closers slice after execution to prevent
  double-close panics (responses.Store.Close closes a channel).
- Service.Close: guard against nil receiver and nil routerResult; nil
  out routerResult after closing for idempotency.
- main.go: use defer svc.Close() right after creation so cleanup runs
  on all exit paths. Remove the redundant explicit call at the bottom.
- Fix closers field comment to accurately describe []func() (not
  io.Closer).
@ilopezluna
Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • The RouterResult.Close and Service.Close methods are documented as idempotent but are not concurrency-safe; if there’s any chance of concurrent shutdown paths, consider guarding the closers slice and routerResult with a mutex or sync.Once or clarify in the doc comments that they are not safe for concurrent use.
  • Since RouterResult is essentially a resource bundle with a Close method, consider having it implement io.Closer (and documenting that), which would make it easier to integrate with existing cleanup patterns and tooling in the rest of the codebase.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `RouterResult.Close` and `Service.Close` methods are documented as idempotent but are not concurrency-safe; if there’s any chance of concurrent shutdown paths, consider guarding the `closers` slice and `routerResult` with a mutex or `sync.Once` or clarify in the doc comments that they are not safe for concurrent use.
- Since `RouterResult` is essentially a resource bundle with a `Close` method, consider having it implement `io.Closer` (and documenting that), which would make it easier to integrate with existing cleanup patterns and tooling in the rest of the codebase.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@ilopezluna ilopezluna requested a review from a team April 20, 2026 15:12
@ilopezluna ilopezluna merged commit ed5aba0 into main Apr 20, 2026
15 of 16 checks passed
@ilopezluna ilopezluna deleted the fix/responses-goroutine-leak branch April 20, 2026 15:35
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.

2 participants