Skip to content

Implement and document thread-safe use of the iterate API#6653

Open
lstwn wants to merge 5 commits into
feldera:mainfrom
lstwn:recursion_via_iterate
Open

Implement and document thread-safe use of the iterate API#6653
lstwn wants to merge 5 commits into
feldera:mainfrom
lstwn:recursion_via_iterate

Conversation

@lstwn

@lstwn lstwn commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

This PR makes it possible to construct recursive circuits using the lower-level
iterative API in a thread-safe manner:

  1. It exposes machinery to manually construct recursive circuits using the
    lower-level iterate API instead of the higher-level recursive API, that
    is, it publicly exports Consensus to make such circuits thread-safe.
  2. The four sibling methods iterate_with_condition, iterate_with_conditions,
    iterate_with_condition_and_scheduler, and
    iterate_with_conditions_and_scheduler are now thread-safe by wrapping
    their condition(s) in a Consensus.
  3. It updates tutorial10.rs and tutorial11.rs to show semantically
    equivalent circuits using the iterate API next to the recursive API and
    does a very basic run time comparison among the two to demonstrate the
    extra distinct happening behind the scenes and its costs.
  4. It contributes the new tutorial tutorial13.rs which does the same thing
    for mutually recursive circuits by reusing the graph coloring example from
    the documentation of recursive_dynamic.

Here are some rough numbers taken from my machine:

Tutorial Speedup with iterative vs recursive
10 1.5-2.0
11 1.0-1.15
13 1.0-1.1

Hence, the circuits constructed with the lower-level iterate API seem to be
at least equally fast or faster even with this little toy data (I expect the
gap to be bigger with larger datasets). The difference is particularly relevant
if you happen to know that your data is acyclic in which case you don't need to
pay for deduplication, as with tutorial 10.

Open Questions

  • @ryzhyk: Your patch on Discord added the is_fixedpoint method which in turn
    simply delegates to the existing check_fixedpoint method which is already
    publicly exposed. Do you still want to keep that extra wrapper method?
  • Maybe there is the opportunity to create a similar helper API to
    RecursiveStreams which allows to specify to opt-out of an implicit
    distinct (at the expense of non-termination in some cases; exposing a
    similar tradeoff as with UNION vs UNION ALL in SQL in its recursive CTEs),
    and which additionally supports a recursion/iteration depth limit?
    What do you think? Or would you consider this as an advance use case and
    leave that to be done manually as shown in the updated tutorials?

Checklist

  • Tests added/updated
  • Documentation updated
  • No changelog updates required

@mythical-fred mythical-fred 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.

Nice fix — Consensus-wrapping the four iterate_with_condition* variants closes a real cross-worker termination bug, and the iterate_with_condition_multi_worker regression test is exactly what I would have asked for. The refactor of tutorial 10/11 into paired recursive/iterative variants against a shared transitive_closure helper reads well, and tutorial13 mirrors that structure. A few blockers and nits below.

Blockers:

  1. Commit is not DCO-signed. git log -1 031f525ec has no Signed-off-by: trailer. CONTRIBUTING.md requires DCO sign-off on every commit. Please amend with git commit --amend -s and force-push.
  2. CI has not run on this branch (gh pr checks 6653 reports "no checks reported on the 'recursion_via_iterate' branch"). After the amend/force-push, please confirm the workflows execute and go green — this touches enough of the iteration machinery that I want to see the test suite before signing off.

Answers to your open questions in the PR body:

  • On is_fixedpoint vs check_fixedpoint: I lean toward dropping the wrapper. check_fixedpoint is already a public trait method with the same signature; adding is_fixedpoint just gives it a second name (with better rustdoc, admittedly). Cleaner alternatives: either move your new rustdoc onto check_fixedpoint itself, or if is_fixedpoint is the preferred user-facing spelling, deprecate check_fixedpoint on that same PR. Leo, up to you — I would not block on this, but two aliases for one operation is a papercut.
  • On a RecursiveStreams-like helper that opts out of implicit distinct and adds a depth limit: yes, worth having, but as a follow-up. This PR is already big enough. Ship the tutorials first; the helper can crystallize out of the shape of make_termination_check once someone writes the second real user of it.

