dbsp: Give every stream in a nested circuit an id of its own - #6853
Merged
Conversation
A child circuit receives a copy of the parent's stream id counter rather than
the counter itself: `last_stream_id()` returns `RefCell<StreamId>` by value, and
cloning a `RefCell` clones the value inside it. The child then hands out ids
that the parent hands out again, so ids are not unique, contrary to what
`allocate_stream_id` and the comment on `last_stream_id` promise.
One of the ids a child allocates belongs to a stream in the *parent*:
`FeedbackOutputNode::with_export` allocates the id of the stream a scope exports
from inside the child. When that id has also gone to a stream in the parent,
edge bookkeeping becomes ambiguous, and `complete_replay` deletes edges by
stream id:
self.circuit.edges_mut().delete_stream(*stream_id);
A recursive pipeline hit exactly that on 0.291.1, where the fix for #6765 makes
operators inside recursive scopes checkpoint: each of the program's 12 scopes
exported a stream whose id had also gone to the replay stream of the accumulate
trace consuming it, so ending a bootstrap deleted `Subcircuit -> Consolidate`,
the live edge carrying the scope's output. With no predecessor left, that
Consolidate ran in the first batch of every following step, ahead of the scope,
and `StreamValue::peek` unwrapped `None` on the export stream that nothing had
written. Whether a program collides this way depends on how its ids fall, so
the same defect is latent here.
Share the counter. Stream ids exist only at runtime, so nothing on disk
changes.
The new test asserts the invariant that broke: no two producers may share a
stream id. Without the fix it reports "stream s5 is produced by both n1 and
n5".
Signed-off-by: Leonid Ryzhyk <leonid@feldera.com>
mihaibudiu
approved these changes
Aug 13, 2026
github-merge-queue
Bot
removed this pull request from the merge queue due to failed status checks
Aug 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stream ids are documented as globally unique, and
Edgesbookkeeping, thereplay machinery and several circuit caches key on them. They are not unique.
A child circuit receives a copy of the parent's counter rather than the counter:
so the child hands out ids that the parent then hands out again. One of the ids
a child allocates belongs to a stream in the parent:
FeedbackOutputNode::with_exportallocates the id of the stream a scope exportsfrom inside the child.
What it broke
CircuitHandle::complete_replayends a bootstrap by deleting the replay edgesby stream id:
On 0.291.1 (the 6765 fixes backported, so operators inside recursive scopes
checkpoint), a recursive pipeline resumed from a checkpoint panicked at
circuit_builder.rs:144,Option::unwrap()onNoneinStreamValue::peek,at 2, 4 and 8 workers, immediately after "Bootstrap complete".
Instrumenting the schedule showed why. Each of the program's 12 recursive scopes
exported a stream whose id had also gone to a root level stream created later,
in every case the replay stream of the accumulate trace that consumes the
export:
Deleting the replay edges took
n579 -> n580with them, so the schedulerprepared
Consolidate [580]with no predecessors:From then on the Consolidate ran in the first batch of every step, ahead of the
scope, and read an export stream that nothing had written:
Whether a program collides depends on how its ids fall, which is why main has
not tripped over it: the same program bootstraps cleanly here. The defect is the
same.
The fix
Share the counter, as the comment on
last_stream_idalways claimed. Stream idsexist only at runtime; nothing on disk changes, and no fingerprint or persistent
id derives from them.
Testing
stream id. It fails without the fix, on this branch and on main:
stream s5 is produced by both n1 and n5.cargo test -p dbsp --lib, 435 tests including the replay tests: pass (run inthe 0.291.1 tree where the fix originated).
completes: bootstrap finishes and
resultmatches a reference run row for rowafter each of the following input batches.
🤖 Generated with Claude Code