Skip to content

Flow.RunLive leaks one goroutine per reconnect: unbuffered errChan abandoned on resumable errors #1152

Closed

Description

@dmora

Description

In internal/llminternal/base_flow.go, Flow.RunLive creates a fresh unbuffered errChan per connection attempt (main @ 0c88126, line 373; present since v1.3.0 and unchanged at v1.5.0). The reader and sender goroutines send to it without a cancellation guard:

  • reader: errChan <- err (line 389)
  • sender: errChan <- err (lines 430, 439)

The consumer reads a single error in the main select (line 521). When that error isResumable, the loop sets reconnect = true, breaks out, and starts a new attempt with a new errChan — abandoning the previous one. If the other goroutine of the old attempt subsequently fails (e.g. the sender hits the closed connection after the reader already reported), it blocks forever on a send to the abandoned unbuffered channel: one leaked goroutine (plus its stack and captured connection state) per reconnect cycle. Long-lived live sessions on flaky networks accumulate these.

Suggested fix

Wrap each errChan send in a select with case <-connCtx.Done(): (mirroring the existing eventsChan pattern in the same function), and ensure the per-attempt cancelConn is invoked on the reconnect path before starting the next attempt — today cleanup() runs on terminal paths but not when reconnect = true breaks the select.

Alternatively, errChan := make(chan error, 2) (one slot per producer goroutine) also unblocks the producers, at the cost of masking rather than modeling the lifecycle.

Environment

Observed by code inspection at v1.5.0 and confirmed unchanged on main (0c88126). Found during an integration review of v1.5.0 for a fork we run in a pre-production live voice deployment.

Activity

  1. indurireddy-TF commented on Jul 15, 2026

    @indurireddy-TF
    Contributor

    Hi @dmora Thanks for pointing this out.

    Using a select with <-connCtx.Done() seems like the cleaner fix as it aligns with the existing eventsChan logic, whereas a buffered channel is fragile if we add more background workers in the future. WDYT?

  2. chethanuk commented on Jul 17, 2026

    @chethanuk
    Contributor

    I'm picking this up with the select { case errChan <- err: case <-connCtx.Done(): } approach suggested above — it extends the existing eventsChan guard convention already in RunLive.

    One small correction to the issue text: cleanup() does run on the reconnect path (it's called unconditionally after the consumer loop, before the reconnect check), so the fix is smaller than proposed — only the three bare errChan <- err sends need guarding. Since cleanup always cancels connCtx before the next attempt, a connCtx-guarded send is guaranteed to unblock the abandoned producer.

    I'll include a reconnect-provoking regression test that fails deterministically on the current code (leaked RunLive producer goroutine blocked on the abandoned channel). PR shortly.

  3. chethanuk commented on Jul 17, 2026

    @chethanuk
    Contributor

    PR up: #1167 — the three errChan sends are now guarded with select { case errChan <- err: case <-connCtx.Done(): }, matching the existing eventsChan convention. The regression test fakes the Gemini Live wire protocol over a local websocket and fails deterministically on the unfixed code (goroutines parked in chan send at base_flow.go:389); details and before/after evidence in the PR body.

  4. self-assigned this
    on Aug 4, 2026
  5. added a commit that references this issue on Aug 6, 2026
    c108489
  6. added a commit that references this issue on Aug 10, 2026
    21f1301
  7. added a commit that references this issue on Aug 13, 2026
    123a257
  8. added a commit that references this issue on Sep 3, 2026
    440e352
  9. added a commit that references this issue on Sep 29, 2026
    302b053
  10. added 2 commits that reference this issue on Sep 29, 2026
    635eea7
    84fa2d6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't working

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions