Skip to content

fix(rsqueue): buffer PollAddress errCh to prevent goroutine leak on abandoned poll#293

Merged
jonyoder merged 1 commit into
mainfrom
fix-polladdress-leak-19008
Jul 20, 2026
Merged

fix(rsqueue): buffer PollAddress errCh to prevent goroutine leak on abandoned poll#293
jonyoder merged 1 commit into
mainfrom
fix-polladdress-leak-19008

Conversation

@jonyoder

Copy link
Copy Markdown
Collaborator

Summary

DatabaseQueue.PollAddress returned an unbuffered error channel. Its polling goroutine has exactly one send (errCh <- err on a non-lock store error). When the caller stops receiving — e.g. its request context is canceled and it returns from the select { case <-ctx.Done(): ... case err = <-errCh: } wait in rscache/file.go — that send parks forever, leaking one goroutine per abandoned poll. Under bulk request pile-ups these accumulate and degrade the server.

Downstream this is rstudio/package-manager#19008 (blocked PollAddress senders observed in production goroutine dumps).

Fix

Buffer the channel by 1 (make(chan error, 1)). The goroutine sends at most one value before closing, so a single slot guarantees the send never blocks regardless of whether a receiver is still present. This mirrors the "guarantee the sender can always complete" approach rather than depending on a live receiver.

  • No behavior change for a live waiter: it still reads the single error (or the close) exactly as before. Errors are neither newly surfaced nor swallowed.
  • The only production caller is rscache/file.go (fileCache.get/Get); RecordingProducer.PollAddress is a test double and unaffected.

Test

Adds TestPollErrAbandonedReceiver — starts a poll, never receives from the returned channel, and asserts (via leaktest) the poll goroutine has exited. RED before the fix (leaked goroutine parked at the send), GREEN after.

Note: the new test uses the correct defer leaktest.Check(c)() form. The existing poll tests call defer leaktest.Check(c) without invoking the returned checker, so their leak assertion is currently a no-op — flagged here; fixing those is out of scope for this change.

go test ./pkg/rsqueue/... ./pkg/rscache/... and go vet pass.

Downstream

Package Manager (#19008) vendors this module; once released (a patch tag, e.g. v4.3.1), PPM will bump go.mod + go mod vendor and add a ### Fixed NEWS entry referencing #19008.

…bandoned poll

DatabaseQueue.PollAddress returned an unbuffered error channel. When the caller
stopped receiving (e.g. its request context was canceled and it returned), the
polling goroutine's single error send parked forever — leaking one goroutine per
abandoned poll. Under load these accumulate and degrade the server.

Buffer the channel by 1: the goroutine sends at most one value before closing,
so a single slot guarantees the send never blocks regardless of whether a
receiver is still present.

Adds TestPollErrAbandonedReceiver (leaktest) covering the abandoned-receiver
case. It uses the correct `defer leaktest.Check(c)()` form; the existing poll
tests call `defer leaktest.Check(c)` without invoking the returned checker, so
their leak assertion is a no-op (noted, out of scope to fix here).

Fixes the platform-lib side of rstudio/package-manager#19008.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jonyoder
jonyoder merged commit 0be8532 into main Jul 20, 2026
3 checks passed
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