Comment thread .gitignore Outdated
Comment thread crates/dbsp/examples/tutorial/tutorial10.rs Outdated
Comment thread crates/dbsp/examples/tutorial/transitive_closure/mod.rs Outdated
Comment thread crates/dbsp/examples/tutorial/transitive_closure/mod.rs Outdated
Comment thread crates/dbsp/src/circuit/circuit_builder.rs
Comment thread crates/dbsp/src/circuit/runtime.rs
Comment thread crates/dbsp/src/operator/condition.rs Outdated
Comment thread crates/dbsp/src/operator/condition.rs Outdated
/// require that a multi-worker run computes the same reachable set as a
/// single-worker run, across repeated builds to shake out scheduling races.
#[test]
fn iterate_with_condition_multi_worker() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Good regression test. Two small extensions worth considering: (1) exercise the iterate_with_conditions (plural) path too — that variant folds an .iter().all(...) across a Vec<Condition> inside the same Consensus, and its per-worker eagerness is a separate code path; (2) the current test loops 5×; on my mental model that is enough to shake most races out, but if you have a #[cfg(feature = "stress")] or similar knob elsewhere in the crate, cranking it to a few hundred for the CI stress job would be cheap.

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.

For now, I leave it as is and let others decide if that's compatible with the CI pipeline here or not.

@mihaibudiu

Copy link
Copy Markdown
Contributor

Maybe there is the opportunity to create a similar helper API to
RecursiveStreams which allows to specify to opt-out of an implicit
distinct (at the expense of non-termination in some cases; exposing a
similar tradeoff as with UNION vs UNION ALL in SQL in its recursive CTEs),
and which additionally supports a recursion/iteration depth limit?
What do you think? Or would you consider this as an advance use case and
leave that to be done manually as shown in the updated tutorials?

I think these are two orthogonal changes. You should probably file issues.
We have an issue about bounded recursion, but at the SQL compiler level; having support in DBSP would help: #3318

@ryzhyk
ryzhyk self-requested a review July 16, 2026 23:29
@ryzhyk ryzhyk added the DBSP core Related to the core DBSP library label Jul 16, 2026

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

Can you reorganize the code structure a bit?
If the data generation is shared between examples, put it into its own file.
Each tutorial example should be self-contained.
If you have negative examples, label the files carefully, so people don't take it as a positive example.

.into_iter();

let edges = root_circuit.add_source(Generator::new(move || edges_data.next().unwrap()));
// Play around with different thread setups on your machine. With this little

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.

I thought that rust comes with some very nice benchmarking tools like Criterion. Why not use these?

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.

At some point I'd love to this but I'd do that in a separate PR. So far I think it is educational to see how to manually construct an equivalent circuit to the ones created by the recursive API through using the lower-level iterative API. If you don't want to see any stats as an indication of what's going on behind the scenes, I'm fine with deleting them. However, I think especially for tutorial 10 the performance gain from dropping the distinct is part of the tutorial.

Comment thread crates/dbsp/examples/tutorial/transitive_closure/mod.rs Outdated
Comment thread crates/dbsp/examples/tutorial/transitive_closure/mod.rs Outdated
Comment thread crates/dbsp/examples/tutorial/transitive_closure/mod.rs Outdated
Comment thread crates/dbsp/examples/tutorial/tutorial10.rs Outdated
Comment thread crates/dbsp/examples/tutorial/tutorial10.rs Outdated
Comment thread crates/dbsp/examples/tutorial/tutorial10.rs Outdated
drive_circuit, make_termination_check,
};

/// If we set STEPS to 3 in this tutorial, the computation will not terminate

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.

I don't understand why you are taking a functional tutorial and turning it into a broken example.
Maybe create a separate "broken-example" fille and leave this one alone

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.

No, the tutorial works just as before! Nothing is broken and in fact, not much has changed from before from a logical point of view. The computation does not terminate only if people include the data from the third transaction/step (not sure about the best terminology here; a year ago when I first contributed these tutorials the concept of a transaction didn't exist and these were usually referred to as steps).

Tutorial 10 and 11 always operated on top of the same data to make the termination issue very clear. The only difference now is that instead of having had the third transaction commented out in tutorial 10 (for people to play around with), the data for both tutorial 10 and 11 is now shared in the transitive_closure module to not have it duplicated anymore. And instead of commenting stuff, you can now control the inclusion of the third transaction with the STEPS constant.

})
}

