Skip to main content

llm_optimizer_processor/kafka/
integration.rs

1//! Kafka integration for stream processing pipeline
2//!
3//! This module provides integration between Kafka and the stream processing pipeline,
4//! enabling Kafka-to-Kafka streaming with automatic offset management, watermarking,
5//! and error recovery.
6
7use 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/// Statistics for Kafka stream processing
24#[derive(Debug, Clone, Serialize, Deserialize, Default)]
25pub struct KafkaStreamStats {
26    /// Messages consumed from Kafka
27    pub messages_consumed: u64,
28
29    /// Messages produced to Kafka
30    pub messages_produced: u64,
31
32    /// Consumer lag (estimated)
33    pub consumer_lag: i64,
34
35    /// Number of consume errors
36    pub consume_errors: u64,
37
38    /// Number of produce errors
39    pub produce_errors: u64,
40
41    /// Number of offsets committed
42    pub offsets_committed: u64,
43
44    /// Number of commit errors
45    pub commit_errors: u64,
46
47    /// Last committed offset per partition
48    pub committed_offsets: HashMap<i32, i64>,
49}
50
51impl KafkaStreamStats {
52    /// Create new empty stats
53    pub fn new() -> Self {
54        Self::default()
55    }
56
57    /// Increment messages consumed
58    pub fn inc_consumed(&mut self) {
59        self.messages_consumed += 1;
60    }
61
62    /// Increment messages produced
63    pub fn inc_produced(&mut self) {
64        self.messages_produced += 1;
65    }
66
67    /// Increment consume errors
68    pub fn inc_consume_errors(&mut self) {
69        self.consume_errors += 1;
70    }
71
72    /// Increment produce errors
73    pub fn inc_produce_errors(&mut self) {
74        self.produce_errors += 1;
75    }
76
77    /// Increment offsets committed
78    pub fn inc_offsets_committed(&mut self) {
79        self.offsets_committed += 1;
80    }
81
82    /// Increment commit errors
83    pub fn inc_commit_errors(&mut self) {
84        self.commit_errors += 1;
85    }
86
87    /// Update committed offset for a partition
88    pub fn update_committed_offset(&mut self, partition: i32, offset: i64) {
89        self.committed_offsets.insert(partition, offset);
90    }
91}
92
93/// Offset manager for tracking and committing Kafka offsets
94pub struct OffsetManager {
95    /// Pending offsets to commit (topic, partition, offset)
96    pending_offsets: Arc<RwLock<HashMap<(String, i32), i64>>>,
97
98    /// Consumer for committing offsets
99    consumer: Arc<StreamConsumer>,
100
101    /// Statistics
102    stats: Arc<RwLock<KafkaStreamStats>>,
103
104    /// Auto-commit interval
105    auto_commit_interval: Duration,
106
107    /// Running flag
108    running: Arc<AtomicBool>,
109}
110
111impl OffsetManager {
112    /// Create a new offset manager
113    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    /// Track an offset for a message that has been processed
128    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    /// Commit all pending offsets
140    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        // Build topic partition list
148        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        // Commit synchronously
162        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        // Update stats
169        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    /// Start auto-commit task
180    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                // Build topic partition list
201                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                // Commit offsets
212                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    /// Stop auto-commit task
226    pub fn stop(&self) {
227        self.running.store(false, Ordering::Relaxed);
228    }
229}
230
231/// Kafka stream source that integrates with StreamExecutor
232///
233/// This component polls messages from Kafka and feeds them into the stream processing pipeline.
234/// It handles:
235/// - Message deserialization
236/// - Event time extraction
237/// - Watermark generation
238/// - Offset tracking
239pub struct KafkaStreamSource<T>
240where
241    T: Clone + Send + Sync + 'static,
242{
243    /// Kafka consumer
244    consumer: Arc<StreamConsumer>,
245
246    /// Topics to consume from
247    topics: Vec<String>,
248
249    /// Stream executor
250    executor: Arc<StreamExecutor<T>>,
251
252    /// Offset manager
253    offset_manager: Arc<OffsetManager>,
254
255    /// Statistics
256    stats: Arc<RwLock<KafkaStreamStats>>,
257
258    /// Running flag
259    running: Arc<AtomicBool>,
260
261    /// Poll timeout
262    poll_timeout: Duration,
263}
264
265impl<T> KafkaStreamSource<T>
266where
267    T: Clone + Send + Sync + 'static + Serialize + for<'de> Deserialize<'de>,
268{
269    /// Create a new Kafka stream source
270    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    /// Start consuming messages and feeding them to the executor
290    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        // Subscribe to topics
300        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        // Start consuming
310        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    /// Process a single Kafka message
330    async fn process_message(&self, message: &BorrowedMessage<'_>) -> Result<()>
331    where
332        T: EventTimeExtractor<T> + KeyExtractor<T>,
333    {
334        // Deserialize message
335        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        // Feed to executor
351        self.executor.ingest(event).await?;
352
353        // Track offset for commit
354        self.offset_manager
355            .track_offset(
356                message.topic().to_string(),
357                message.partition(),
358                message.offset(),
359            )
360            .await;
361
362        // Update stats
363        self.stats.write().await.inc_consumed();
364
365        Ok(())
366    }
367
368    /// Stop the source
369    pub fn stop(&self) {
370        info!("Stopping Kafka stream source");
371        self.running.store(false, Ordering::Relaxed);
372    }
373
374    /// Check if running
375    pub fn is_running(&self) -> bool {
376        self.running.load(Ordering::Relaxed)
377    }
378
379    /// Get statistics
380    pub async fn stats(&self) -> KafkaStreamStats {
381        self.stats.read().await.clone()
382    }
383}
384
385/// Kafka stream sink that receives results from the pipeline
386///
387/// This component receives aggregated results from the stream processing pipeline
388/// and produces them to a Kafka topic. It handles:
389/// - Message serialization
390/// - Delivery guarantees
391/// - Retries on failure
392/// - Error handling
393pub struct KafkaStreamSink<R>
394where
395    R: Clone + Send + Sync + 'static,
396{
397    /// Kafka producer
398    producer: Arc<FutureProducer>,
399
400    /// Output topic
401    output_topic: String,
402
403    /// Result receiver channel
404    result_rx: Arc<RwLock<mpsc::Receiver<ProcessorEvent<R>>>>,
405
406    /// Statistics
407    stats: Arc<RwLock<KafkaStreamStats>>,
408
409    /// Running flag
410    running: Arc<AtomicBool>,
411
412    /// Delivery timeout
413    delivery_timeout: Duration,
414
415    /// Max retries
416    max_retries: u32,
417}
418
419impl<R> KafkaStreamSink<R>
420where
421    R: Clone + Send + Sync + 'static + Serialize + for<'de> Deserialize<'de>,
422{
423    /// Create a new Kafka stream sink
424    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    /// Start consuming results and producing to Kafka
444    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                    // Timeout - continue
468                    continue;
469                }
470            }
471        }
472
473        info!("Kafka stream sink stopped");
474        Ok(())
475    }
476
477    /// Produce a result to Kafka with retries
478    async fn produce_result(&self, result: ProcessorEvent<R>) -> Result<()> {
479        // Serialize result
480        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        // Build headers
487        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        // Retry logic
498        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                    // Exponential backoff
537                    let backoff = Duration::from_millis(100 * 2u64.pow(retries - 1));
538                    tokio::time::sleep(backoff).await;
539                }
540            }
541        }
542    }
543
544    /// Stop the sink
545    pub fn stop(&self) {
546        info!("Stopping Kafka stream sink");
547        self.running.store(false, Ordering::Relaxed);
548    }
549
550    /// Check if running
551    pub fn is_running(&self) -> bool {
552        self.running.load(Ordering::Relaxed)
553    }
554
555    /// Get statistics
556    pub async fn stats(&self) -> KafkaStreamStats {
557        self.stats.read().await.clone()
558    }
559}
560
561/// Configuration for Kafka stream pipeline
562#[derive(Debug, Clone)]
563pub struct KafkaStreamConfig {
564    /// Kafka brokers
565    pub brokers: String,
566
567    /// Consumer group ID
568    pub group_id: String,
569
570    /// Input topics
571    pub input_topics: Vec<String>,
572
573    /// Output topic
574    pub output_topic: String,
575
576    /// Auto-commit interval
577    pub auto_commit_interval_ms: u64,
578
579    /// Poll timeout
580    pub poll_timeout_ms: u64,
581
582    /// Delivery timeout
583    pub delivery_timeout_ms: u64,
584
585    /// Max retries for producing
586    pub max_produce_retries: u32,
587
588    /// Additional consumer config
589    pub consumer_config: HashMap<String, String>,
590
591    /// Additional producer config
592    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
612/// End-to-end Kafka stream processing pipeline
613///
614/// This component orchestrates the complete Kafka-to-Kafka stream processing flow:
615/// 1. Consume events from input topics
616/// 2. Process through StreamExecutor (windowing, aggregation, etc.)
617/// 3. Produce results to output topic
618/// 4. Manage offsets automatically
619/// 5. Handle errors and recovery
620pub struct KafkaStreamPipeline<T, R>
621where
622    T: Clone + Send + Sync + 'static,
623    R: Clone + Send + Sync + 'static,
624{
625    /// Configuration
626    config: KafkaStreamConfig,
627
628    /// Stream executor (public for accessing outputs)
629    pub executor: Arc<StreamExecutor<T>>,
630
631    /// Kafka consumer
632    consumer: Arc<StreamConsumer>,
633
634    /// Kafka producer
635    producer: Arc<FutureProducer>,
636
637    /// Offset manager
638    offset_manager: Arc<OffsetManager>,
639
640    /// Stream source
641    source: Option<KafkaStreamSource<T>>,
642
643    /// Stream sink
644    sink: Option<KafkaStreamSink<R>>,
645
646    /// Statistics (public for monitoring)
647    pub stats: Arc<RwLock<KafkaStreamStats>>,
648
649    /// Running flag
650    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    /// Create a new Kafka stream pipeline
659    pub fn new(
660        config: KafkaStreamConfig,
661        executor: StreamExecutor<T>,
662    ) -> Result<Self> {
663        // Create consumer
664        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        // Apply additional consumer config
672        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        // Create producer
683        let mut producer_config = ClientConfig::new();
684        producer_config
685            .set("bootstrap.servers", &config.brokers)
686            .set("message.timeout.ms", "5000");
687
688        // Apply additional producer config
689        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        // Create offset manager
705        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    /// Run the complete pipeline
725    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        // Create source
734        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        // Create sink
744        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        // Start offset auto-commit
754        self.offset_manager.start_auto_commit();
755
756        // Start executor
757        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        // Start source
767        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        // Clone sink for storage
777        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        // Start sink
788        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        // Wait for all tasks
798        let _ = tokio::try_join!(executor_handle, source_handle, sink_handle);
799
800        info!("Kafka stream pipeline stopped");
801        Ok(())
802    }
803
804    /// Gracefully shutdown the pipeline
805    pub async fn shutdown(&mut self) -> Result<()> {
806        info!("Shutting down Kafka stream pipeline");
807
808        self.running.store(false, Ordering::Relaxed);
809
810        // Stop components
811        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        // Flush producer
822        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        // Commit final offsets
830        info!("Committing final offsets");
831        self.offset_manager.commit_offsets().await?;
832
833        // Stop offset manager
834        self.offset_manager.stop();
835
836        info!("Shutdown complete");
837        Ok(())
838    }
839
840    /// Get current statistics
841    pub async fn stats(&self) -> KafkaStreamStats {
842        self.stats.read().await.clone()
843    }
844
845    /// Check if pipeline is running
846    pub fn is_running(&self) -> bool {
847        self.running.load(Ordering::Relaxed)
848    }
849
850    /// Perform health check
851    pub async fn health_check(&self) -> bool {
852        // Check if executor is running
853        if !self.executor.is_running() {
854            warn!("Executor is not running");
855            return false;
856        }
857
858        // Check if source is running
859        if let Some(source) = &self.source {
860            if !source.is_running() {
861                warn!("Source is not running");
862                return false;
863            }
864        }
865
866        // Check if sink is running
867        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
878// Make KafkaStreamSource cloneable by cloning the Arc references
879impl<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}