Skip to content

Test that one subscriber leaving keeps the multicast upstream for the other - #73

Merged
twittemb merged 1 commit into
sideeffect-io:mainfrom
lhoward:upstream/multicast-two-subscriber-tests
Oct 4, 2026
Merged

twittemb merged 1 commit into
sideeffect-io:mainfrom
lhoward:upstream/multicast-two-subscriber-tests

Conversation

@lhoward

@lhoward lhoward commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Four tests for AsyncMulticastSequence with two subscribers: subscriber A leaves, by task cancellation or by releasing its iterator, under connect() and under autoconnect(), and subscriber B must keep receiving the shared upstream's elements and finish with it.

They exercise the case noted when closing #38 ("cancelling one subscriber terminates the shared upstream for another subscriber"), which fd6753e fixed by pulling the upstream on a task of its own. A hand-fed AsyncThrowingStream with an expectation on entering next() makes A the puller deterministically before B starts; no sleeps.

A's iterator is created inside its task, so no copy of it outlives A. The test waits for A to finish while upstream is still silent, checks that the subject (read through @testable import) has dropped A's registration, and only then sends elements; A's own result is asserted too.

On Linux (Swift 6.4, x86_64), rebased on b4adff5: the multicast suites pass (11 tests), 20 runs in a row. With the multicast implementation from before fd6753e the two cancellation tests fail; with a retained copy of A's iterator the two release tests fail on the registration check.

🤖 Generated with Claude Code

https://claude.ai/code/session_01T8cahEd4GfsHQ89sNrmP4p

@twittemb

twittemb commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

These are useful scenarios to cover: when one subscriber leaves, the other should keep receiving values. Covering both connect() and autoconnect() makes sense. However, I found two issues with how the tests currently check this.

  1. The cancellation tests can pass even when cancellation is broken.

    After cancelling A, the test immediately sends another upstream value. That value can wake A up, so the test still passes even if A was stuck waiting for upstream instead of responding to cancellation.

    I checked this in an isolated checkout by restoring the multicast implementation from before the cancellation fix in fd6753e. All four new tests still passed. The existing test_cancelling_subscriber_waiting_for_upstream_finishes_without_cancelling_shared_stream correctly failed because A did not finish before upstream emitted.

    Please cancel A and wait for A to finish while upstream is still silent. Only then send values and check that B receives them and finishes normally. Also await A's task result so an unexpected error cannot go unnoticed.

  2. The iterator-release tests keep a copy of A's iterator alive.

    iteratorA is created outside the task, and the task makes another copy of it. These copies share one subscription. Finishing the task does not prove that the last copy has been released.

    In an instrumented debug run, I waited for A's task to complete and checked the subject before sending the remaining values: it still had two registered subscribers in both release tests.

    Please arrange the test so all copies of A's iterator are released when A leaves, and verify that A is no longer registered before sending more values to B.

Validation was on macOS with Swift 6.4, applying this PR's test file to current main: all 11 multicast tests passed. The checks above show why passing alone does not yet establish the two behaviours the new tests intend to protect. I would strengthen these tests before merging.

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

See my comment

… other

Two subscribers share a hand-fed upstream; the one holding the pull is
cancelled, or releases its iterator, while the other must still receive
every element and finish normally. Covers connect() and autoconnect().

A's iterator is created inside its task, so no copy outlives it. The
test waits for A to finish while upstream is still silent, checks that
the subject has dropped A's registration, and only then sends elements.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T8cahEd4GfsHQ89sNrmP4p
@lhoward
lhoward force-pushed the upstream/multicast-two-subscriber-tests branch from 697dc49 to fbc8f2f Compare October 4, 2026 10:43
@twittemb
twittemb merged commit 20d9524 into sideeffect-io:main Oct 4, 2026
4 checks passed
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