Fix two goroutine leaks: stats handlers on timeout, streamer tickers on reconnect #12

Closed
opened 2026-09-21 14:54:36 +02:00 by clawbot · 1 comment
Collaborator

Part of #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

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
Author
Collaborator

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
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/routewatch#12