Skip to content

fix(clientcore): make consumer signaling backoff context-aware - #372

Merged
myleshorton merged 2 commits into
mainfrom
fisk/unbounded-signaling-retry-ctx
Jul 17, 2026
Merged

fix(clientcore): make consumer signaling backoff context-aware#372
myleshorton merged 2 commits into
mainfrom
fisk/unbounded-signaling-retry-ctx

Conversation

@myleshorton

@myleshorton myleshorton commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Problem

The consumer WebRTC FSM's genesis/offer retry paths in consumer.go slept on a bare <-time.After(options.ErrorBackoff) (default 5s) that ignored the FSM's ctx. WorkerFSM.Stop() cancels that ctx, but the run loop only checks it between states — so an FSM parked in a backoff sleep lingers up to a full ErrorBackoff before it can exit.

That matters under rapid outbound rebuilds: when a client roams across networks, the unbounded outbound is torn down and re-created repeatedly. Each stopped FSM that's mid-backoff keeps its goroutine (and the signaling/connection resources it holds) alive for the remainder of the backoff, so overlapping FSMs stack up. This was a contributor to the iOS memory/goroutine growth in getlantern/engineering#3698 (ticket 180321: freddie signaling was failing 453×, so the FSM was almost always sitting in exactly these backoff sleeps).

Fix

Add a sleepOrDone(ctx, d) helper that waits for d or ctx.Done(), and use it for all 8 backoff sleeps in the consumer FSM. Steady-state reconnection cadence is unchanged; only the cancellation path is faster (prompt instead of up-to-5s).

Relationship to #371

Complementary, non-overlapping. #371 (garmr/fix-engine-teardown-leak) closes connections/goroutines on FSM cancel but does not touch consumer.go. This PR removes the backoff delay before that cancel is observed on the consumer path. Both are wanted.

Verification

  • go build ./clientcore/ clean; go test ./clientcore/ passes.
  • Added TestSleepOrDoneReturnsEarlyOnCancel / TestSleepOrDoneWaitsWhenNotCancelled.

Refs getlantern/engineering#3698

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved consumer FSM responsiveness so error-backoff pauses end promptly when cancellation is requested.
    • Corrected genesis-related and signaling-related backoff behavior to respect cancellation and avoid continuing in stalled loops.
  • Tests
    • Added unit tests validating the cancellation-fast path and the expected backoff timing behavior.

The consumer WebRTC FSM's genesis/offer retry paths slept on a bare
`<-time.After(options.ErrorBackoff)` (default 5s) that ignored the
FSM's context. When the outbound is torn down mid-backoff — e.g. an
unbounded outbound being rebuilt on a network change — the WorkerFSM
goroutine lingered up to a full ErrorBackoff before the run loop could
observe cancellation and exit. Under rapid outbound rebuilds (a client
roaming across networks) this stacks overlapping FSMs, each still
holding its signaling/connection resources, which contributed to the
iOS memory/goroutine growth in getlantern/engineering#3698.

Replace the 8 backoff sleeps with a sleepOrDone(ctx, d) helper that
also returns on ctx.Done(), so a stopped FSM exits its backoff
immediately. Steady-state reconnection cadence is unchanged; only the
cancellation path is faster.

