Skip to content

Harden broadcast fan-out ownership and failures - #490

Open
goodboy wants to merge 6 commits into
mainfrom
wkt/fix_broadcast_lag_count
Open

Harden broadcast fan-out ownership and failures#490
goodboy wants to merge 6 commits into
mainfrom
wkt/fix_broadcast_lag_count

Conversation

@goodboy

@goodboy goodboy commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Harden broadcast fan-out ownership and failures

Motivation

BroadcastReceiver fan-out had several edge cases where subscriber
state stopped matching actual retained data or task ownership. Lag
counts were off by one, statistics exposed cursor indexes, shared
receive failures could strand peers, and closing one subscriber could
wake or cancel another subscriber's source-read path.

Downstream users also had no public way to select non-strict lag
handling, which forced private receiver mutation. This branch makes
lag, terminal-state and close behavior explicit and gives stream
subscribers a public per-child lag policy.


Summary of changes

  • Report exact displaced-value counts and receivable queue depths for
    caught-up, queued and lagged subscribers.

  • Publish shared source failures as terminal broadcast state, wake
    all peers, and replay durable EOC without letting peers re-enter a
    closed source.

  • Give each BroadcastReceiver private source-read and peer-wait
    close scopes so closing one child cannot disturb siblings.

  • Bound cancelled-task diagnostics to receiver progress, terminal
    state and resource lifetime; reject non-positive retention
    capacity.

  • Add raise_on_lag to MsgStream.subscribe() and
    LinkedTaskChannel.subscribe() for caller-selected strict or
    warn/drop/resume behavior.

  • Close wrapper-owned broadcaster roots during explicit stream
    teardown without re-entrant EOC closure or breaking
    MsgStream.aclose() overrides.

  • Add deterministic broadcast, real IPC and infected-asyncio
    regressions for lag, failure, close and lifecycle interleavings.


TODOs before landing

  • Land PR #475 first. Its cpu_perf_headroom() timeout
    scaling fixes the unrelated macOS
    test_lifetime_stack_wipes_tmpfile CI failure seen on this branch.

Future follow up

  • Add a public duplex child-stream context interface so
    downstream consumers do not need to graft a parent MsgStream.ctx
    onto a BroadcastReceiver for send-side controls.

Links

  • PR #475 - supplies the macOS CI timeout fix required by
    this branch.
  • piker follow-up: cached quote consumers use these public subscriber
    and lifecycle contracts.

(this pr content was generated in some part by opencode using
gpt-5.6-sol (openai))

`BroadcastReceiver.receive_nowait()` treated `seq` as a deque index
but subtracted `BroadcastState.maxlen` without counting the first
invalid index. A one-slot queue thus claimed it dropped zero values
after its subscriber missed one.

Include that first displaced value in the count. Preserve the
existing Tokio-style reset to the oldest retained item.

Also, cover exact loss reporting and recovery for one- and
three-slot retention windows.

Prompt-IO: ai/prompt-io/opencode/20260811T233833Z_7cbd64ee_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
`BroadcastState.subs` stores each receiver's next unread deque
index, but `.statistics()` exposed that index as a queue length. A
caught-up receiver looked correct by accident while every queued
count was one short.

Convert cursors to retained, receivable counts and clamp lagged
receivers to the current queue length. Also avoid deprecated
`trio.Event` truthiness when reporting waiter counts.

Cover caught-up, queued, lagged and real-event states using actual
broadcast sends and receives.

Prompt-IO: ai/prompt-io/opencode/20260812T012324Z_06c4af17_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
Only `EndOfChannel` and direct cancellation woke tasks waiting
behind the subscriber which owned the underlying receive. Any other
failure cleared `BroadcastState.recv_ready` while peers remained
blocked on its unreachable event.

Publish ordinary receive exceptions as terminal broadcast state.
The owner keeps the original failure while peers drain retained
values and then raise `BroadcastReceiveError` from that cause. Also
wake peers on process-control exits without retaining them as state.

Document the public owner/peer contract and cover current, late and
control-flow subscribers with deterministic bounded regressions.

Prompt-IO: ai/prompt-io/opencode/20260812T030608Z_1095e7f7_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
Closing any subscriber set the shared `recv_ready` event, even when
another receiver owned the source read. Waiting peers then looped
until an idle source produced another value.

Give each receiver private source-read and peer-wait cancellation
scopes. Closing a waiting peer interrupts only that peer. Closing
the source owner discards post-close source outcomes and then wakes
peers for a clean ownership handoff.

Keep outer task cancellation as `trio.Cancelled`; only explicit
receiver close maps either private scope's cancellation to
`ClosedResourceError`. Assert that scope cancellation implies the
receiver is closed and document the source-owner key check.

Prompt-IO: ai/prompt-io/opencode/20260812T150027Z_c2a6ccef_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
`MsgStream.subscribe()` and `LinkedTaskChannel.subscribe()` omitted
`BroadcastReceiver.raise_on_lag`, forcing downstream consumers to
mutate a private receiver attribute when overruns were acceptable.

Add `raise_on_lag` to both public wrappers. The first subscription
sets the irreversible root broadcaster's policy, while every child
selects its own strict or warn/drop/resume behavior independently.

Document both fan-out APIs. Cover policy forwarding plus real IPC
and infected-asyncio paths.