/// Computes the transitive closure of a graph using DBSP's lower level `iterate`

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.

Why not put this into a separate file?

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.

The iterate_variants and recursive_variants of tutorial 10, 11, and 13 show two approaches to construct circuits for the exact same problems: transitive closure with acyclic data (tutorial 10), transitive closure with cyclic data (tutorial 11), as well as mutual recursion and graph coloring (tutorial 13). Now, for each tutorial we show how to construct such a circuit using the higher-level recursive API and the lower-level iterative API. I'm fond of having each tutorial deal with one problem and possibly show multiple variants of how to approach that problem with DBSP circuits. Hence, both variants still live in the same file.

Of course we can structure things following a different paradigm but I tend to write the tutorials in a way I had loved to read them when I started figuring out how to compute these problems with DBSP.

Comment thread crates/dbsp/examples/tutorial/tutorial10.rs Outdated

@mythical-fred mythical-fred 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.

Nice piece of work. The Consensus-wrapping of iterate_with_condition[_and_scheduler] (and the plural variants) is a real bugfix, not just a hardening — those methods previously let a worker whose local condition went true exit while peers still had work, which would silently produce wrong results in multi-worker runs. The iterate_with_condition_multi_worker regression test with the 5-iteration loop is exactly the right shape for shaking out the scheduling races.

The tutorial pairing (recursive vs. iterate side by side, plus mutually recursive in tutorial13) is genuinely useful documentation; publishing Consensus and the is_fixedpoint helper gives users a real path to bypass the implicit distinct when they know the shape of their data. The make_termination_check helper reads well and correctly folds the per-worker fixed-point vote through the shared Consensus, and the MAX_ITERATIONS fallback is a sensible safety net.

Three inline observations below (one perf note, one micro-nit, one API question — none are blockers). Also see the author's own open question about is_fixedpoint vs the already-public check_fixedpoint — I'd side with dropping the alias, elaborated inline.

Approving.

