Repository navigation
feat(runtime): Implement pull based arroyo #560
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
41 commits
Select commit
Hold shift + click to select a range
319e885
Full stage based implementation of pull streaming
tryangul da3d724
Handle N:1 and N:0 use cases. Refactor StageResult, handlers and orga…
tryangul da211d7
Support concurrent stage application.
tryangul 448488a
Actually commit offsets.
tryangul ada8559
Add graceful termination aka cancellation support.
tryangul 7acfc20
Comment tweaks.
tryangul a855d06
handle fatal kafka errors, preserve dlq headers, init coarsetime upda…
tryangul e65d4ac
Basic rebalance handling.
tryangul 5a36b93
Factor out PipelineRunner. Update examples.
tryangul 543b7df
Formatting and comment cleanup.
tryangul 6a6d6b9
Fix stage name lifetimes.
tryangul 44387cd
Use singleton updater to prevent leak.
tryangul cf646b1
Synchronize drain on rebalance.
tryangul 3f89896
Drain regardless of outcome
tryangul 2e3d449
Revert drain on rebalance due to potential threading deadlock.
tryangul ad8cbcc
Add BatchStage
tryangul 840632a
Break out stream collector.
tryangul bace648
Add flush timer
tryangul 36533f3
Buffer now has an Out type
tryangul 4f3c5a7
Formatting
tryangul bff165d
Add Pipeline trait
tryangul 15e873a
Fmt.
tryangul e7af45f
Doc test import
tryangul c2c4b09
Cleanup types with aliases. Convert kafka stats logging to metrics.
tryangul f79f05b
Cargo fmt.
tryangul 4b03c33
ref(pull): remove unnecessary Arc around raw payload
tryangul 4d1c367
ref(pull): use coarsetime for stage durations.
tryangul 43fd7ed
ref(pull): use parking_lot mutex for batch stage to avoid mutex syscall.
tryangul 13a872d
ref(pull): boxed dyn producer for dlq handler.
tryangul 498b419
ref(pull): owned streams.
tryangul 47e5e8c
ref(pull): Map_emit.
tryangul 47e1a08
ref(pull): Fix-up pipeline runner.
tryangul b274934
ref(pull): Add batch time metric with flush reason for parity with push.
tryangul 1bf5057
ref(pull): Fmt.
tryangul ee52fc4
ref(pull): gate pull/stream API behind `pull` feature flag
tryangul eb619a8
Remove commit() in favor of run(); drop/reject skip offsets. Drop ext…
tryangul 1d6c5c4
fmt and add test entry for ci.
tryangul 5b15448
Update variant comments for clarity.
tryangul 7ee18ac
StreamCollector now implements a vistor for every variant. Try_commit…
tryangul 527c27e
Pass map of offsets through to accommodate batches containing mulitpl…
tryangul 61e4b6c
Merge branch 'main' into rbroughan/pull-based-arroyo-part-2
tryangul File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,8 +15,10 @@ path = "rust-arroyo/src/lib.rs" | |
| [features] | ||
| ssl = ["rdkafka/ssl"] | ||
| gssapi = ["rdkafka/gssapi"] | ||
| pull = [] | ||
|
|
||
| [dependencies] | ||
| async-stream = "0.3" | ||
| chrono = "0.4.26" | ||
| coarsetime = "0.1.33" | ||
| metrics = "0.24" | ||
|
|
@@ -28,12 +30,15 @@ serde = { version = "1.0.137", features = ["derive"] } | |
| serde_json = "1.0.81" | ||
| thiserror = "1.0" | ||
| tokio = { version = "1.19.2", features = ["full"] } | ||
| futures = "0.3" | ||
| tokio-stream = { version = "0.1", features = ["time"] } | ||
| tokio-util = "0.7" | ||
|
Comment on lines
+33
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please sync with @lvthanh03 on the dependencies you are adding as he is trying to embed arroyo into several applications and he will have to keep the dependencies under control. |
||
| tracing = "0.1.40" | ||
| uuid = { version = "1.5.0", features = ["v4"] } | ||
| parking_lot = "0.12.1" | ||
|
|
||
| [dev-dependencies] | ||
| tracing-subscriber = "0.3.18" | ||
| tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } | ||
|
|
||
| [[test]] | ||
| name = "metrics" | ||
|
|
@@ -54,3 +59,8 @@ path = "rust-arroyo/examples/base_consumer.rs" | |
| [[example]] | ||
| name = "reloading_producer" | ||
| path = "rust-arroyo/examples/reloading_producer.rs" | ||
|
|
||
| [[example]] | ||
| name = "transform_and_produce_pull" | ||
| path = "rust-arroyo/examples/transform_and_produce_pull.rs" | ||
| required-features = ["pull"] | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| /// Pull-based version of transform_and_produce. | ||
| /// | ||
| /// Pipeline: | ||
| /// KafkaSource → apply(reverse) → on_next(produce) → on_reject(log) → run | ||
| /// | ||
| /// `PipelineRunner::run()` handles the rebalance restart loop — | ||
| /// the build closure is called once per partition assignment with fresh | ||
| /// stages and handlers. | ||
| extern crate sentry_arroyo; | ||
|
|
||
| use std::time::Duration; | ||
|
|
||
| use futures::Stream; | ||
| use sentry_arroyo::backends::kafka::config::KafkaConfig; | ||
| use sentry_arroyo::backends::kafka::producer::KafkaProducer; | ||
| use sentry_arroyo::backends::kafka::types::KafkaPayload; | ||
| use sentry_arroyo::backends::kafka::InitialOffset; | ||
| use sentry_arroyo::processing::stream::{ | ||
| KafkaProducerHandler, KafkaSource, LogHandler, Pipeline, PipelineEnvelope, PipelineExt, | ||
| PipelineRunner, Stage, StageResult, | ||
| }; | ||
| use sentry_arroyo::types::{Topic, TopicOrPartition}; | ||
|
|
||
| /// A Stage that reverses the string payload. | ||
| struct ReverseStage; | ||
|
|
||
| impl Stage for ReverseStage { | ||
| type In = KafkaPayload; | ||
| type Out = KafkaPayload; | ||
|
|
||
| async fn process(&self, envelope: PipelineEnvelope<KafkaPayload>) -> StageResult<KafkaPayload> { | ||
| let reversed = envelope.map_payload(|p| { | ||
| let bytes = p.payload().unwrap(); | ||
| let s = std::str::from_utf8(bytes).unwrap(); | ||
| let reversed: String = s.chars().rev().collect(); | ||
| println!("transforming: {:?} -> {:?}", s, reversed); | ||
| KafkaPayload::new( | ||
| p.key().cloned(), | ||
| p.headers().cloned(), | ||
| Some(reversed.into_bytes()), | ||
| ) | ||
| }); | ||
|
|
||
| StageResult::Emit(reversed) | ||
| } | ||
|
|
||
| fn name(&self) -> &'static str { | ||
| "reverse_string" | ||
| } | ||
| } | ||
|
|
||
| struct TransformAndProducePipeline { | ||
| reverse: ReverseStage, | ||
| produce_handler: KafkaProducerHandler, | ||
| error_handler: LogHandler, | ||
| } | ||
|
|
||
| impl Pipeline for TransformAndProducePipeline { | ||
| type Output = KafkaPayload; | ||
|
|
||
| fn stream( | ||
| self, | ||
| source: impl Stream<Item = StageResult<KafkaPayload>> + Send, | ||
| ) -> impl Stream<Item = StageResult<KafkaPayload>> + Send { | ||
| source | ||
| .apply(self.reverse) | ||
| .on_next(self.produce_handler) | ||
| .on_reject(self.error_handler) | ||
| } | ||
| } | ||
|
|
||
| #[tokio::main] | ||
| async fn main() { | ||
| tracing_subscriber::fmt::init(); | ||
|
|
||
| let consumer_config = KafkaConfig::new_consumer_config( | ||
| vec!["0.0.0.0:9092".to_string()], | ||
| "my_group".to_string(), | ||
| InitialOffset::Latest, | ||
| false, | ||
| 30_000, | ||
| None, | ||
| ); | ||
| let source = KafkaSource::new(consumer_config, &[Topic::new("test_in")]); | ||
|
|
||
| let producer_config = KafkaConfig::new_producer_config(vec!["0.0.0.0:9092".to_string()], None); | ||
|
|
||
| let result = PipelineRunner::run(&source, Duration::from_secs(1), || { | ||
| let producer = KafkaProducer::new(producer_config.clone()).unwrap(); | ||
| TransformAndProducePipeline { | ||
| reverse: ReverseStage, | ||
| produce_handler: KafkaProducerHandler::new( | ||
| producer, | ||
| TopicOrPartition::Topic(Topic::new("test_out")), | ||
| ), | ||
| error_handler: LogHandler, | ||
| } | ||
| }) | ||
| .await; | ||
|
|
||
| if let Err(e) = result { | ||
| tracing::error!("Pipeline stopped: {}", e); | ||
| } | ||
| } |
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| /// Accumulates items for batching. Implementations define how items | ||
| /// are stored, how their byte size is measured, and what the flushed | ||
| /// output type is. | ||
| /// | ||
| /// The batch stage calls `push()` for each item, uses the returned | ||
| /// byte count to track size-based thresholds, and calls `flush()` | ||
| /// when a threshold is reached. | ||
| pub trait Buffer<T>: Send + Sync { | ||
| /// The type returned by `flush()`. For collecting buffers this is `Vec<T>`. | ||
| /// For merging buffers this can be a single aggregated value. | ||
| type Output: Send + Sync; | ||
|
|
||
| /// Add an item to the buffer. Returns the item's size in bytes. | ||
| fn push(&mut self, item: T) -> u64; | ||
|
|
||
| /// Number of items in the buffer. | ||
| fn len(&self) -> u64; | ||
|
|
||
| /// Whether the buffer is empty. | ||
| fn is_empty(&self) -> bool { | ||
| self.len() == 0 | ||
| } | ||
|
|
||
| /// Drain the buffer and return the accumulated output. | ||
| fn flush(&mut self) -> Self::Output; | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| use std::time::Duration; | ||
|
|
||
| use tokio::time::Interval; | ||
|
|
||
| use crate::processing::stream::offset_tracker::ensure_time_updater; | ||
|
|
||
| /// A monotonic time source for `FlushTimer`. | ||
| /// | ||
| /// Production uses `CoarseClock` (coarsetime's cached global, ~1ns reads). | ||
| /// Tests can inject a controllable clock for deterministic timing. | ||
| pub trait Clock: Send + Sync { | ||
| fn now(&self) -> coarsetime::Instant; | ||
| } | ||
|
|
||
| /// Production clock — reads from coarsetime's cached global. | ||
| pub struct CoarseClock; | ||
|
|
||
| impl Clock for CoarseClock { | ||
| fn now(&self) -> coarsetime::Instant { | ||
| coarsetime::Instant::recent() | ||
| } | ||
| } | ||
|
|
||
| /// Manages time-based flush triggers for `apply_with_timer`. | ||
| /// | ||
| /// Uses coarsetime watermarks checked on each interval tick. | ||
| /// No per-item timer wheel operations. | ||
| /// | ||
| /// Two triggers: | ||
| /// - **Idle**: flush if no upstream item arrives within `idle_timeout`. | ||
| /// Watermark `last_activity` resets on every item. | ||
| /// - **Cadence**: flush at `max_cadence` after the first item accumulated. | ||
| /// Watermark `batch_start` set once per batch. | ||
| pub struct FlushTimer<C: Clock> { | ||
| clock: C, | ||
| /// Interval tick — poll this in `select!`. | ||
| pub interval: Interval, | ||
|
|
||
| idle_dur: coarsetime::Duration, | ||
| cadence_dur: coarsetime::Duration, | ||
|
|
||
| batch_start: coarsetime::Instant, | ||
| last_activity: coarsetime::Instant, | ||
| } | ||
|
|
||
| /// Far-future offset used as a sentinel — makes `is_active()` return false | ||
| /// and `should_flush()` return false without branches. | ||
| fn far_future() -> coarsetime::Duration { | ||
| coarsetime::Duration::from_secs(365 * 24 * 3600) | ||
| } | ||
|
|
||
| impl FlushTimer<CoarseClock> { | ||
| /// Create a production `FlushTimer` with coarsetime and a tokio interval. | ||
| pub fn new(idle_timeout: Option<Duration>, max_cadence: Option<Duration>) -> Self { | ||
| ensure_time_updater(); | ||
|
|
||
| let min_dur = [idle_timeout, max_cadence] | ||
| .into_iter() | ||
| .flatten() | ||
| .min() | ||
| .unwrap_or(Duration::from_secs(1)); | ||
| let tick = (min_dur / 10) | ||
| .max(Duration::from_millis(1)) | ||
| .min(Duration::from_millis(100)); | ||
|
|
||
| Self::with_clock( | ||
| CoarseClock, | ||
| tokio::time::interval(tick), | ||
| idle_timeout, | ||
| max_cadence, | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| impl<C: Clock> FlushTimer<C> { | ||
| /// Create a `FlushTimer` with an injected clock and interval. | ||
| pub fn with_clock( | ||
| clock: C, | ||
| interval: Interval, | ||
| idle_timeout: Option<Duration>, | ||
| max_cadence: Option<Duration>, | ||
| ) -> Self { | ||
| let far = clock.now() + far_future(); | ||
|
|
||
| Self { | ||
| clock, | ||
| interval, | ||
| idle_dur: idle_timeout.map_or(far_future(), |d| d.into()), | ||
| cadence_dur: max_cadence.map_or(far_future(), |d| d.into()), | ||
| batch_start: far, | ||
| last_activity: far, | ||
| } | ||
| } | ||
|
|
||
| /// Whether a batch is currently accumulating. | ||
| pub fn is_active(&self) -> bool { | ||
| self.batch_start <= self.clock.now() | ||
| } | ||
|
|
||
| /// An item was accumulated. Updates watermarks (~1ns). | ||
| pub fn on_accumulate(&mut self) { | ||
| let now = self.clock.now(); | ||
| self.last_activity = now; | ||
| if self.batch_start > now { | ||
| self.batch_start = now; | ||
| } | ||
| } | ||
|
|
||
| /// The batch was flushed. Unsets watermarks. | ||
| pub fn on_flush(&mut self) { | ||
| self.unset(); | ||
| } | ||
|
|
||
| /// Duration since the batch started accumulating. | ||
| /// Returns zero if no batch is active. | ||
| pub fn batch_time(&self) -> Duration { | ||
| let now = self.clock.now(); | ||
| if self.batch_start > now { | ||
| Duration::ZERO | ||
| } else { | ||
| now.duration_since(self.batch_start).into() | ||
| } | ||
| } | ||
|
|
||
| /// Check if a flush trigger has fired based on watermarks. | ||
| /// Called on each interval tick. | ||
| pub fn should_flush(&self) -> bool { | ||
| let now = self.clock.now(); | ||
| now.duration_since(self.batch_start) >= self.cadence_dur | ||
| || now.duration_since(self.last_activity) >= self.idle_dur | ||
| } | ||
|
|
||
| /// Reset watermarks to far-future sentinels. | ||
| fn unset(&mut self) { | ||
| let far = self.clock.now() + far_future(); | ||
| self.batch_start = far; | ||
| self.last_activity = far; | ||
| } | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| pub mod buffer; | ||
| pub mod flush_timer; | ||
| pub mod stage; | ||
| pub mod triggers; |
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seems like CI for the new pull feature is not ran, could we run
cargo test --features pullin the Rust test matrix?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. Let me see what I can do