Part of #3 (analysis and plan in the comment dated 2026-09-21 14:34 there).
handleStatusJSON and handleStats (internal/server/handlers.go:183,402) run the stats query in a goroutine that sends on an unbuffered channel. When the 4 s timeout wins, nothing receives and the goroutine blocks forever. The status page polls every 2 s, so once stats take over 4 s every poll leaks one goroutine.
Streamer.stream (internal/streamer/streamer.go:514,529) starts two ticker goroutines per connection that stop only with the streamer's lifetime context, so every reconnect leaks two.
Requirements
Give statsChan and errChan capacity 1 so the sender never blocks.
Give the two ticker goroutines a context cancelled when that connection's stream call returns.
Files: internal/server/handlers.go, internal/streamer/streamer.go, and their tests.
Definition of done
A test forces the stats timeout repeatedly and shows the goroutine count returns to its starting value; a test (or the existing reconnect test extended) shows the same across a reconnect. Use plain runtime.NumGoroutine with a short settle loop unless the repo already uses a leak-check library.
make check green. Commit title ends (closes #N).
Model: fable-5-1
Part of https://git.eeqj.de/sneak/routewatch/issues/3 (analysis and plan in the comment dated 2026-09-21 14:34 there).
1. `handleStatusJSON` and `handleStats` (`internal/server/handlers.go:183,402`) run the stats query in a goroutine that sends on an unbuffered channel. When the 4 s timeout wins, nothing receives and the goroutine blocks forever. The status page polls every 2 s, so once stats take over 4 s every poll leaks one goroutine.
2. `Streamer.stream` (`internal/streamer/streamer.go:514,529`) starts two ticker goroutines per connection that stop only with the streamer's lifetime context, so every reconnect leaks two.
## Requirements
- Give `statsChan` and `errChan` capacity 1 so the sender never blocks.
- Give the two ticker goroutines a context cancelled when that connection's `stream` call returns.
- Files: `internal/server/handlers.go`, `internal/streamer/streamer.go`, and their tests.
## Definition of done
- A test forces the stats timeout repeatedly and shows the goroutine count returns to its starting value; a test (or the existing reconnect test extended) shows the same across a reconnect. Use plain `runtime.NumGoroutine` with a short settle loop unless the repo already uses a leak-check library.
- `make check` green. Commit title ends ` (closes #N)`.
Model: fable-5-1
Implemented as #19 (branch issue-12-goroutine-leaks, base next).
Stats handlers: statsChan and errChan given capacity 1 in both handleStatusJSON and handleStats, so the query goroutine's send completes even when the request timeout wins.
Streamer: the two per-connection ticker goroutines now use a per-connection context cancelled when stream returns, so a reconnect no longer leaks them. The streamer gained an unexported endpoint field (default: the live RIS URL) so a test can point stream at a local server.
Tests force the stats timeout repeatedly and drive many reconnects, then assert the goroutine count returns to its starting value via runtime.NumGoroutine with a short settle loop.
Disclosure: make check was verified green inside the Docker lint stage (pinned linter); host make lint panics on a toolchain mismatch (host linter built with go1.25, stdlib needs go1.26), so lint was not run on the host.
Model: opus-4-8
Implemented as https://git.eeqj.de/sneak/routewatch/pulls/19 (branch `issue-12-goroutine-leaks`, base `next`).
Stats handlers: `statsChan` and `errChan` given capacity 1 in both `handleStatusJSON` and `handleStats`, so the query goroutine's send completes even when the request timeout wins.
Streamer: the two per-connection ticker goroutines now use a per-connection context cancelled when `stream` returns, so a reconnect no longer leaks them. The streamer gained an unexported endpoint field (default: the live RIS URL) so a test can point `stream` at a local server.
Tests force the stats timeout repeatedly and drive many reconnects, then assert the goroutine count returns to its starting value via `runtime.NumGoroutine` with a short settle loop.
Disclosure: `make check` was verified green inside the Docker lint stage (pinned linter); host `make lint` panics on a toolchain mismatch (host linter built with go1.25, stdlib needs go1.26), so lint was not run on the host.
Model: opus-4-8
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Part of #3 (analysis and plan in the comment dated 2026-09-21 14:34 there).
handleStatusJSONandhandleStats(internal/server/handlers.go:183,402) run the stats query in a goroutine that sends on an unbuffered channel. When the 4 s timeout wins, nothing receives and the goroutine blocks forever. The status page polls every 2 s, so once stats take over 4 s every poll leaks one goroutine.Streamer.stream(internal/streamer/streamer.go:514,529) starts two ticker goroutines per connection that stop only with the streamer's lifetime context, so every reconnect leaks two.Requirements
statsChananderrChancapacity 1 so the sender never blocks.streamcall returns.internal/server/handlers.go,internal/streamer/streamer.go, and their tests.Definition of done
runtime.NumGoroutinewith a short settle loop unless the repo already uses a leak-check library.make checkgreen. Commit title ends(closes #N).Model: fable-5-1
Implemented as #19 (branch
issue-12-goroutine-leaks, basenext).Stats handlers:
statsChananderrChangiven capacity 1 in bothhandleStatusJSONandhandleStats, so the query goroutine's send completes even when the request timeout wins.Streamer: the two per-connection ticker goroutines now use a per-connection context cancelled when
streamreturns, so a reconnect no longer leaks them. The streamer gained an unexported endpoint field (default: the live RIS URL) so a test can pointstreamat a local server.Tests force the stats timeout repeatedly and drive many reconnects, then assert the goroutine count returns to its starting value via
runtime.NumGoroutinewith a short settle loop.Disclosure:
make checkwas verified green inside the Docker lint stage (pinned linter); hostmake lintpanics on a toolchain mismatch (host linter built with go1.25, stdlib needs go1.26), so lint was not run on the host.Model: opus-4-8