/// detect convergence, typically combining the per-worker results across
/// workers via [`Consensus`]. See the documentation of [`Scope`] for the
/// meaning of the parameter but passing `0` refers to the current circuit.
fn is_fixedpoint(&self, scope: Scope) -> bool {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is a pure alias for the already-public check_fixedpoint (declared on this trait at line 1864, plumbed through everywhere else). Adding a second name for the same operation isn't free — users hit two docs.rs entries doing the same thing and have to guess which is canonical. If the goal is a friendlier is_* predicate name for the iterate-based API, consider renaming check_fixedpointis_fixedpoint outright (Rust convention for -> bool predicates) and moving this docstring onto that method. If you want to keep the existing name, at least drop this alias and add the extra prose to check_fixedpoint — one canonical entry point beats two. (Also called out in the PR description as an open question for @ryzhyk.)

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.

I'll leave that up to @ryzhyk to decide! I have no preference here.

Comment thread crates/dbsp/src/operator/condition.rs Outdated
Comment thread crates/dbsp/examples/tutorial/transitive_closure/mod.rs Outdated
Comment thread .gitignore
flake.nix
flake.lock
.envrc
.direnv

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nit: flake.nix and flake.lock are project-level artifacts, not personal ones. If the team ever adds a Nix flake for the repo, ignoring them by default will silently hide someone's legitimate check-in. Personal Nix bits usually live under .envrc/.direnv (already covered here) or a locally-git-excluded flake.local.nix per convention. Consider dropping the two flake.* lines and letting anyone who wants a private flake add it to .git/info/exclude locally.

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.

I've deleted the two flake* lines and excluded them locally.

Is there interest in a flake.nix file for this project? I have one on my machine which sets up everything to have a dev environment for DBSP but not for any Java stuff required for the compiler so far. If you want, I can include this as a starting point.. Let me know!

@lstwn

lstwn commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Maybe there is the opportunity to create a similar helper API to
RecursiveStreams which allows to specify to opt-out of an implicit
distinct (at the expense of non-termination in some cases; exposing a
similar tradeoff as with UNION vs UNION ALL in SQL in its recursive CTEs),
and which additionally supports a recursion/iteration depth limit?
What do you think? Or would you consider this as an advance use case and
leave that to be done manually as shown in the updated tutorials?

I think these are two orthogonal changes. You should probably file issues. We have an issue about bounded recursion, but at the SQL compiler level; having support in DBSP would help: #3318

Good idea, I've created #6666 to discuss a new API for recursive streams which also refers to #3318 and #5193.

@lstwn
lstwn force-pushed the recursion_via_iterate branch 2 times, most recently from 4002806 to 436a721 Compare July 17, 2026 15:41
lstwn added 2 commits July 17, 2026 17:43
1. It exposes machinery to manually construct recursive circuits using the
   lower-level `iterate` API instead of the higher-level `recursive` API, that
   is, it publicly exports `Consensus` to make such circuits thread-safe.
2. The four sibling methods `iterate_with_condition`, `iterate_with_conditions`,
   `iterate_with_condition_and_scheduler`, and
   `iterate_with_conditions_and_scheduler` are now thread-safe by wrapping
   their condition(s) in a `Consensus`.
3. It updates `tutorial10.rs` and `tutorial11.rs` to show semantically
   equivalent circuits using the `iterate` API next to the `recursive` API and
   does a very basic runtime comparison among the two to demonstrate the
   extra `distinct` happening behind the scenes and its costs.
4. It contributes the new tutorial `tutorial13.rs` which does the same thing
   for mutually recursive circuits by reusing the graph coloring example from
   the documentation of `recursive_dynamic`.

Signed-off-by: Leo Stewen <lstwn@mailbox.org>
Signed-off-by: Leo Stewen <lstwn@mailbox.org>
@lstwn
lstwn force-pushed the recursion_via_iterate branch from 436a721 to 7c4d6d7 Compare July 17, 2026 15:46
@lstwn

lstwn commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the feedback, I've addressed it so far. Open questions or things you might still want to look into are left as unresolved above. Let me know :)

@mythical-fred mythical-fred 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.

All four nits from the previous round addressed. Nice bonus: with_consensus now scoped by GlobalNodeId so identical labels in different subcircuits can't collide on the broadcast, and it's reused inside fixedpoint() — good DRY. Consensus also gained a proper # Safety section describing the "same order, once per logical decision, reuse across ticks" invariants.

One soft nit for the merge: please squash Address PR feedback into the original commit before landing — feldera keeps a linear history on main and that message shouldn't survive as its own commit.

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

I like the general direction, but I don't think this is the right shape for these changes.

  1. The tutorial is intended to demonstrate the basic usage of the DBSP API to new users. I don't think that distinct-less termination belongs in this category. I would keep the tutorial as is regardless of where we go with the changes in this PR.
  2. iterate_with_condition is intended as a low-level API. It is the user's responsibility to provide a sound termination condition that guarantees that all workers stop iterating at the same time. Not all possible termination conditions require a consensus object. For example, a condition that iterates a fixed number of times doesn't require it. The new functionality you are implementing can be achieved using the current iterate_with_condition API by providing consensus object as part of the condition.
  3. Out of curiosity: do you have some example use cases where distinct is unnecessary for termination (I am sure they exist, just can't think of many off the top of my head. Perhaps some graph queries where the graph has a special shape?)

I'd like to suggest two options for improving this PR:

  1. Minimal DBSP changes: add any missing exports and implement the flexible recursion API outside of the crate, possibly as an example as opposed to a tutorial.
  2. Add a new operator to the DBSP crate (e.g., bounded_recursion). I think it's a useful extension that will benefit other users.

@mihaibudiu

Copy link
Copy Markdown
Contributor

I think he already has the example where the distincted value is the result of an aggregate, so it cannot have a weight larger than 1.

@ryzhyk

ryzhyk commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

I think he already has the example where the distincted value is the result of an aggregate, so it cannot have a weight larger than 1.

In this case, distinct should be a noop and should be harmless.

@mihaibudiu

Copy link
Copy Markdown
Contributor

According to his benchmarks it's not. But I suggested to use mark_distinct.

lstwn added 3 commits July 20, 2026 17:41
Signed-off-by: Leo Stewen <lstwn@mailbox.org>
Signed-off-by: Leo Stewen <lstwn@mailbox.org>
@lstwn

lstwn commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the feedback! I agree, maybe the tutorials become too overwhelming by including the iterate variants. I've reverted them. Also, I've removed the Consensus from the iterate_* API. The core changes to DBSP are now minimal, that is, Consensus is now part of the public API along with some documentation and there is the is_fixedpoint function on the Circuit trait to query a circuit for its fixed point state.

I've created proper benchmarks using Criterion for the three cases: transitive closure over acyclic data (which doesn't require the distinct, (1)), transitive closure over cyclic data (2), and bipartite graphs using mutual recursion (3). Here's what I found:

  1. I really shouldn't make any claims without a proper benchmarking tool. The recursive-based variants come out equally fast for the cases where distinct is required anyways, that is (2) and (3). In my previous setup the bipartite graphs tutorial only ran faster because I forgot reusing the edges indexing for the red and blue frontier in the recursive variant.
  2. In cases where you don't need a distinct (1), the iterative variant has a consistent speedup of 1.5-1.6 over the recursive variant. I tried to equalize that using mark_distinct but this did not have any effect. Am I applying it wrong? Otherwise, this might be a bug.

If we can resolve the issue with mark_distinct, I'd add some documentation on recursive and recursive_dynamic, so users know how to optionally circumvent the implicit distinct without having to dive into the low-level iterate API.

@ryzhyk: Here are some examples I know about:

  1. In hierarchical networks, reachability problems (like the transitive closure) and routing problems don't need to pay for the distinct.
  2. Static program analysis sometimes needs to traverse the abstract syntax tree which is also acyclic.

Then there is another small semantic nuance: Distinct collapses the z-weights to either 0 or 1. If you are working with an acyclic directed graph, I think the z-weights lend themselves to track multiplicity quite naturally: If there are multiple paths from node A to node B the z-weight should reflect that multiplicity without any extra aggregation step, I suppose.

)
// We explicitly mark the stream as distinct to avoid the
// unnecessary implicit distinct.
.mark_distinct();

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.

