Skip to content

unbounded: keep consumer Source on close events (fix orphaned globe arcs + runaway helped counter)#512

Open
myleshorton wants to merge 1 commit into
fisk/peer-unbounded-integrationfrom
fisk/unbounded-conn-source-on-close
Open

unbounded: keep consumer Source on close events (fix orphaned globe arcs + runaway helped counter)#512
myleshorton wants to merge 1 commit into
fisk/peer-unbounded-integrationfrom
fisk/unbounded-conn-source-on-close

Conversation

@myleshorton

Copy link
Copy Markdown
Contributor

Problem

The Unbounded donor tab's "People being helped right now" counter runs away (a tester saw 131 while the pill still said "Waiting for connections…"), and it equals "Total people helped to date" — the live count only ever grows. The globe also accumulates arcs that never disappear.

Root cause is here in unbounded.go. broflake's OnConnectionChangeFunc hands us the consumer's addr on accept but a nil addr on close (the WebRTC session is already torn down, so the remote IP is gone). We emit ConnectionEvent{Source: addr.String()}, so a close goes out with Source == "".

Downstream, both consumers of the event identify a connection by its Source IP:

  • the Flutter globe matches a close to the arc it should remove by source IP;
  • the "people helped" counter decrements by source IP.

So an empty-Source close can't be matched — the arc orphans and the counter never comes back down. broflake does fire the close (it has to, to recycle its small consumer table), so these aren't missing events; they just arrive un-attributable. (The 131 magnitude was amplified by a separate consumer-routing flood, throttled on the lantern-cloud side.)

Fix

Track each consumer slot's addr on accept and restore it on close, so every -1 carries the same Source its +1 did:

  • connSources — a small workerIdx → addr map (broflake's workerIdx is the consumer slot, stable across a single connection's accept→close), concurrency-safe since broflake fires callbacks from per-worker goroutines.
  • OnConnectionChangeFunc calls sources.resolve(state, workerIdx, addrStr) — records on accept, restores on close.

No change to the ConnectionEvent shape or to any consumer; they already key off Source. An accept that genuinely has no addr stays empty through its close (neither end is counted), which is the correct behavior when broflake can't surface the consumer IP at all.

Tests

  • TestConnSources_resolve — unit-tests the backfill: accept stores, nil-addr close restores, freed slot restores nothing, slots independent, empty-addr accept stays empty, real-addr close passes through, slot reuse.
  • TestConnectionEventBridge — updated to assert the installed callback now emits the accept's IP on the close (was asserting the old empty-Source bug). Passes with -race.

broflake's OnConnectionChange delivers the consumer addr on accept but a
nil addr on close (the WebRTC session is already torn down, so the remote
IP is gone). The close ConnectionEvent therefore carried an empty Source,
and downstream consumers couldn't pair it with its accept: the Flutter
globe matches the arc to remove by source IP, and the "people helped"
counter decrements the same way. So closes were dropped — arcs orphaned
and accumulated, and the live counter only ever grew (it equalled the
lifetime total).

Track each consumer slot's addr on accept (connSources, keyed by
broflake's workerIdx — stable across a connection's accept->close) and
restore it on close, so every -1 carries the same Source its +1 did. No
event-shape change; downstream consumers already key off Source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 7, 2026 00:01

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 fixes Unbounded connection close events emitting an empty Source (when broflake provides nil addr on close), which prevents downstream consumers from matching close events to prior accepts—leading to orphaned globe arcs and a “People being helped right now” counter that never decrements.

Changes:

  • Add connSources to cache per-consumer-slot (workerIdx) source IPs on accept and restore them on close.
  • Update the broflake connection-change callback to backfill Source on close events.
  • Add/adjust unit tests to pin the accept→close Source backfill behavior.

Reviewed changes

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

File Description
unbounded/unbounded.go Introduces connSources and uses it to backfill Source on close events.
unbounded/unbounded_test.go Updates the bridge test to assert close events preserve the accept’s Source.
unbounded/conn_sources_test.go Adds unit coverage for connSources.resolve behavior.

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

Comment thread unbounded/unbounded.go
Comment on lines +94 to +97
case state > 0:
if addrStr != "" {
c.addrs[workerIdx] = addrStr
}
Comment on lines +50 to +55
// Slot reuse: broflake recycles a workerIdx; a fresh accept overwrites
// the prior addr even without an intervening close.
c.resolve(1, 12, "2.2.2.2")
c.resolve(1, 12, "3.3.3.3")
assert.Equal(t, "3.3.3.3", c.resolve(-1, 12, ""),
"reused slot restores the latest accept's addr")
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