Skip to main content

llm_optimizer_processor/
stream_processor.rs

1//! High-level stream processor for event processing with windowing and aggregation
2//!
3//! This module provides the `StreamProcessor` which coordinates:
4//! - Event ingestion and time extraction
5//! - Window assignment and management
6//! - Watermark generation and propagation
7//! - Aggregation computation per window
8//! - Result emission
9//!
10//! # Architecture
11//!
12//! ```text
13//! ┌─────────────┐
14//! │   Events    │
15//! └──────┬──────┘
16//!        │
17//!        ▼
18//! ┌─────────────────────┐
19//! │  Time Extraction    │
20//! └──────┬──────────────┘
21//!        │
22//!        ▼
23//! ┌─────────────────────┐
24//! │ Window Assignment   │ ◄─── WindowManager
25//! └──────┬──────────────┘
26//!        │
27//!        ▼
28//! ┌─────────────────────┐
29//! │   Aggregation       │ ◄─── Aggregators
30//! └──────┬──────────────┘
31//!        │
32//!        ▼
33//! ┌─────────────────────┐
34//! │ Watermark Check     │
35//! └──────┬──────────────┘
36//!        │
37//!        ▼
38//! ┌─────────────────────┐
39//! │  Trigger & Emit     │
40//! └──────┬──────────────┘
41//!        │
42//!        ▼
43//! ┌─────────────────────┐
44//! │    Results          │
45//! └─────────────────────┘
46//! ```
47//!
48//! # Example
49//!
50//! ```rust
51//! use processor::{
52//!     StreamProcessor, StreamProcessorBuilder,
53//!     TumblingWindowAssigner, OnWatermarkTrigger,
54//! };
55//! use processor::aggregation::CompositeAggregator;
56//! use chrono::Duration;
57//!
58//! # async fn example() -> anyhow::Result<()> {
59//! // Build a stream processor for latency metrics
60//! let processor = StreamProcessorBuilder::new()
61//!     .with_tumbling_window(Duration::minutes(5))
62//!     .with_watermark_trigger()
63//!     .with_aggregation(|_window_id| {
64//!         let mut agg = CompositeAggregator::new();
65//!         agg.add_count("request_count")
66//!            .add_avg("avg_latency")
67//!            .add_p50("p50_latency")
68//!            .add_p95("p95_latency")
69//!            .add_p99("p99_latency")
70//!            .add_max("max_latency");
71//!         agg
72//!     })
73//!     .build();
74//!
75//! // Process events
76//! // processor.process_event(event).await?;
77//! # Ok(())
78//! # }
79//! ```
80
81use crate::aggregation::{Aggregator, CompositeAggregator, AggregationResult};
82use crate::core::{EventTimeExtractor, KeyExtractor, ProcessorEvent};
83use crate::error::{ProcessorError, WindowError};
84use crate::watermark::{BoundedOutOfOrdernessWatermark, WatermarkGenerator};
85use crate::window::{
86    Window, WindowAssigner, WindowManager, WindowTrigger, WindowMetadata,
87    TumblingWindowAssigner, SlidingWindowAssigner, SessionWindowAssigner,
88    OnWatermarkTrigger, ProcessingTimeTrigger, CountTrigger,
89};
90use chrono::{DateTime, Duration, Utc};
91use dashmap::DashMap;
92use futures::Stream;
93use serde::{Deserialize, Serialize};
94use std::collections::HashMap;
95use std::marker::PhantomData;
96use std::pin::Pin;
97use std::sync::Arc;
98use tokio::sync::{mpsc, RwLock};
99use tokio_stream::wrappers::ReceiverStream;
100use tracing::{debug, error, info, trace, warn};
101
102/// Result emitted by the stream processor
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct WindowResult {
105    /// Window that produced this result
106    pub window: Window,
107    /// Aggregation key
108    pub key: String,
109    /// Aggregated results
110    pub results: AggregationResult,
111    /// Number of events processed
112    pub event_count: u64,
113    /// Processing timestamp
114    pub processed_at: DateTime<Utc>,
115    /// Watermark at the time of processing
116    pub watermark: Option<DateTime<Utc>>,
117}
118
119/// Configuration for the stream processor
120#[derive(Debug, Clone)]
121pub struct StreamProcessorConfig {
122    /// Allow processing of late events
123    pub allow_late_events: bool,
124    /// Maximum lateness threshold
125    pub late_event_threshold: Duration,
126    /// Watermark generation interval
127    pub watermark_interval: Duration,
128    /// Window cleanup retention duration
129    pub window_retention: Duration,
130    /// Maximum number of concurrent windows per key
131    pub max_windows_per_key: usize,
132    /// Buffer size for result emission
133    pub result_buffer_size: usize,
134}
135
136impl Default for StreamProcessorConfig {
137    fn default() -> Self {
138        Self {
139            allow_late_events: true,
140            late_event_threshold: Duration::minutes(5),
141            watermark_interval: Duration::seconds(1),
142            window_retention: Duration::hours(1),
143            max_windows_per_key: 1000,
144            result_buffer_size: 1000,
145        }
146    }
147}
148
149/// Statistics for the stream processor
150#[derive(Debug, Clone, Default, Serialize, Deserialize)]
151pub struct ProcessorStats {
152    /// Total events processed
153    pub events_processed: u64,
154    /// Total events dropped (late or errors)
155    pub events_dropped: u64,
156    /// Total windows created
157    pub windows_created: u64,
158    /// Total windows fired
159    pub windows_fired: u64,
160    /// Current active windows
161    pub active_windows: usize,
162    /// Current watermark
163    pub current_watermark: Option<DateTime<Utc>>,
164    /// Processing latency in milliseconds (p50, p95, p99)
165    pub latency_p50_ms: f64,
166    pub latency_p95_ms: f64,
167    pub latency_p99_ms: f64,
168    /// Last update timestamp
169    pub last_update: DateTime<Utc>,
170}
171
172/// Per-key aggregation state
173struct KeyState<A: Aggregator> {
174    /// Aggregators for each active window
175    aggregators: HashMap<String, A>,
176    /// Last seen event time
177    last_event_time: Option<DateTime<Utc>>,
178}
179
180impl<A: Aggregator> KeyState<A> {
181    fn new() -> Self {
182        Self {
183            aggregators: HashMap::new(),
184            last_event_time: None,
185        }
186    }
187}
188
189/// Stream processor for windowed aggregations
190pub struct StreamProcessor<T, A, W, Tr>
191where
192    A: Aggregator<Input = f64, Output = f64> + Clone + Send + Sync + 'static,
193    W: WindowAssigner + Clone + Send + Sync + 'static,
194    Tr: WindowTrigger + Clone + Send + Sync + 'static,
195{
196    /// Configuration
197    config: StreamProcessorConfig,
198    /// Window manager
199    window_manager: Arc<RwLock<WindowManager<W, Tr>>>,
200    /// Watermark generator
201    watermark_generator: Arc<RwLock<dyn WatermarkGenerator>>,
202    /// Aggregators per key per window
203    aggregators: Arc<DashMap<String, KeyState<A>>>,
204    /// Result sender
205    result_tx: mpsc::Sender<WindowResult>,
206    /// Result receiver
207    result_rx: Option<mpsc::Receiver<WindowResult>>,
208    /// Statistics
209    stats: Arc<RwLock<ProcessorStats>>,
210    /// Aggregator factory
211    aggregator_factory: Arc<dyn Fn(&str) -> A + Send + Sync>,
212    /// Phantom data for event type
213    _phantom: PhantomData<T>,
214}
215
216impl<T, A, W, Tr> StreamProcessor<T, A, W, Tr>
217where
218    A: Aggregator<Input = f64, Output = f64> + Clone + Send + Sync + 'static,
219    W: WindowAssigner + Clone + Send + Sync + 'static,
220    Tr: WindowTrigger + Clone + Send + Sync + 'static,
221{
222    /// Create a new stream processor
223    pub fn new<F>(
224        config: StreamProcessorConfig,
225        assigner: W,
226        trigger: Tr,
227        aggregator_factory: F,
228    ) -> Self
229    where
230        F: Fn(&str) -> A + Send + Sync + 'static,
231    {
232        let window_manager = WindowManager::with_late_events(
233            assigner,
234            trigger,
235            config.allow_late_events,
236            config.late_event_threshold,
237        );
238
239        let (result_tx, result_rx) = mpsc::channel(config.result_buffer_size);
240
241        // Convert late_event_threshold before moving config
242        let watermark_threshold = config.late_event_threshold.to_std().unwrap_or(std::time::Duration::from_secs(300));
243
244        Self {
245            config,
246            window_manager: Arc::new(RwLock::new(window_manager)),
247            watermark_generator: Arc::new(RwLock::new(
248                BoundedOutOfOrdernessWatermark::new(
249                    watermark_threshold,
250                    None,
251                )
252            )),
253            aggregators: Arc::new(DashMap::new()),
254            result_tx,
255            result_rx: Some(result_rx),
256            stats: Arc::new(RwLock::new(ProcessorStats::default())),
257            aggregator_factory: Arc::new(aggregator_factory),
258            _phantom: PhantomData,
259        }
260    }
261
262    /// Process a single event
263    ///
264    /// This is the main entry point for event processing. It:
265    /// 1. Assigns the event to windows
266    /// 2. Updates aggregators for each window
267    /// 3. Updates watermark
268    /// 4. Checks for windows ready to fire
269    /// 5. Emits results for completed windows
270    pub async fn process_event(
271        &self,
272        key: String,
273        event_time: DateTime<Utc>,
274        value: f64,
275    ) -> Result<(), ProcessorError> {
276        let start_time = Utc::now();
277
278        // Update statistics
279        {
280            let mut stats = self.stats.write().await;
281            stats.events_processed += 1;
282            stats.last_update = Utc::now();
283        }
284
285        // Assign event to windows
286        let windows = {
287            let mut manager = self.window_manager.write().await;
288            match manager.assign_event(event_time) {
289                Ok(windows) => windows,
290                Err(e) => {
291                    warn!(
292                        error = %e,
293                        event_time = %event_time,
294                        "Failed to assign event to windows"
295                    );
296                    let mut stats = self.stats.write().await;
297                    stats.events_dropped += 1;
298                    return Err(e);
299                }
300            }
301        };
302
303        if windows.is_empty() {
304            // Event was dropped (too late)
305            let mut stats = self.stats.write().await;
306            stats.events_dropped += 1;
307            return Ok(());
308        }
309
310        trace!(
311            key = %key,
312            event_time = %event_time,
313            window_count = windows.len(),
314            "Processing event"
315        );
316
317        // Update aggregators for each window
318        let mut key_state = self.aggregators
319            .entry(key.clone())
320            .or_insert_with(KeyState::new);
321
322        for window in &windows {
323            let aggregator = key_state.aggregators
324                .entry(window.id.clone())
325                .or_insert_with(|| (self.aggregator_factory)(&key));
326
327            if let Err(e) = aggregator.update(value) {
328                error!(
329                    window_id = %window.id,
330                    error = %e,
331                    "Failed to update aggregator"
332                );
333                return Err(ProcessorError::Aggregation(
334                    crate::error::AggregationError::CorruptedState {
335                        aggregation_type: "unknown".to_string(),
336                        details: e.to_string(),
337                    }
338                ));
339            }
340        }
341
342        key_state.last_event_time = Some(event_time);
343        drop(key_state);
344
345        // Update watermark
346        let watermark = {
347            let mut wm_gen = self.watermark_generator.write().await;
348            wm_gen.on_event(event_time.timestamp_millis(), 0);
349            wm_gen.current_watermark()
350        };
351
352        // Check for windows ready to fire
353        let ready_windows = {
354            let mut manager = self.window_manager.write().await;
355            manager.advance_watermark(watermark.to_datetime())?
356        };
357
358        // Process ready windows
359        for window_id in ready_windows {
360            self.fire_window(&key, &window_id, watermark.to_datetime()).await?;
361        }
362
363        // Update latency statistics
364        let latency = (Utc::now() - start_time).num_milliseconds() as f64;
365        self.update_latency_stats(latency).await;
366
367        Ok(())
368    }
369
370    /// Fire a window and emit results
371    async fn fire_window(
372        &self,
373        key: &str,
374        window_id: &str,
375        watermark: DateTime<Utc>,
376    ) -> Result<(), ProcessorError> {
377        // Get window metadata
378        let metadata = {
379            let manager = self.window_manager.read().await;
380            manager.get_window(window_id)
381        };
382
383        let metadata = match metadata {
384            Some(m) => m,
385            None => {
386                warn!(window_id = %window_id, "Window not found");
387                return Ok(());
388            }
389        };
390
391        // Get and remove aggregator
392        let results = {
393            let mut key_state = match self.aggregators.get_mut(key) {
394                Some(state) => state,
395                None => {
396                    warn!(key = %key, window_id = %window_id, "Key state not found");
397                    return Ok(());
398                }
399            };
400
401            let aggregator = match key_state.aggregators.remove(window_id) {
402                Some(agg) => agg,
403                None => {
404                    warn!(key = %key, window_id = %window_id, "Aggregator not found");
405                    return Ok(());
406                }
407            };
408
409            // Finalize aggregation based on type
410            // For CompositeAggregator, use finalize directly
411            // For other aggregators, we need to handle differently
412            if let Some(composite) = (&aggregator as &dyn std::any::Any).downcast_ref::<CompositeAggregator>() {
413                composite.finalize().map_err(|e| {
414                    ProcessorError::Aggregation(crate::error::AggregationError::CorruptedState {
415                        aggregation_type: "composite".to_string(),
416                        details: e.to_string(),
417                    })
418                })?
419            } else {
420                // For simple aggregators, create a result with single value
421                let value = aggregator.finalize().map_err(|e| {
422                    ProcessorError::Aggregation(crate::error::AggregationError::CorruptedState {
423                        aggregation_type: "simple".to_string(),
424                        details: e.to_string(),
425                    })
426                })?;
427
428                let mut result = AggregationResult::new();
429                result.set_value("value", value);
430                result.set_count("count", aggregator.count());
431                result
432            }
433        };
434
435        // Create window result
436        let window_result = WindowResult {
437            window: metadata.window.clone(),
438            key: key.to_string(),
439            results,
440            event_count: metadata.stats.event_count,
441            processed_at: Utc::now(),
442            watermark: Some(watermark),
443        };
444
445        debug!(
446            window_id = %window_id,
447            key = %key,
448            event_count = metadata.stats.event_count,
449            "Window fired"
450        );
451
452        // Emit result
453        if let Err(e) = self.result_tx.send(window_result).await {
454            error!(error = %e, "Failed to send window result");
455        }
456
457        // Update statistics
458        {
459            let mut stats = self.stats.write().await;
460            stats.windows_fired += 1;
461            stats.current_watermark = Some(watermark);
462        }
463
464        Ok(())
465    }
466
467    /// Update latency statistics
468    async fn update_latency_stats(&self, latency_ms: f64) {
469        // Simple exponential moving average for now
470        // In production, you'd want more sophisticated percentile tracking
471        let mut stats = self.stats.write().await;
472
473        if stats.latency_p50_ms == 0.0 {
474            stats.latency_p50_ms = latency_ms;
475            stats.latency_p95_ms = latency_ms;
476            stats.latency_p99_ms = latency_ms;
477        } else {
478            // EMA with different smoothing factors
479            stats.latency_p50_ms = 0.9 * stats.latency_p50_ms + 0.1 * latency_ms;
480            stats.latency_p95_ms = 0.95 * stats.latency_p95_ms + 0.05 * latency_ms.max(stats.latency_p95_ms);
481            stats.latency_p99_ms = 0.99 * stats.latency_p99_ms + 0.01 * latency_ms.max(stats.latency_p99_ms);
482        }
483    }
484
485    /// Get a stream of window results
486    pub fn results(&mut self) -> Option<impl Stream<Item = WindowResult>> {
487        self.result_rx.take().map(ReceiverStream::new)
488    }
489
490    /// Get current statistics
491    pub async fn stats(&self) -> ProcessorStats {
492        let mut stats = self.stats.read().await.clone();
493
494        // Update active windows count
495        let manager = self.window_manager.read().await;
496        stats.active_windows = manager.active_window_count();
497        stats.current_watermark = manager.current_watermark();
498
499        stats
500    }
501
502    /// Trigger cleanup of old windows
503    pub async fn cleanup(&self) -> Result<usize, ProcessorError> {
504        let mut manager = self.window_manager.write().await;
505        let removed = manager.cleanup_old_windows(self.config.window_retention);
506
507        if removed > 0 {
508            info!(removed, "Cleaned up old windows");
509        }
510
511        Ok(removed)
512    }
513
514    /// Reset the processor (for testing)
515    pub async fn reset(&self) {
516        let mut manager = self.window_manager.write().await;
517        manager.reset();
518
519        // Cannot directly assign to dyn trait, so we need to replace the entire Arc
520        // For now, just reset the internal state by clearing aggregators
521        // A proper reset would require restructuring the watermark_generator field
522
523        self.aggregators.clear();
524
525        let mut stats = self.stats.write().await;
526        *stats = ProcessorStats::default();
527
528        info!("Stream processor reset");
529    }
530}
531
532/// Builder for creating stream processors
533pub struct StreamProcessorBuilder<T, A> {
534    config: StreamProcessorConfig,
535    _phantom: PhantomData<(T, A)>,
536}
537
538impl<T, A> Default for StreamProcessorBuilder<T, A>
539where
540    A: Aggregator<Input = f64, Output = f64> + Clone + Send + Sync + 'static,
541{
542    fn default() -> Self {
543        Self::new()
544    }
545}
546
547impl<T, A> StreamProcessorBuilder<T, A>
548where
549    A: Aggregator<Input = f64, Output = f64> + Clone + Send + Sync + 'static,
550{
551    /// Create a new builder
552    pub fn new() -> Self {
553        Self {
554            config: StreamProcessorConfig::default(),
555            _phantom: PhantomData,
556        }
557    }
558
559    /// Set configuration
560    pub fn with_config(mut self, config: StreamProcessorConfig) -> Self {
561        self.config = config;
562        self
563    }
564
565    /// Allow late events
566    pub fn allow_late_events(mut self, allow: bool, threshold: Duration) -> Self {
567        self.config.allow_late_events = allow;
568        self.config.late_event_threshold = threshold;
569        self
570    }
571
572    /// Set watermark interval
573    pub fn watermark_interval(mut self, interval: Duration) -> Self {
574        self.config.watermark_interval = interval;
575        self
576    }
577
578    /// Build with tumbling windows
579    pub fn build_tumbling<F>(
580        self,
581        window_size: Duration,
582        aggregator_factory: F,
583    ) -> StreamProcessor<T, A, TumblingWindowAssigner, OnWatermarkTrigger>
584    where
585        F: Fn(&str) -> A + Send + Sync + 'static,
586    {
587        let assigner = TumblingWindowAssigner::new(window_size);
588        let trigger = OnWatermarkTrigger::new();
589
590        StreamProcessor::new(self.config, assigner, trigger, aggregator_factory)
591    }
592
593    /// Build with sliding windows
594    pub fn build_sliding<F>(
595        self,
596        window_size: Duration,
597        slide: Duration,
598        aggregator_factory: F,
599    ) -> StreamProcessor<T, A, SlidingWindowAssigner, OnWatermarkTrigger>
600    where
601        F: Fn(&str) -> A + Send + Sync + 'static,
602    {
603        let assigner = SlidingWindowAssigner::new(window_size, slide);
604        let trigger = OnWatermarkTrigger::new();
605
606        StreamProcessor::new(self.config, assigner, trigger, aggregator_factory)
607    }
608
609    /// Build with session windows
610    pub fn build_session<F>(
611        self,
612        gap: Duration,
613        aggregator_factory: F,
614    ) -> StreamProcessor<T, A, SessionWindowAssigner, OnWatermarkTrigger>
615    where
616        F: Fn(&str) -> A + Send + Sync + 'static,
617    {
618        let assigner = SessionWindowAssigner::new(gap);
619        let trigger = OnWatermarkTrigger::new();
620
621        StreamProcessor::new(self.config, assigner, trigger, aggregator_factory)
622    }
623}
624
625#[cfg(test)]
626mod tests {
627    use super::*;
628    use crate::aggregation::AverageAggregator;
629    use tokio_stream::StreamExt;
630
631    #[tokio::test]
632    async fn test_stream_processor_basic() {
633        let processor = StreamProcessorBuilder::new()
634            .build_tumbling(
635                Duration::seconds(1),
636                |_key| AverageAggregator::new(),
637            );
638
639        // Process events
640        let base_time = Utc::now();
641        processor.process_event("key1".to_string(), base_time, 10.0).await.unwrap();
642        processor.process_event("key1".to_string(), base_time + Duration::milliseconds(500), 20.0).await.unwrap();
643
644        let stats = processor.stats().await;
645        assert_eq!(stats.events_processed, 2);
646    }
647
648    #[tokio::test]
649    async fn test_stream_processor_windowing() {
650        let mut processor = StreamProcessorBuilder::new()
651            .build_tumbling(
652                Duration::milliseconds(1000),
653                |_key| AverageAggregator::new(),
654            );
655
656        let mut results_stream = processor.results().unwrap();
657
658        // Process events in first window
659        let base_time = Utc::now();
660        processor.process_event("key1".to_string(), base_time, 10.0).await.unwrap();
661        processor.process_event("key1".to_string(), base_time + Duration::milliseconds(500), 20.0).await.unwrap();
662
663        // Process event in second window to trigger watermark
664        processor.process_event(
665            "key1".to_string(),
666            base_time + Duration::milliseconds(2000),
667            30.0
668        ).await.unwrap();
669
670        // Should have fired first window
671        tokio::time::timeout(
672            std::time::Duration::from_secs(1),
673            results_stream.next()
674        ).await.ok();
675    }
676
677    #[tokio::test]
678    async fn test_stream_processor_multiple_keys() {
679        let processor = StreamProcessorBuilder::new()
680            .build_tumbling(
681                Duration::seconds(1),
682                |_key| AverageAggregator::new(),
683            );
684
685        let base_time = Utc::now();
686        processor.process_event("key1".to_string(), base_time, 10.0).await.unwrap();
687        processor.process_event("key2".to_string(), base_time, 20.0).await.unwrap();
688        processor.process_event("key1".to_string(), base_time + Duration::milliseconds(500), 30.0).await.unwrap();
689
690        let stats = processor.stats().await;
691        assert_eq!(stats.events_processed, 3);
692    }
693
694    #[tokio::test]
695    async fn test_stream_processor_cleanup() {
696        let processor = StreamProcessorBuilder::new()
697            .build_tumbling(
698                Duration::milliseconds(100),
699                |_key| AverageAggregator::new(),
700            );
701
702        let base_time = Utc::now();
703        processor.process_event("key1".to_string(), base_time, 10.0).await.unwrap();
704
705        // Trigger window
706        processor.process_event(
707            "key1".to_string(),
708            base_time + Duration::seconds(1),
709            20.0
710        ).await.unwrap();
711
712        // Cleanup
713        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
714        let removed = processor.cleanup().await.unwrap();
715        assert!(removed > 0);
716    }
717}