Is this the wrong place to mark the stream distinct?

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.

I recommend using the profiler in the webui to inspect the generated circuit, you will see exactly all the lowest-level operators that are being used.

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.

@mihaibudiu , there is no webui for DBSP circuits.

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.

You can use DBSPHandle::dump_profile to dump the profile in the graphviz format. Please feel free to share it here. My guess is the mark_distinct annotation is in the right place, but the annotation itself may not be accurate, i.e., the output is not distinct, and the circuit iterates more without the distinct call.

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.

And I was hoping to retire this API...

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 is the profile of the run with 500 nodes on the recursive circuit. It looks like the distinct is still applied.

Profile of worker 0:
0

Full profile of all four workers profile.zip.

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.

Claude found the issue (see below). I am not 100% sure about the output of join being sharded, but the rest of it is correct. My takeaway is that mark_distinct is a low-level API and probably shouldn't be used in user code. I like the idea of a distinct-less variant of recursive. We should probably add that instead of hacking around it.

The layer mismatch

recursive is a thin typed wrapper. The user closure returns a typed stream, and recursive immediately erases it with .inner() before handing it to dyn_recursive:

benchmark closure (TYPED)                     recursive.rs:235          dyn_recursive
──────────────────────────                    ───────────────           ─────────────
len_1.plus(..).mark_distinct()   ──►  .map(|s| s.inner())   ──►   S::distinct(stream)   (recursive.rs:252)
   writes key                          erases TYPED→DYN            = Stream::dyn_distinct  (dyn/recursive.rs:77)
   DistinctIncrementalId<                (same StreamId,             reads key
     _, OrdIndexedZSet<Path,..>>          payload type erased)       DistinctIncrementalId<_, DynInner>
        ▲                                                                     ▲
        └──────────────── different key type → cache MISS ───────────────────┘

- Accumulator = Stream<NestedCircuit, OrdIndexedZSet<Path, TransClosurePath>> (typed; Path/TransClosurePath are concrete Tup2/Tup4, not DynData). .mark_distinct() records DistinctIncrementalId<_, OrdIndexedZSet<Path,TransClosurePath>>.
- recursive (recursive.rs:235) calls streams.inner(), which is transmute_payload (circuit_builder.rs:781). It keeps the StreamId (line 784) but changes the payload type D to the erased dynamic batch.
- dyn_recursive then runs S::distinct (dyn/recursive.rs:252 → 77 → Stream::dyn_distinct), which looks up DistinctIncrementalId<_, DynInner>.