Prompt-IO: ai/prompt-io/opencode/20260812T213117Z_51185487_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
Bound `BroadcastState.cancelled` entries to receiver progress,
terminal state and resource lifetime instead of retaining completed
`Task`s indefinitely.

Deats,
- make EOC durable so awakened peers never re-enter a closed source.
- close root broadcasters during explicit `MsgStream` and
  `LinkedTaskChannel` teardown without re-entrant EOC closure or
  breaking `MsgStream.aclose()` overrides.
- reject non-positive fan-out retention capacity before constructing
  an unusable zero-length queue.
- cover child/root cancellation cleanup, terminal peer wakeups,
  wrapper teardown, subclass compatibility and zero-buffer rejection.

Prompt-IO: ai/prompt-io/opencode/20260813T181901Z_a2e0df4b_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
@goodboy

goodboy commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

The current macOS CI failure in test_lifetime_stack_wipes_tmpfile[error_in_child=True] is unrelated to the broadcast changes and is already fixed by #475 in commit 3b95ea22, which scales that test deadline with cpu_perf_headroom(). PR #490 depends on #475 landing first for that CI fix; the duplicate patch is intentionally not included here.

@goodboy goodboy changed the title Fix BroadcastReceiver lag counts Harden broadcast fan-out ownership and failures Aug 14, 2026
@goodboy
goodboy marked this pull request as ready for review August 14, 2026 00:26
Copilot AI lite review requested due to automatic review settings August 14, 2026 00:26

Copilot AI 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.

Pull request overview

This PR hardens the Trio broadcast fan-out implementation so subscriber ownership, lag accounting, and terminal failures behave deterministically and don’t strand peers. It also exposes per-subscriber lag policy through public wrapper APIs and adds regressions + docs to lock in the intended semantics.

Changes:

  • Make broadcast fan-out lifecycle/ownership explicit (private source-read + peer-wait cancel scopes; terminal EOC/receive-failure state is durable and wakes peers).
  • Fix lag drop counts and broadcast statistics to report queued counts (not cursor indexes), and reject non-positive buffer sizes.
  • Add raise_on_lag to MsgStream.subscribe() and LinkedTaskChannel.subscribe(), document behavior, and expand test coverage for lag/failure/close interleavings.

Reviewed changes

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

Show a summary per file
File Description
tractor/trionics/_broadcast.py Hardens broadcast receiver state machine (lag counts, terminal failure/EOC propagation, close scoping, stats).
tractor/trionics/init.py Exports BroadcastReceiveError as public API.
tractor/to_asyncio.py Adds raise_on_lag to linked-channel subscriptions and closes wrapper-owned root broadcaster on teardown.
tractor/_streaming.py Adds raise_on_lag to MsgStream.subscribe() and prevents EOC-driven internal close from re-entrantly closing the root broadcaster.
tests/test_task_broadcasting.py Adds deterministic regressions for lag counts, stats reporting, terminal failure/EOC, cancellation diagnostics, and wrapper forwarding/teardown.
docs/guide/streaming.rst Documents per-subscriber lag policy and the “first subscription fixes root policy” behavior.
docs/guide/asyncio.rst Documents per-subscriber lag policy for asyncio-linked channels.
docs/api/trionics.rst Documents BroadcastReceiveError semantics and how underlying failures are delivered to peers.
ai/prompt-io/opencode/20260813T181901Z_a2e0df4b_prompt_io.raw.md Adds opencode prompt I/O artifact for this PR iteration (raw).
ai/prompt-io/opencode/20260813T181901Z_a2e0df4b_prompt_io.md Adds opencode prompt I/O artifact for this PR iteration (summary).
ai/prompt-io/opencode/20260812T213117Z_51185487_prompt_io.raw.md Adds opencode prompt I/O artifact for prior iteration (raw).
ai/prompt-io/opencode/20260812T213117Z_51185487_prompt_io.md Adds opencode prompt I/O artifact for prior iteration (summary).
ai/prompt-io/opencode/20260812T150027Z_c2a6ccef_prompt_io.raw.md Adds opencode prompt I/O artifact for prior iteration (raw).
ai/prompt-io/opencode/20260812T150027Z_c2a6ccef_prompt_io.md Adds opencode prompt I/O artifact for prior iteration (summary).
ai/prompt-io/opencode/20260812T030608Z_1095e7f7_prompt_io.raw.md Adds opencode prompt I/O artifact for prior iteration (raw).
ai/prompt-io/opencode/20260812T030608Z_1095e7f7_prompt_io.md Adds opencode prompt I/O artifact for prior iteration (summary).
ai/prompt-io/opencode/20260812T012324Z_06c4af17_prompt_io.raw.md Adds opencode prompt I/O artifact for prior iteration (raw).
ai/prompt-io/opencode/20260812T012324Z_06c4af17_prompt_io.md Adds opencode prompt I/O artifact for prior iteration (summary).
ai/prompt-io/opencode/20260811T233833Z_7cbd64ee_prompt_io.raw.md Adds opencode prompt I/O artifact for initial iteration (raw).
ai/prompt-io/opencode/20260811T233833Z_7cbd64ee_prompt_io.md Adds opencode prompt I/O artifact for initial iteration (summary).

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +145 to 147
# Retain the latest interrupted source-reader task until its
# receiver next makes progress or closes.
cancelled: dict[int, Task] = {}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants