Skip to content

fix: Prevent a full status listener from hanging Close - #498

Open
beekld wants to merge 6 commits into
v7from
bklimt/SDK-3425/broadcaster-unblock
Open

beekld wants to merge 6 commits into
v7from
bklimt/SDK-3425/broadcaster-unblock

Conversation

@beekld

@beekld beekld commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

#489 made FDv2.Stop() broadcast an Off status after its goroutines exit. A broadcast does a blocking send to each listener channel, which buffers 10 values. If an application listener stops reading while its buffer is full, Stop() blocks forever. That hangs LDClient.Close(), and GetStatus() hangs with it, because the send ran under the status lock.

The hazard predates #489. Any status update can block the same way while the data system runs, and the broadcaster holds its lock during the send. So removing the stuck listener also blocks.

This PR:

  • Makes RemoveListener and Close unblock a send to a full listener. Listeners that keep reading still get every value, in order. No broadcast drops events.
  • Sends the FDv2 data source status broadcast after the status lock is released, as FDv1 already does. GetStatus() no longer waits on a listener.
  • Stops broadcasting Off from FDv2.Stop(). The status still changes to Off, and listeners learn of shutdown when their channels close.
  • Removes the drain goroutines in the FDv1 and FDv2 WaitFor that only existed to work around the blocking removal.
  • Makes WaitFor return false when its listener channel closes, in both FDv1 and FDv2. Before, a WaitFor still running at Close() spun on the closed channel until its timeout.

Note

Overview
Fixes deadlocks when a data-source status listener stops reading while its buffered channel is full—LDClient.Close(), Stop(), RemoveListener, and GetStatus() could block because broadcasts sent under locks.

The shared Broadcaster now sends without holding the subscriber list lock, and each subscriber uses a done signal so RemoveListener/Close can abort an in-flight send to a full channel. Active readers still receive every event in order.

FDv2 mirrors FDv1 by broadcasting status after releasing the status mutex via maybeUpdateStatus. Stop() sets state to Off locally but does not broadcast Off; shutdown is signaled by closing listener channels. WaitFor no longer spins drain goroutines and returns false when the listener channel closes.

Reviewed by Cursor Bugbot for commit c1e6e39. Bugbot is set up for automated code reviews on this repo. Configure here.

@beekld
beekld added this pull request to stack #503 November 2, 2026 21:04
@beekld
beekld marked this pull request as ready for review November 2, 2026 21:05
@beekld
beekld requested a review from a team as a code owner November 2, 2026 21:05

@cursor cursor 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.

Stale Bugbot comment from a previous run.

Comment thread internal/broadcasters.go Resolved
@beekld
beekld force-pushed the bklimt/SDK-3425/broadcaster-unblock branch from 036e2f4 to c1e6e39 Compare November 3, 2026 00:05

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c1e6e39. Configure here.

case status := <-ch:
case status, ok := <-ch:
if !ok {
return false

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WaitFor misses Off after stop

Medium Severity

Stop() now sets Off without broadcasting, then closes listener channels. WaitFor treats that close as failure and returns false without reading the current status, so a wait already in progress for DataSourceStateOff fails even though the data system did reach Off.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c1e6e39. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This was already the case before #489, and this PR just reverts to that previous behavior. We should address broadcasting Off, but in a later PR.

@kinyoklion kinyoklion left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approved with consideration of 505.

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