Same StreamId, different type parameter, different bucket. The mark is never consulted. This is also why the typed .distinct() itself is written as self.inner().dyn_distinct().typed() (operator/distinct.rs:42): the whole distinct/mark machinery lives at the dynamic layer, and a mark placed on a typed stream is invisible to it.

A second, independent trap (also live here)

Even if the mark were placed on the dynamic stream, dyn_distinct keys on self.try_sharded_version().stream_id() (distinct.rs:353), i.e. the sharded sibling, not the stream you marked. With WORKERS = 4 the join output is sharded, so the distinct's input carries a different StreamId than the unsharded stream. The mark would have to land on the sharded version (mirroring dyn_distinct_inner, which does .mark_sharded().mark_distinct() at distinct.rs:339-340).

So: bug or misuse?

Both, arguably. It is misuse in that mark_distinct() only works within a single type layer, and recursive crosses that layer internally, so no placement in user code reaches the exact key. It is a footgun in that mark_distinct() is public on typed streams and silently no-ops there.

Fixes worth proposing

1. Explicit opt-out in the recursion API (cleanest, matches ryzhyk's "add a bounded_recursion operator" and issue #6666): let the caller tell dyn_recursive to skip the S::distinct step (dyn/recursive.rs:252) instead of threading a mark through two boundaries. This is exactly what the low-level iterate variant already achieves by not calling distinct at all, which is why it benchmarks 1.5-1.6x faster.
2. Make the typed API honor the mark: have typed recursive forward the mark across .inner() and onto the post-shard stream, or have RecursiveStreams::distinct short-circuit when the dynamic, sharded stream is_distinct(). dyn_distinct already skips a correctly-placed dynamic mark, so this only needs the mark on the right key.

Want me to prototype option 1 (a skip_distinct/recursive_multiset variant) or write a failing regression test that pins the current no-op behavior?

@mythical-fred mythical-fred 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.

Re-reviewed at c73daf1. The design pivot — removing Consensus from iterate_with_condition* per ryzhyk's feedback and exporting Consensus as a public building block — is the right call. Criterion benchmarks are well-structured. Two nits inline; please squash before merge (see below).

/// detect convergence, typically combining the per-worker results across
/// workers via [`Consensus`]. See the documentation of [`Scope`] for the
/// meaning of the parameter but passing `0` refers to the current circuit.
fn is_fixedpoint(&self, scope: Scope) -> bool {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Still two public names for the same operation (is_fixedpoint here, check_fixedpoint at line 1963). I know this is punted to #6666 — just flagging that the alias survived the reset. If ryzhyk is fine with it, so am I; if not, one name should win before this lands.

@@ -0,0 +1,348 @@
//! Benchmark comparing two transitive-closure computation approaches on acyclic

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice benchmarks overall — deterministic seeding, correctness assertions before measuring, throughput in closure-paths/sec. The forest-only restriction is well-justified in the generate_forest docstring. One optional nit: consider adding a brief ## Running section to the module doc (or a top-level benches/README.md) noting the --bench invocations for all three benchmarks, since new contributors may not know they exist.

@mythical-fred

Copy link
Copy Markdown

Before merge: the history needs a git rebase -i pass. Right now the five commits tell a story of back-and-forth that doesn't belong on main:

  1. 5188288d describes iterate_with_condition consensus wrapping + tutorials that no longer exist at tip
  2. 7c4d6d74 "Address PR feedback" — classic fixup commit
  3. d7623962 benchmarks — substantive, keep
  4. 16488f44 "Reset tutorials" — reverts part of (1)
  5. c73daf12 "Reset iterate_* APIs" — reverts more of (1)

Suggested rewrite: squash 1+2+4+5 into a single commit with an updated message reflecting what actually lands (public Consensus + is_fixedpoint + .gitignore), keep (3) as a separate commit for the benchmarks. Two clean commits, linear history, each message accurate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

DBSP core Related to the core DBSP library

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants