Skip to content

GROOVY-12571: end a subscriber that arrives after a BroadcastChannel closed like one attached before - #3105

Open
codeconsole wants to merge 1 commit into
apache:masterfrom
codeconsole:c3-broadcast-late-subscriber
Open

codeconsole wants to merge 1 commit into
apache:masterfrom
codeconsole:c3-broadcast-late-subscriber

Conversation

@codeconsole

Copy link
Copy Markdown
Contributor

BroadcastChannel.asPublisher() now ends a subscriber that arrives after the broadcast closed the same way as one that was attached before the close. After close() it receives onComplete, so for await (x in broadcast.asPublisher()) ends without values and await broadcast.asPublisher() returns null. Before, both threw ChannelClosedException. After close(failure) it receives failure itself, not a ChannelClosedException wrapping it. This matches SubmissionPublisher, Agent.changes() and for await over a closed AsyncChannel, and makes the result independent of whether the consumer subscribed before or after the producer closed. BroadcastChannel.subscribe() still throws ChannelClosedException once the broadcast is closed. The async channels guide says so.

BroadcastChannel shipped in 6.0.0, so this changes behavior there; it is a candidate for master only.

Tests

BroadcastChannelAsPublisherTest (marked // GROOVY-12571): a subscriber and a for await that arrive after close() and after close(failure); they fail without the change.

The groovy.concurrent tests pass (271), as do the Checkstyle and CodeNarc gates.

…closed like one attached before

BroadcastChannel.asPublisher() signalled onError(ChannelClosedException)
to a subscriber that arrived after the broadcast had closed. After a
normal close, `for await` over the publisher threw ChannelClosedException
instead of ending without values. After close(failure) it threw the
wrapper instead of the failure. A subscriber attached a moment before the
close completes, or receives the failure itself, so the same program
either finished or failed depending on whether the consumer subscribed
before or after the producer closed.

A late subscriber now receives onComplete after a normal close and
onError with the retained failure after close(failure), straight after
onSubscribe. That matches SubmissionPublisher, Agent.changes() and
`for await` over a closed AsyncChannel. BroadcastChannel.subscribe()
still throws ChannelClosedException, as before.
@testlens-app

testlens-app Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

🚨 All tests passed but jobs failed 🚨

Failed Jobs without Test Failures

❌ Build and test / lts (17, macos-latest)

🏷️ Commit: 43ec544
▶️ Tests: 99364 executed
⚪️ Checks: 26/26 completed

Rerun Controls

Click the checkbox to trigger a rerun:

  • Rerun jobs

Learn more about TestLens at testlens.app/docs.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 73.6788%. Comparing base (5b077f9) to head (43ec544).
⚠️ Report is 4 commits behind head on master.

Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##               master      #3105        +/-   ##
==================================================
+ Coverage     73.6711%   73.6788%   +0.0077%     
- Complexity      42584      42590         +6     
==================================================
  Files            1678       1678                
  Lines          146907     146916         +9     
  Branches        27790      27793         +3     
==================================================
+ Hits           108228     108246        +18     
+ Misses          28528      28519         -9     
  Partials        10151      10151                
Files with missing lines Coverage Δ
.../main/java/groovy/concurrent/BroadcastChannel.java 78.2946% <100.0000%> (+4.4850%) ⬆️

... and 15 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

GROOVY-12571 behavior change is coherent with Reactive Streams: a late asPublisher() subscriber after close should see the same terminal signal as an in-flight one — onComplete for normal close, onError(cause) for close(failure) — not ChannelClosedException.

Implementation matches closedNow(): normal close has null cause → onComplete; failed close wraps the failure as CCE cause → onError(cause). Docs and tests (subscriberArrivingAfterANormalCloseCompletes / …FailedCloseReceivesTheFailure, including for await / await) are good. Direct subscribe() still throws CCE — correctly left alone.

CI: lts (17, macos-latest) 60m Gradle timeout — please re-run; then APPROVE. Codecov clean.

Compat: callers that caught ChannelClosedException from late publisher subscribe will now see completion/error instead — call that out on the JIRA if not already.

This branch has not been deployed

No deployments
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.

3 participants