Complements #371 (engine teardown leak): #371 closes connections on FSM
cancel, this removes the backoff delay before that cancel is observed
on the consumer path (consumer.go is not touched by #371).

Refs getlantern/engineering#3698

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 16, 2026 17:38
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d508108c-3bb1-47ec-9c75-89c600537137

📥 Commits

Reviewing files that changed from the base of the PR and between 072135a and 2d78516.

📒 Files selected for processing (1)
  • clientcore/consumer.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • clientcore/consumer.go

📝 Walkthrough

Walkthrough

The consumer FSM adds a context-aware sleep helper and uses it for error-backoff waits across genesis, offer signaling, and ICE-candidate signaling. Unit tests verify cancellation and duration-based waiting.

Changes

Consumer backoff cancellation

Layer / File(s) Summary
Context-aware sleep helper
clientcore/consumer.go, clientcore/consumer_test.go
Adds sleepOrDone and tests cancellation and duration-based behavior.
FSM backoff integration
clientcore/consumer.go
Replaces fixed backoff timers across genesis, offer signaling, unexpected-status, and ICE-candidate error paths, including cancellation handling after genesis decode errors.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: making clientcore consumer signaling backoff respond to context cancellation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fisk/unbounded-signaling-retry-ctx

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
clientcore/consumer.go (1)

176-176: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Explicitly ignore the error returned by res.Body.Close().

Static analysis flags that the error return value of res.Body.Close() is not checked. Although ignoring errors from HTTP response body closures is generally safe, explicitly ignoring the error with a blank identifier properly silences the linter and signals intent to future maintainers.

  • clientcore/consumer.go#L176-L176: Explicitly ignore the error using defer func() { _ = res.Body.Close() }().
  • clientcore/consumer.go#L319-L319: Explicitly ignore the error using defer func() { _ = res.Body.Close() }().
♻️ Proposed fixes

clientcore/consumer.go#L176-L176

-			defer res.Body.Close()
+			defer func() { _ = res.Body.Close() }()

clientcore/consumer.go#L319-L319

-			defer res.Body.Close()
+			defer func() { _ = res.Body.Close() }()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@clientcore/consumer.go` at line 176, Explicitly discard the errors returned
when closing response bodies by replacing both deferred res.Body.Close calls in
clientcore/consumer.go at lines 176-176 and 319-319 with deferred closures that
assign the Close result to the blank identifier.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@clientcore/consumer.go`:
- Line 176: Explicitly discard the errors returned when closing response bodies
by replacing both deferred res.Body.Close calls in clientcore/consumer.go at
lines 176-176 and 319-319 with deferred closures that assign the Close result to
the blank identifier.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a03427c6-7ec7-4b0b-ad08-74cfbb9ae202

📥 Commits

Reviewing files that changed from the base of the PR and between 0a043c7 and 072135a.

📒 Files selected for processing (2)
  • clientcore/consumer.go
  • clientcore/consumer_test.go

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR makes the consumer WebRTC WorkerFSM backoff sleeps context-aware so cancellation via WorkerFSM.Stop() can be observed promptly instead of waiting out ErrorBackoff, reducing overlapping FSM lifetimes during rapid outbound rebuilds.

Changes:

  • Added sleepOrDone(ctx, d) to wait for either a duration or ctx.Done().
  • Replaced ErrorBackoff time.After sleeps in the consumer FSM with sleepOrDone.
  • Added unit tests validating sleepOrDone returns promptly on cancellation and waits otherwise.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
clientcore/consumer.go Introduces sleepOrDone and applies it to error-backoff sleeps in the consumer FSM.
clientcore/consumer_test.go Adds unit tests covering the new context-aware sleep helper.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread clientcore/consumer.go
Addresses PR review: the genesis listen loop's decode-error path slept
then `continue`d within the same state. With the backoff now
context-aware, a cancel during that sleep returned immediately and the
loop continued without ever handing control back to the FSM runner
(which only observes cancellation between states) — delaying shutdown
and risking a tight loop if more malformed messages arrived. Return
early when ctx is done instead, matching the scanner.Err() path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@myleshorton
myleshorton merged commit dc7d5ec into main Jul 17, 2026
2 checks passed
myleshorton added a commit to getlantern/lantern-box that referenced this pull request Jul 17, 2026
Bumps github.com/getlantern/broflake c4d1516 → f2cacf69, pulling the
consumer-side unbounded fixes into lantern-box's unbounded outbound:

- getlantern/unbounded#371: engine teardown no longer leaks the bus
  observer, table routers, UI ticker, or a live PeerConnection per
  connect/disconnect cycle.
- getlantern/unbounded#372: consumer signaling backoff is context-aware,
  so a torn-down outbound exits its retry backoff immediately instead of
  lingering a full ErrorBackoff.
- getlantern/unbounded#373: QUICLayer initializes ctx/cancel in its
  constructor, fixing the Close() data race + lost-cancel leak.

broflake is a direct dependency here (protocol/unbounded imports
broflake/clientcore), so this is the correct home for the bump — radiance
picks it up by bumping lantern-box rather than overriding the transitive
version. Addresses getlantern/engineering#3698. Ran go mod tidy.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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