feat(stream): add pull-based pipeline runtime [STREAM-1668] - #559
feat(stream): add pull-based pipeline runtime [STREAM-1668]#559tryangul wants to merge 15 commits into
Conversation
…nize for clarity.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 7db0dbb. Configure here.
| yield StageResult::Exit(PipelineExit::Shutdown); | ||
| return; | ||
| } | ||
| _ = revoke.notified() => { |
There was a problem hiding this comment.
Bug: A shared Notify instance for rebalances can cause a race condition. If rebalances occur rapidly, a new stream may immediately exit, leading to a processing loop without consuming messages.
Severity: MEDIUM
Suggested Fix
To prevent the stale notification permit from being carried over, create a new Notify instance for each new stream created by the source.stream() method. This ensures that each stream instance has its own isolated rebalance signal, avoiding the race condition.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: rust-arroyo/src/processing/stream/source.rs#L159
Potential issue: The `KafkaSource` uses a single shared `Arc<Notify>` instance
(`revoke`) to signal rebalances across stream restarts. A race condition exists where a
rebalance can occur after the old stream has terminated but before the new stream has
registered its `revoke.notified()` future. If rebalances happen in rapid succession, a
permit from a second rebalance can be stored and then immediately consumed by the new
stream. This causes the stream to exit with `PipelineExit::Rebalance` without processing
any messages, potentially leading to a tight loop that stalls message consumption.
Also affects:
rust-arroyo/src/processing/stream/source.rs:101~101rust-arroyo/src/processing/stream/source.rs:128~128
There was a problem hiding this comment.
I think this winds up being a noop because the window is small and we rebuild everything anyway.

What
New
processing/stream/module — a pull-based pipeline runtime using async streams. Fully additive, no existing code changed.How
(Sorry for the wall of text)
Core abstractions:
Stagetrait — singleprocess()method to encapsulate a 'step' in the chain. ReturnsStageResultwhich encapsulates the outcome of processing a message. Open so consumers can define ad-hoc stages, but we could easily provide "canned" stages for the common usecases.PipelineExtcombinators — compose the stages together top to bottom via.apply(),.apply_concurrent(),.on_next(),.on_reject(),.commit().PipelineRunner— core harness that handles rebalance / restart loop.NextHandler(side-effects on success),RejectionHandler(DLQ/logging). Canned:KafkaProducerHandler,DlqHandler(preserves original headers),LogHandler.Why
Addresses the pain points in STREAM-1668 via a new runtime rather than rearchitecting the existing processor:
MessageRejectedspin loopRejectvariant flows through the pipeline to handlers.apply_concurrent(stage, N)for I/O-bound stagesPipelineRunnerhandles the restart loop, caller just defines the pipeline shapeNotes
examples/transform_and_produce_pull.rsStageResult::Fail(recoverable errors skipped)Next steps
PullBasedAdapterthat maps the Python DSL to pull-based stages (tested e2e with real protobuf + parquet)STREAM-1668