1use crate::core::{EventTimeExtractor, KeyExtractor, ProcessorEvent};
8use crate::error::{ProcessorError, Result};
9use crate::pipeline::StreamExecutor;
10use rdkafka::config::ClientConfig;
11use rdkafka::consumer::{Consumer, StreamConsumer};
12use rdkafka::producer::{FutureProducer, FutureRecord, Producer};
13use rdkafka::message::{BorrowedMessage, OwnedHeaders};
14use rdkafka::{Message, Offset, TopicPartitionList};
15use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17use std::sync::atomic::{AtomicBool, Ordering};
18use std::sync::Arc;
19use std::time::Duration;
20use tokio::sync::{mpsc, RwLock};
21use tracing::{debug, error, info, trace, warn};
22
23#[derive(Debug, Clone, Serialize, Deserialize, Default)]
25pub struct KafkaStreamStats {
26 pub messages_consumed: u64,
28
29 pub messages_produced: u64,
31
32 pub consumer_lag: i64,
34
35 pub consume_errors: u64,
37
38 pub produce_errors: u64,
40
41 pub offsets_committed: u64,
43
44 pub commit_errors: u64,
46
47 pub committed_offsets: HashMap<i32, i64>,
49}
50
51impl KafkaStreamStats {
52 pub fn new() -> Self {
54 Self::default()
55 }
56
57 pub fn inc_consumed(&mut self) {
59 self.messages_consumed += 1;
60 }
61
62 pub fn inc_produced(&mut self) {
64 self.messages_produced += 1;
65 }
66
67 pub fn inc_consume_errors(&mut self) {
69 self.consume_errors += 1;
70 }
71
72 pub fn inc_produce_errors(&mut self) {
74 self.produce_errors += 1;
75 }
76
77 pub fn inc_offsets_committed(&mut self) {
79 self.offsets_committed += 1;
80 }
81
82 pub fn inc_commit_errors(&mut self) {
84 self.commit_errors += 1;
85 }
86
87 pub fn update_committed_offset(&mut self, partition: i32, offset: i64) {
89 self.committed_offsets.insert(partition, offset);
90 }
91}
92
93pub struct OffsetManager {
95 pending_offsets: Arc<RwLock<HashMap<(String, i32), i64>>>,
97
98 consumer: Arc<StreamConsumer>,
100
101 stats: Arc<RwLock<KafkaStreamStats>>,
103
104 auto_commit_interval: Duration,
106
107 running: Arc<AtomicBool>,
109}
110
111impl OffsetManager {
112 pub fn new(
114 consumer: Arc<StreamConsumer>,
115 stats: Arc<RwLock<KafkaStreamStats>>,
116 auto_commit_interval: Duration,
117 ) -> Self {
118 Self {
119 pending_offsets: Arc::new(RwLock::new(HashMap::new())),
120 consumer,
121 stats,
122 auto_commit_interval,
123 running: Arc::new(AtomicBool::new(false)),
124 }
125 }
126
127 pub async fn track_offset(&self, topic: String, partition: i32, offset: i64) {
129 let mut offsets = self.pending_offsets.write().await;
130 offsets.insert((topic.clone(), partition), offset);
131 trace!(
132 topic = %topic,
133 partition = partition,
134 offset = offset,
135 "Tracked offset for commit"
136 );
137 }
138
139 pub async fn commit_offsets(&self) -> Result<()> {
141 let offsets = self.pending_offsets.read().await;
142
143 if offsets.is_empty() {
144 return Ok(());
145 }
146
147 let mut tpl = TopicPartitionList::new();
149 for ((topic, partition), offset) in offsets.iter() {
150 tpl.add_partition_offset(topic, *partition, Offset::from_raw(*offset + 1))
151 .map_err(|e| ProcessorError::Kafka {
152 source: format!("Failed to add partition to commit list: {}", e).into(),
153 })?;
154 }
155
156 debug!(
157 partitions = offsets.len(),
158 "Committing offsets"
159 );
160
161 self.consumer
163 .commit(&tpl, rdkafka::consumer::CommitMode::Sync)
164 .map_err(|e| ProcessorError::Kafka {
165 source: format!("Failed to commit offsets: {}", e).into(),
166 })?;
167
168 let mut stats = self.stats.write().await;
170 stats.inc_offsets_committed();
171 for ((_, partition), offset) in offsets.iter() {
172 stats.update_committed_offset(*partition, *offset);
173 }
174
175 info!("Offsets committed successfully");
176 Ok(())
177 }
178
179 pub fn start_auto_commit(&self) {
181 self.running.store(true, Ordering::Relaxed);
182
183 let pending_offsets = Arc::clone(&self.pending_offsets);
184 let consumer = Arc::clone(&self.consumer);
185 let stats = Arc::clone(&self.stats);
186 let running = Arc::clone(&self.running);
187 let interval = self.auto_commit_interval;
188
189 tokio::spawn(async move {
190 let mut ticker = tokio::time::interval(interval);
191
192 while running.load(Ordering::Relaxed) {
193 ticker.tick().await;
194
195 let offsets = pending_offsets.read().await;
196 if offsets.is_empty() {
197 continue;
198 }
199
200 let mut tpl = TopicPartitionList::new();
202 for ((topic, partition), offset) in offsets.iter() {
203 if let Err(e) = tpl.add_partition_offset(topic, *partition, Offset::from_raw(*offset + 1)) {
204 error!(error = %e, "Failed to add partition to commit list");
205 stats.write().await.inc_commit_errors();
206 continue;
207 }
208 }
209 drop(offsets);
210
211 if let Err(e) = consumer.commit(&tpl, rdkafka::consumer::CommitMode::Async) {
213 error!(error = %e, "Failed to commit offsets");
214 stats.write().await.inc_commit_errors();
215 } else {
216 debug!("Auto-committed offsets");
217 stats.write().await.inc_offsets_committed();
218 }
219 }
220
221 debug!("Auto-commit task stopped");
222 });
223 }
224
225 pub fn stop(&self) {
227 self.running.store(false, Ordering::Relaxed);
228 }
229}
230
231pub struct KafkaStreamSource<T>
240where
241 T: Clone + Send + Sync + 'static,
242{
243 consumer: Arc<StreamConsumer>,
245
246 topics: Vec<String>,
248
249 executor: Arc<StreamExecutor<T>>,
251
252 offset_manager: Arc<OffsetManager>,
254
255 stats: Arc<RwLock<KafkaStreamStats>>,
257
258 running: Arc<AtomicBool>,
260
261 poll_timeout: Duration,
263}
264
265impl<T> KafkaStreamSource<T>
266where
267 T: Clone + Send + Sync + 'static + Serialize + for<'de> Deserialize<'de>,
268{
269 pub fn new(
271 consumer: Arc<StreamConsumer>,
272 topics: Vec<String>,
273 executor: Arc<StreamExecutor<T>>,
274 offset_manager: Arc<OffsetManager>,
275 stats: Arc<RwLock<KafkaStreamStats>>,
276 poll_timeout: Duration,
277 ) -> Self {
278 Self {
279 consumer,
280 topics,
281 executor,
282 offset_manager,
283 stats,
284 running: Arc::new(AtomicBool::new(false)),
285 poll_timeout,
286 }
287 }
288
289 pub async fn run(&self) -> Result<()>
291 where
292 T: EventTimeExtractor<T> + KeyExtractor<T>,
293 {
294 info!(
295 topics = ?self.topics,
296 "Starting Kafka stream source"
297 );
298
299 let topic_refs: Vec<&str> = self.topics.iter().map(|s| s.as_str()).collect();
301 self.consumer
302 .subscribe(&topic_refs)
303 .map_err(|e| ProcessorError::Kafka {
304 source: format!("Failed to subscribe to topics: {}", e).into(),
305 })?;
306
307 self.running.store(true, Ordering::Relaxed);
308
309 while self.running.load(Ordering::Relaxed) {
311 match self.consumer.recv().await {
312 Ok(message) => {
313 if let Err(e) = self.process_message(&message).await {
314 error!(error = %e, "Failed to process message");
315 self.stats.write().await.inc_consume_errors();
316 }
317 }
318 Err(e) => {
319 error!(error = %e, "Error receiving message from Kafka");
320 self.stats.write().await.inc_consume_errors();
321 }
322 }
323 }
324
325 info!("Kafka stream source stopped");
326 Ok(())
327 }
328
329 async fn process_message(&self, message: &BorrowedMessage<'_>) -> Result<()>
331 where
332 T: EventTimeExtractor<T> + KeyExtractor<T>,
333 {
334 let payload = message.payload().ok_or_else(|| ProcessorError::Kafka {
336 source: "Message has no payload".into(),
337 })?;
338
339 let event: T = serde_json::from_slice(payload).map_err(|e| ProcessorError::Kafka {
340 source: format!("Failed to deserialize message: {}", e).into(),
341 })?;
342
343 trace!(
344 topic = message.topic(),
345 partition = message.partition(),
346 offset = message.offset(),
347 "Received message"
348 );
349
350 self.executor.ingest(event).await?;
352
353 self.offset_manager
355 .track_offset(
356 message.topic().to_string(),
357 message.partition(),
358 message.offset(),
359 )
360 .await;
361
362 self.stats.write().await.inc_consumed();
364
365 Ok(())
366 }
367
368 pub fn stop(&self) {
370 info!("Stopping Kafka stream source");
371 self.running.store(false, Ordering::Relaxed);
372 }
373
374 pub fn is_running(&self) -> bool {
376 self.running.load(Ordering::Relaxed)
377 }
378
379 pub async fn stats(&self) -> KafkaStreamStats {
381 self.stats.read().await.clone()
382 }
383}
384
385pub struct KafkaStreamSink<R>
394where
395 R: Clone + Send + Sync + 'static,
396{
397 producer: Arc<FutureProducer>,
399
400 output_topic: String,
402
403 result_rx: Arc<RwLock<mpsc::Receiver<ProcessorEvent<R>>>>,
405
406 stats: Arc<RwLock<KafkaStreamStats>>,
408
409 running: Arc<AtomicBool>,
411
412 delivery_timeout: Duration,
414
415 max_retries: u32,
417}
418
419impl<R> KafkaStreamSink<R>
420where
421 R: Clone + Send + Sync + 'static + Serialize + for<'de> Deserialize<'de>,
422{
423 pub fn new(
425 producer: Arc<FutureProducer>,
426 output_topic: String,
427 result_rx: mpsc::Receiver<ProcessorEvent<R>>,
428 stats: Arc<RwLock<KafkaStreamStats>>,
429 delivery_timeout: Duration,
430 max_retries: u32,
431 ) -> Self {
432 Self {
433 producer,
434 output_topic,
435 result_rx: Arc::new(RwLock::new(result_rx)),
436 stats,
437 running: Arc::new(AtomicBool::new(false)),
438 delivery_timeout,
439 max_retries,
440 }
441 }
442
443 pub async fn run(&self) -> Result<()> {
445 info!(
446 topic = %self.output_topic,
447 "Starting Kafka stream sink"
448 );
449
450 self.running.store(true, Ordering::Relaxed);
451
452 let mut rx = self.result_rx.write().await;
453
454 while self.running.load(Ordering::Relaxed) {
455 match tokio::time::timeout(Duration::from_millis(100), rx.recv()).await {
456 Ok(Some(result)) => {
457 if let Err(e) = self.produce_result(result).await {
458 error!(error = %e, "Failed to produce result to Kafka");
459 self.stats.write().await.inc_produce_errors();
460 }
461 }
462 Ok(None) => {
463 debug!("Result channel closed");
464 break;
465 }
466 Err(_) => {
467 continue;
469 }
470 }
471 }
472
473 info!("Kafka stream sink stopped");
474 Ok(())
475 }
476
477 async fn produce_result(&self, result: ProcessorEvent<R>) -> Result<()> {
479 let payload = serde_json::to_vec(&result).map_err(|e| ProcessorError::Kafka {
481 source: format!("Failed to serialize result: {}", e).into(),
482 })?;
483
484 let key = result.key.as_bytes();
485
486 let headers = OwnedHeaders::new()
488 .insert(rdkafka::message::Header {
489 key: "event_time",
490 value: Some(result.event_time.to_rfc3339().as_bytes()),
491 })
492 .insert(rdkafka::message::Header {
493 key: "processing_time",
494 value: Some(result.processing_time.to_rfc3339().as_bytes()),
495 });
496
497 let mut retries = 0;
499 loop {
500 let record = FutureRecord::to(&self.output_topic)
501 .payload(&payload)
502 .key(key)
503 .headers(headers.clone());
504
505 match self.producer.send(record, self.delivery_timeout).await {
506 Ok((partition, offset)) => {
507 trace!(
508 topic = %self.output_topic,
509 partition = partition,
510 offset = offset,
511 "Produced result to Kafka"
512 );
513
514 self.stats.write().await.inc_produced();
515 return Ok(());
516 }
517 Err((e, _)) => {
518 retries += 1;
519 if retries >= self.max_retries {
520 return Err(ProcessorError::Kafka {
521 source: format!(
522 "Failed to produce after {} retries: {}",
523 retries, e
524 )
525 .into(),
526 });
527 }
528
529 warn!(
530 error = %e,
531 retry = retries,
532 max_retries = self.max_retries,
533 "Failed to produce, retrying"
534 );
535
536 let backoff = Duration::from_millis(100 * 2u64.pow(retries - 1));
538 tokio::time::sleep(backoff).await;
539 }
540 }
541 }
542 }
543
544 pub fn stop(&self) {
546 info!("Stopping Kafka stream sink");
547 self.running.store(false, Ordering::Relaxed);
548 }
549
550 pub fn is_running(&self) -> bool {
552 self.running.load(Ordering::Relaxed)
553 }
554
555 pub async fn stats(&self) -> KafkaStreamStats {
557 self.stats.read().await.clone()
558 }
559}
560
561#[derive(Debug, Clone)]
563pub struct KafkaStreamConfig {
564 pub brokers: String,
566
567 pub group_id: String,
569
570 pub input_topics: Vec<String>,
572
573 pub output_topic: String,
575
576 pub auto_commit_interval_ms: u64,
578
579 pub poll_timeout_ms: u64,
581
582 pub delivery_timeout_ms: u64,
584
585 pub max_produce_retries: u32,
587
588 pub consumer_config: HashMap<String, String>,
590
591 pub producer_config: HashMap<String, String>,
593}
594
595impl Default for KafkaStreamConfig {
596 fn default() -> Self {
597 Self {
598 brokers: "localhost:9092".to_string(),
599 group_id: "stream-processor".to_string(),
600 input_topics: vec!["input".to_string()],
601 output_topic: "output".to_string(),
602 auto_commit_interval_ms: 5000,
603 poll_timeout_ms: 100,
604 delivery_timeout_ms: 5000,
605 max_produce_retries: 3,
606 consumer_config: HashMap::new(),
607 producer_config: HashMap::new(),
608 }
609 }
610}
611
612pub struct KafkaStreamPipeline<T, R>
621where
622 T: Clone + Send + Sync + 'static,
623 R: Clone + Send + Sync + 'static,
624{
625 config: KafkaStreamConfig,
627
628 pub executor: Arc<StreamExecutor<T>>,
630
631 consumer: Arc<StreamConsumer>,
633
634 producer: Arc<FutureProducer>,
636
637 offset_manager: Arc<OffsetManager>,
639
640 source: Option<KafkaStreamSource<T>>,
642
643 sink: Option<KafkaStreamSink<R>>,
645
646 pub stats: Arc<RwLock<KafkaStreamStats>>,
648
649 running: Arc<AtomicBool>,
651}
652
653impl<T, R> KafkaStreamPipeline<T, R>
654where
655 T: Clone + Send + Sync + 'static + Serialize + for<'de> Deserialize<'de>,
656 R: Clone + Send + Sync + 'static + Serialize + for<'de> Deserialize<'de>,
657{
658 pub fn new(
660 config: KafkaStreamConfig,
661 executor: StreamExecutor<T>,
662 ) -> Result<Self> {
663 let mut consumer_config = ClientConfig::new();
665 consumer_config
666 .set("bootstrap.servers", &config.brokers)
667 .set("group.id", &config.group_id)
668 .set("enable.auto.commit", "false")
669 .set("auto.offset.reset", "earliest");
670
671 for (key, value) in &config.consumer_config {
673 consumer_config.set(key, value);
674 }
675
676 let consumer: StreamConsumer = consumer_config.create().map_err(|e| {
677 ProcessorError::Kafka {
678 source: format!("Failed to create consumer: {}", e).into(),
679 }
680 })?;
681
682 let mut producer_config = ClientConfig::new();
684 producer_config
685 .set("bootstrap.servers", &config.brokers)
686 .set("message.timeout.ms", "5000");
687
688 for (key, value) in &config.producer_config {
690 producer_config.set(key, value);
691 }
692
693 let producer: FutureProducer = producer_config.create().map_err(|e| {
694 ProcessorError::Kafka {
695 source: format!("Failed to create producer: {}", e).into(),
696 }
697 })?;
698
699 let consumer = Arc::new(consumer);
700 let producer = Arc::new(producer);
701 let executor = Arc::new(executor);
702 let stats = Arc::new(RwLock::new(KafkaStreamStats::new()));
703
704 let offset_manager = Arc::new(OffsetManager::new(
706 Arc::clone(&consumer),
707 Arc::clone(&stats),
708 Duration::from_millis(config.auto_commit_interval_ms),
709 ));
710
711 Ok(Self {
712 config,
713 executor,
714 consumer,
715 producer,
716 offset_manager,
717 source: None,
718 sink: None,
719 stats,
720 running: Arc::new(AtomicBool::new(false)),
721 })
722 }
723
724 pub async fn run(&mut self, result_rx: mpsc::Receiver<ProcessorEvent<R>>) -> Result<()>
726 where
727 T: EventTimeExtractor<T> + KeyExtractor<T>,
728 {
729 info!("Starting Kafka stream pipeline");
730
731 self.running.store(true, Ordering::Relaxed);
732
733 let source = KafkaStreamSource::new(
735 Arc::clone(&self.consumer),
736 self.config.input_topics.clone(),
737 Arc::clone(&self.executor),
738 Arc::clone(&self.offset_manager),
739 Arc::clone(&self.stats),
740 Duration::from_millis(self.config.poll_timeout_ms),
741 );
742
743 let sink = KafkaStreamSink::new(
745 Arc::clone(&self.producer),
746 self.config.output_topic.clone(),
747 result_rx,
748 Arc::clone(&self.stats),
749 Duration::from_millis(self.config.delivery_timeout_ms),
750 self.config.max_produce_retries,
751 );
752
753 self.offset_manager.start_auto_commit();
755
756 let executor_handle = {
758 let executor = Arc::clone(&self.executor);
759 tokio::spawn(async move {
760 if let Err(e) = executor.run().await {
761 error!(error = %e, "Executor error");
762 }
763 })
764 };
765
766 let source_handle = {
768 let source_clone = source.clone();
769 tokio::spawn(async move {
770 if let Err(e) = source_clone.run().await {
771 error!(error = %e, "Source error");
772 }
773 })
774 };
775
776 let sink_clone = KafkaStreamSink {
778 producer: Arc::clone(&sink.producer),
779 output_topic: sink.output_topic.clone(),
780 result_rx: Arc::clone(&sink.result_rx),
781 stats: Arc::clone(&sink.stats),
782 running: Arc::clone(&sink.running),
783 delivery_timeout: sink.delivery_timeout,
784 max_retries: sink.max_retries,
785 };
786
787 let sink_handle = tokio::spawn(async move {
789 if let Err(e) = sink.run().await {
790 error!(error = %e, "Sink error");
791 }
792 });
793
794 self.source = Some(source);
795 self.sink = Some(sink_clone);
796
797 let _ = tokio::try_join!(executor_handle, source_handle, sink_handle);
799
800 info!("Kafka stream pipeline stopped");
801 Ok(())
802 }
803
804 pub async fn shutdown(&mut self) -> Result<()> {
806 info!("Shutting down Kafka stream pipeline");
807
808 self.running.store(false, Ordering::Relaxed);
809
810 if let Some(source) = &self.source {
812 source.stop();
813 }
814
815 if let Some(sink) = &self.sink {
816 sink.stop();
817 }
818
819 self.executor.stop();
820
821 info!("Flushing producer");
823 self.producer.flush(Duration::from_secs(30)).map_err(|e| {
824 ProcessorError::Kafka {
825 source: format!("Failed to flush producer: {:?}", e).into(),
826 }
827 })?;
828
829 info!("Committing final offsets");
831 self.offset_manager.commit_offsets().await?;
832
833 self.offset_manager.stop();
835
836 info!("Shutdown complete");
837 Ok(())
838 }
839
840 pub async fn stats(&self) -> KafkaStreamStats {
842 self.stats.read().await.clone()
843 }
844
845 pub fn is_running(&self) -> bool {
847 self.running.load(Ordering::Relaxed)
848 }
849
850 pub async fn health_check(&self) -> bool {
852 if !self.executor.is_running() {
854 warn!("Executor is not running");
855 return false;
856 }
857
858 if let Some(source) = &self.source {
860 if !source.is_running() {
861 warn!("Source is not running");
862 return false;
863 }
864 }
865
866 if let Some(sink) = &self.sink {
868 if !sink.is_running() {
869 warn!("Sink is not running");
870 return false;
871 }
872 }
873
874 true
875 }
876}
877
878impl<T> Clone for KafkaStreamSource<T>
880where
881 T: Clone + Send + Sync + 'static,
882{
883 fn clone(&self) -> Self {
884 Self {
885 consumer: Arc::clone(&self.consumer),
886 topics: self.topics.clone(),
887 executor: Arc::clone(&self.executor),
888 offset_manager: Arc::clone(&self.offset_manager),
889 stats: Arc::clone(&self.stats),
890 running: Arc::clone(&self.running),
891 poll_timeout: self.poll_timeout,
892 }
893 }
894}
895
896#[cfg(test)]
897mod tests {
898 use super::*;
899
900 #[test]
901 fn test_kafka_stream_stats() {
902 let mut stats = KafkaStreamStats::new();
903
904 stats.inc_consumed();
905 assert_eq!(stats.messages_consumed, 1);
906
907 stats.inc_produced();
908 assert_eq!(stats.messages_produced, 1);
909
910 stats.inc_consume_errors();
911 assert_eq!(stats.consume_errors, 1);
912
913 stats.inc_produce_errors();
914 assert_eq!(stats.produce_errors, 1);
915
916 stats.inc_offsets_committed();
917 assert_eq!(stats.offsets_committed, 1);
918
919 stats.inc_commit_errors();
920 assert_eq!(stats.commit_errors, 1);
921
922 stats.update_committed_offset(0, 100);
923 assert_eq!(stats.committed_offsets.get(&0), Some(&100));
924 }
925
926 #[test]
927 fn test_kafka_stream_config_default() {
928 let config = KafkaStreamConfig::default();
929
930 assert_eq!(config.brokers, "localhost:9092");
931 assert_eq!(config.group_id, "stream-processor");
932 assert_eq!(config.input_topics, vec!["input"]);
933 assert_eq!(config.output_topic, "output");
934 assert_eq!(config.auto_commit_interval_ms, 5000);
935 assert_eq!(config.max_produce_retries, 3);
936 }
937}