Skip to main content

llm_optimizer_processor/
watermark.rs

1//! Watermarking system for handling late events in stream processing
2//!
3//! This module provides watermarking capabilities to handle out-of-order and late-arriving events
4//! in the stream processing pipeline. Watermarks represent a timestamp threshold indicating that
5//! all events with timestamps below this threshold have been processed.
6//!
7//! # Overview
8//!
9//! The watermarking system supports:
10//! - Bounded out-of-orderness handling
11//! - Per-partition watermark tracking
12//! - Idle partition detection
13//! - Periodic and punctuated watermark generation
14//! - Late event detection and handling
15//!
16//! # Example
17//!
18//! ```rust
19//! use processor::watermark::{Watermark, BoundedOutOfOrdernessWatermark, WatermarkGenerator};
20//! use std::time::Duration;
21//!
22//! let mut generator = BoundedOutOfOrdernessWatermark::new(
23//!     Duration::from_secs(10),  // 10 second max out-of-orderness
24//!     Some(Duration::from_secs(60)) // 60 second idle timeout
25//! );
26//!
27//! // Process an event with timestamp 1000ms
28//! if let Some(watermark) = generator.on_event(1000, 0) {
29//!     println!("New watermark: {:?}", watermark);
30//! }
31//! ```
32
33use chrono::{DateTime, Duration as ChronoDuration, Utc};
34use dashmap::DashMap;
35use serde::{Deserialize, Serialize};
36use std::cmp::max;
37use std::collections::HashMap;
38use std::sync::{Arc, Mutex};
39use std::time::Duration;
40use tracing::{debug, trace, warn};
41
42/// Represents a watermark timestamp
43///
44/// Watermarks indicate that all events with timestamps less than or equal to the watermark
45/// have been processed. This allows the system to trigger time-based operations and
46/// detect late-arriving events.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
48pub struct Watermark {
49    /// The watermark timestamp in milliseconds since epoch
50    pub timestamp: i64,
51}
52
53impl Watermark {
54    /// Creates a new watermark with the given timestamp
55    pub fn new(timestamp: i64) -> Self {
56        Self { timestamp }
57    }
58
59    /// Creates a watermark from a DateTime
60    pub fn from_datetime(dt: DateTime<Utc>) -> Self {
61        Self {
62            timestamp: dt.timestamp_millis(),
63        }
64    }
65
66    /// Converts the watermark to a DateTime
67    pub fn to_datetime(&self) -> DateTime<Utc> {
68        DateTime::from_timestamp_millis(self.timestamp)
69            .unwrap_or_else(|| Utc::now())
70    }
71
72    /// Returns the minimum possible watermark (beginning of time)
73    pub fn min() -> Self {
74        Self { timestamp: i64::MIN }
75    }
76
77    /// Returns the maximum possible watermark (end of time)
78    pub fn max() -> Self {
79        Self { timestamp: i64::MAX }
80    }
81
82    /// Checks if this watermark is before the given timestamp
83    pub fn is_before(&self, timestamp: i64) -> bool {
84        self.timestamp < timestamp
85    }
86
87    /// Checks if this watermark is after the given timestamp
88    pub fn is_after(&self, timestamp: i64) -> bool {
89        self.timestamp > timestamp
90    }
91
92    /// Returns true if this is the minimum watermark
93    pub fn is_min(&self) -> bool {
94        self.timestamp == i64::MIN
95    }
96
97    /// Returns true if this is the maximum watermark
98    pub fn is_max(&self) -> bool {
99        self.timestamp == i64::MAX
100    }
101}
102
103impl Default for Watermark {
104    fn default() -> Self {
105        Self::min()
106    }
107}
108
109impl std::fmt::Display for Watermark {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        write!(f, "Watermark({})", self.to_datetime())
112    }
113}
114
115/// Trait for generating watermarks from event streams
116pub trait WatermarkGenerator: Send + Sync {
117    /// Called when a new event arrives
118    ///
119    /// # Arguments
120    /// * `timestamp` - The event timestamp in milliseconds
121    /// * `partition` - The partition ID the event belongs to
122    ///
123    /// # Returns
124    /// An optional watermark if one should be emitted
125    fn on_event(&mut self, timestamp: i64, partition: u32) -> Option<Watermark>;
126
127    /// Called periodically to check if a watermark should be emitted
128    ///
129    /// # Returns
130    /// An optional watermark if one should be emitted
131    fn on_periodic_check(&mut self) -> Option<Watermark>;
132
133    /// Gets the current watermark without advancing it
134    fn current_watermark(&self) -> Watermark;
135
136    /// Resets the watermark generator to its initial state
137    fn reset(&mut self);
138
139    /// Gets the watermark for a specific partition
140    fn partition_watermark(&self, partition: u32) -> Option<Watermark>;
141}
142
143/// Configuration for bounded out-of-orderness watermarking
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct BoundedOutOfOrdernessConfig {
146    /// Maximum allowed out-of-orderness (delay to apply)
147    pub max_out_of_orderness: Duration,
148    /// Timeout after which a partition is considered idle
149    pub idle_timeout: Option<Duration>,
150    /// Whether to emit watermarks on every event
151    pub emit_per_event: bool,
152}
153
154impl Default for BoundedOutOfOrdernessConfig {
155    fn default() -> Self {
156        Self {
157            max_out_of_orderness: Duration::from_secs(5),
158            idle_timeout: Some(Duration::from_secs(60)),
159            emit_per_event: false,
160        }
161    }
162}
163
164/// Bounded out-of-orderness watermark generator
165///
166/// This generator tracks the maximum timestamp seen and generates watermarks by subtracting
167/// a fixed delay. It handles per-partition watermarks and can detect idle partitions.
168///
169/// # Example
170///
171/// ```rust
172/// use processor::watermark::{BoundedOutOfOrdernessWatermark, WatermarkGenerator};
173/// use std::time::Duration;
174///
175/// let mut generator = BoundedOutOfOrdernessWatermark::new(
176///     Duration::from_secs(5),
177///     None,
178/// );
179///
180/// // Process events
181/// generator.on_event(1000, 0);
182/// generator.on_event(2000, 0);
183/// if let Some(wm) = generator.on_event(3000, 0) {
184///     // Watermark at 3000 - 5000 = -2000 (clamped to min)
185/// }
186/// ```
187pub struct BoundedOutOfOrdernessWatermark {
188    /// Maximum timestamp seen across all partitions
189    max_timestamp: i64,
190    /// Per-partition maximum timestamps
191    partition_max_timestamps: DashMap<u32, i64>,
192    /// Per-partition last activity time
193    partition_last_activity: DashMap<u32, DateTime<Utc>>,
194    /// Configuration
195    config: BoundedOutOfOrdernessConfig,
196    /// Current global watermark
197    current_watermark: Arc<Mutex<Watermark>>,
198    /// Per-partition watermarks
199    partition_watermarks: DashMap<u32, Watermark>,
200}
201
202impl BoundedOutOfOrdernessWatermark {
203    /// Creates a new bounded out-of-orderness watermark generator
204    ///
205    /// # Arguments
206    /// * `max_out_of_orderness` - The maximum allowed delay for late events
207    /// * `idle_timeout` - Optional timeout for detecting idle partitions
208    pub fn new(max_out_of_orderness: Duration, idle_timeout: Option<Duration>) -> Self {
209        Self {
210            max_timestamp: i64::MIN,
211            partition_max_timestamps: DashMap::new(),
212            partition_last_activity: DashMap::new(),
213            config: BoundedOutOfOrdernessConfig {
214                max_out_of_orderness,
215                idle_timeout,
216                emit_per_event: false,
217            },
218            current_watermark: Arc::new(Mutex::new(Watermark::min())),
219            partition_watermarks: DashMap::new(),
220        }
221    }
222
223    /// Creates a new watermark generator with custom configuration
224    pub fn with_config(config: BoundedOutOfOrdernessConfig) -> Self {
225        Self {
226            max_timestamp: i64::MIN,
227            partition_max_timestamps: DashMap::new(),
228            partition_last_activity: DashMap::new(),
229            config,
230            current_watermark: Arc::new(Mutex::new(Watermark::min())),
231            partition_watermarks: DashMap::new(),
232        }
233    }
234
235    /// Computes the watermark based on the maximum timestamp and configured delay
236    fn compute_watermark(&self, max_ts: i64) -> Watermark {
237        let delay_ms = self.config.max_out_of_orderness.as_millis() as i64;
238        let watermark_ts = max_ts.saturating_sub(delay_ms);
239        Watermark::new(watermark_ts)
240    }
241
242    /// Updates the partition activity timestamp
243    fn update_partition_activity(&self, partition: u32) {
244        self.partition_last_activity.insert(partition, Utc::now());
245    }
246
247    /// Checks if a partition is idle based on the configured timeout
248    fn is_partition_idle(&self, partition: u32) -> bool {
249        if let Some(timeout) = self.config.idle_timeout {
250            if let Some(last_activity) = self.partition_last_activity.get(&partition) {
251                let elapsed = Utc::now().signed_duration_since(*last_activity);
252                return elapsed > ChronoDuration::from_std(timeout).unwrap_or_default();
253            }
254        }
255        false
256    }
257
258    /// Gets all active partitions (non-idle)
259    pub fn active_partitions(&self) -> Vec<u32> {
260        self.partition_max_timestamps
261            .iter()
262            .filter(|entry| !self.is_partition_idle(*entry.key()))
263            .map(|entry| *entry.key())
264            .collect()
265    }
266
267    /// Gets all idle partitions
268    pub fn idle_partitions(&self) -> Vec<u32> {
269        self.partition_max_timestamps
270            .iter()
271            .filter(|entry| self.is_partition_idle(*entry.key()))
272            .map(|entry| *entry.key())
273            .collect()
274    }
275
276    /// Merges partition watermarks to compute the global watermark
277    ///
278    /// The global watermark is the minimum of all partition watermarks to ensure
279    /// we don't advance past events that haven't been processed yet
280    fn merge_partition_watermarks(&self) -> Option<Watermark> {
281        let active_partitions: Vec<_> = self.active_partitions();
282
283        if active_partitions.is_empty() {
284            return None;
285        }
286
287        let min_watermark = active_partitions
288            .iter()
289            .filter_map(|&partition| self.partition_watermarks.get(&partition).map(|w| *w))
290            .min()?;
291
292        Some(min_watermark)
293    }
294
295    /// Checks if an event is late based on the current watermark
296    pub fn is_late_event(&self, timestamp: i64) -> bool {
297        let current = self.current_watermark.lock().unwrap();
298        timestamp < current.timestamp
299    }
300
301    /// Gets the lateness of an event (how late it is compared to the watermark)
302    ///
303    /// Returns 0 if the event is not late
304    pub fn get_lateness(&self, timestamp: i64) -> i64 {
305        let current = self.current_watermark.lock().unwrap();
306        if timestamp < current.timestamp {
307            current.timestamp - timestamp
308        } else {
309            0
310        }
311    }
312}
313
314impl WatermarkGenerator for BoundedOutOfOrdernessWatermark {
315    fn on_event(&mut self, timestamp: i64, partition: u32) -> Option<Watermark> {
316        trace!(
317            timestamp = timestamp,
318            partition = partition,
319            "Processing event for watermark"
320        );
321
322        // Update partition activity
323        self.update_partition_activity(partition);
324
325        // Update partition max timestamp
326        self.partition_max_timestamps
327            .entry(partition)
328            .and_modify(|max_ts| *max_ts = max(*max_ts, timestamp))
329            .or_insert(timestamp);
330
331        // Update global max timestamp
332        self.max_timestamp = max(self.max_timestamp, timestamp);
333
334        // Compute partition watermark
335        let partition_watermark = self.compute_watermark(timestamp);
336        self.partition_watermarks.insert(partition, partition_watermark);
337
338        // Merge partition watermarks to get global watermark
339        if let Some(new_watermark) = self.merge_partition_watermarks() {
340            let mut current = self.current_watermark.lock().unwrap();
341            if new_watermark > *current {
342                *current = new_watermark;
343                debug!(
344                    watermark = %new_watermark,
345                    "Advanced watermark"
346                );
347                return if self.config.emit_per_event {
348                    Some(new_watermark)
349                } else {
350                    None
351                };
352            }
353        }
354
355        None
356    }
357
358    fn on_periodic_check(&mut self) -> Option<Watermark> {
359        // Check for idle partitions and warn
360        let idle = self.idle_partitions();
361        if !idle.is_empty() {
362            warn!(partitions = ?idle, "Detected idle partitions");
363        }
364
365        // Recompute watermark based on active partitions
366        if let Some(new_watermark) = self.merge_partition_watermarks() {
367            let mut current = self.current_watermark.lock().unwrap();
368            if new_watermark > *current {
369                *current = new_watermark;
370                debug!(watermark = %new_watermark, "Periodic watermark update");
371                return Some(new_watermark);
372            }
373        }
374
375        None
376    }
377
378    fn current_watermark(&self) -> Watermark {
379        *self.current_watermark.lock().unwrap()
380    }
381
382    fn reset(&mut self) {
383        self.max_timestamp = i64::MIN;
384        self.partition_max_timestamps.clear();
385        self.partition_last_activity.clear();
386        self.partition_watermarks.clear();
387        *self.current_watermark.lock().unwrap() = Watermark::min();
388        debug!("Watermark generator reset");
389    }
390
391    fn partition_watermark(&self, partition: u32) -> Option<Watermark> {
392        self.partition_watermarks.get(&partition).map(|w| *w)
393    }
394}
395
396/// Configuration for periodic watermark emission
397#[derive(Debug, Clone, Serialize, Deserialize)]
398pub struct PeriodicWatermarkConfig {
399    /// Interval at which to emit watermarks
400    pub interval: Duration,
401    /// Maximum allowed out-of-orderness
402    pub max_out_of_orderness: Duration,
403}
404
405impl Default for PeriodicWatermarkConfig {
406    fn default() -> Self {
407        Self {
408            interval: Duration::from_secs(1),
409            max_out_of_orderness: Duration::from_secs(5),
410        }
411    }
412}
413
414/// Periodic watermark generator
415///
416/// Emits watermarks at regular intervals based on wall-clock time rather than event time.
417/// Useful for systems that need predictable watermark progression.
418pub struct PeriodicWatermark {
419    /// Last emission time
420    last_emission: DateTime<Utc>,
421    /// Configuration
422    config: PeriodicWatermarkConfig,
423    /// Underlying bounded watermark generator
424    bounded_generator: BoundedOutOfOrdernessWatermark,
425}
426
427impl PeriodicWatermark {
428    /// Creates a new periodic watermark generator
429    pub fn new(config: PeriodicWatermarkConfig) -> Self {
430        Self {
431            last_emission: Utc::now(),
432            config: config.clone(),
433            bounded_generator: BoundedOutOfOrdernessWatermark::new(
434                config.max_out_of_orderness,
435                None,
436            ),
437        }
438    }
439
440    /// Checks if it's time to emit a watermark
441    fn should_emit(&self) -> bool {
442        let elapsed = Utc::now().signed_duration_since(self.last_emission);
443        elapsed >= ChronoDuration::from_std(self.config.interval).unwrap_or_default()
444    }
445}
446
447impl WatermarkGenerator for PeriodicWatermark {
448    fn on_event(&mut self, timestamp: i64, partition: u32) -> Option<Watermark> {
449        // Update internal state but don't emit
450        self.bounded_generator.on_event(timestamp, partition);
451        None
452    }
453
454    fn on_periodic_check(&mut self) -> Option<Watermark> {
455        if self.should_emit() {
456            self.last_emission = Utc::now();
457            let watermark = self.bounded_generator.current_watermark();
458            if !watermark.is_min() {
459                debug!(watermark = %watermark, "Periodic watermark emission");
460                return Some(watermark);
461            }
462        }
463        None
464    }
465
466    fn current_watermark(&self) -> Watermark {
467        self.bounded_generator.current_watermark()
468    }
469
470    fn reset(&mut self) {
471        self.bounded_generator.reset();
472        self.last_emission = Utc::now();
473    }
474
475    fn partition_watermark(&self, partition: u32) -> Option<Watermark> {
476        self.bounded_generator.partition_watermark(partition)
477    }
478}
479
480/// Punctuated watermark generator
481///
482/// Emits watermarks based on specific patterns or markers in the event stream.
483/// For example, when a special "watermark event" is encountered.
484pub struct PunctuatedWatermark {
485    /// Function to extract watermark from event
486    extractor: Box<dyn Fn(i64, u32) -> Option<i64> + Send + Sync>,
487    /// Current watermark
488    current_watermark: Watermark,
489    /// Per-partition watermarks
490    partition_watermarks: HashMap<u32, Watermark>,
491}
492
493impl PunctuatedWatermark {
494    /// Creates a new punctuated watermark generator
495    ///
496    /// # Arguments
497    /// * `extractor` - Function that returns Some(watermark_timestamp) if the event
498    ///   should trigger a watermark emission, None otherwise
499    pub fn new<F>(extractor: F) -> Self
500    where
501        F: Fn(i64, u32) -> Option<i64> + Send + Sync + 'static,
502    {
503        Self {
504            extractor: Box::new(extractor),
505            current_watermark: Watermark::min(),
506            partition_watermarks: HashMap::new(),
507        }
508    }
509
510    /// Creates a watermark generator that emits on every Nth event
511    pub fn every_n_events(n: u64) -> Self {
512        let counter = Arc::new(Mutex::new(0u64));
513        Self::new(move |timestamp, _partition| {
514            let mut count = counter.lock().unwrap();
515            *count += 1;
516            if *count % n == 0 {
517                Some(timestamp)
518            } else {
519                None
520            }
521        })
522    }
523
524    /// Creates a watermark generator that emits when timestamp crosses boundaries
525    pub fn on_timestamp_boundary(boundary_ms: i64) -> Self {
526        let last_boundary = Arc::new(Mutex::new(0i64));
527        Self::new(move |timestamp, _partition| {
528            let mut last = last_boundary.lock().unwrap();
529            let current_boundary = (timestamp / boundary_ms) * boundary_ms;
530            if current_boundary > *last {
531                *last = current_boundary;
532                Some(current_boundary)
533            } else {
534                None
535            }
536        })
537    }
538}
539
540impl WatermarkGenerator for PunctuatedWatermark {
541    fn on_event(&mut self, timestamp: i64, partition: u32) -> Option<Watermark> {
542        if let Some(watermark_ts) = (self.extractor)(timestamp, partition) {
543            let new_watermark = Watermark::new(watermark_ts);
544
545            // Update partition watermark
546            self.partition_watermarks.insert(partition, new_watermark);
547
548            // Update global watermark if it's newer
549            if new_watermark > self.current_watermark {
550                self.current_watermark = new_watermark;
551                debug!(
552                    watermark = %new_watermark,
553                    partition = partition,
554                    "Punctuated watermark emission"
555                );
556                return Some(new_watermark);
557            }
558        }
559        None
560    }
561
562    fn on_periodic_check(&mut self) -> Option<Watermark> {
563        None // Punctuated watermarks only emit on events
564    }
565
566    fn current_watermark(&self) -> Watermark {
567        self.current_watermark
568    }
569
570    fn reset(&mut self) {
571        self.current_watermark = Watermark::min();
572        self.partition_watermarks.clear();
573    }
574
575    fn partition_watermark(&self, partition: u32) -> Option<Watermark> {
576        self.partition_watermarks.get(&partition).copied()
577    }
578}
579
580/// Late event handler configuration
581#[derive(Debug, Clone, Serialize, Deserialize)]
582pub struct LateEventConfig {
583    /// Maximum allowed lateness before dropping events
584    pub allowed_lateness: Duration,
585    /// Whether to log late events
586    pub log_late_events: bool,
587    /// Whether to emit metrics for late events
588    pub emit_metrics: bool,
589}
590
591impl Default for LateEventConfig {
592    fn default() -> Self {
593        Self {
594            allowed_lateness: Duration::from_secs(60),
595            log_late_events: true,
596            emit_metrics: true,
597        }
598    }
599}
600
601/// Handler for late events
602pub struct LateEventHandler {
603    /// Configuration
604    config: LateEventConfig,
605    /// Count of dropped late events
606    dropped_count: Arc<Mutex<u64>>,
607    /// Count of accepted late events
608    accepted_count: Arc<Mutex<u64>>,
609    /// Total lateness of accepted events
610    total_lateness: Arc<Mutex<i64>>,
611}
612
613impl LateEventHandler {
614    /// Creates a new late event handler
615    pub fn new(config: LateEventConfig) -> Self {
616        Self {
617            config,
618            dropped_count: Arc::new(Mutex::new(0)),
619            accepted_count: Arc::new(Mutex::new(0)),
620            total_lateness: Arc::new(Mutex::new(0)),
621        }
622    }
623
624    /// Handles a late event
625    ///
626    /// # Returns
627    /// True if the event should be processed, false if it should be dropped
628    pub fn handle_late_event(&self, lateness: i64, partition: u32) -> bool {
629        let lateness_duration = Duration::from_millis(lateness as u64);
630
631        if lateness_duration > self.config.allowed_lateness {
632            // Drop the event
633            *self.dropped_count.lock().unwrap() += 1;
634
635            if self.config.log_late_events {
636                warn!(
637                    lateness_ms = lateness,
638                    partition = partition,
639                    "Dropping late event beyond allowed lateness"
640                );
641            }
642
643            false
644        } else {
645            // Accept the event
646            *self.accepted_count.lock().unwrap() += 1;
647            *self.total_lateness.lock().unwrap() += lateness;
648
649            if self.config.log_late_events {
650                debug!(
651                    lateness_ms = lateness,
652                    partition = partition,
653                    "Accepting late event within allowed lateness"
654                );
655            }
656
657            true
658        }
659    }
660
661    /// Gets statistics about late event handling
662    pub fn get_stats(&self) -> LateEventStats {
663        let dropped = *self.dropped_count.lock().unwrap();
664        let accepted = *self.accepted_count.lock().unwrap();
665        let total_lateness = *self.total_lateness.lock().unwrap();
666
667        let avg_lateness = if accepted > 0 {
668            total_lateness / accepted as i64
669        } else {
670            0
671        };
672
673        LateEventStats {
674            dropped_count: dropped,
675            accepted_count: accepted,
676            average_lateness_ms: avg_lateness,
677        }
678    }
679
680    /// Resets the statistics
681    pub fn reset_stats(&self) {
682        *self.dropped_count.lock().unwrap() = 0;
683        *self.accepted_count.lock().unwrap() = 0;
684        *self.total_lateness.lock().unwrap() = 0;
685    }
686}
687
688/// Statistics for late event handling
689#[derive(Debug, Clone, Serialize, Deserialize)]
690pub struct LateEventStats {
691    /// Number of dropped late events
692    pub dropped_count: u64,
693    /// Number of accepted late events
694    pub accepted_count: u64,
695    /// Average lateness in milliseconds
696    pub average_lateness_ms: i64,
697}
698
699#[cfg(test)]
700mod tests {
701    use super::*;
702
703    #[test]
704    fn test_watermark_creation() {
705        let wm = Watermark::new(1000);
706        assert_eq!(wm.timestamp, 1000);
707        assert!(wm.is_before(2000));
708        assert!(wm.is_after(500));
709    }
710
711    #[test]
712    fn test_watermark_ordering() {
713        let wm1 = Watermark::new(1000);
714        let wm2 = Watermark::new(2000);
715        assert!(wm1 < wm2);
716        assert!(wm2 > wm1);
717    }
718
719    #[test]
720    fn test_watermark_min_max() {
721        let min = Watermark::min();
722        let max = Watermark::max();
723        assert!(min.is_min());
724        assert!(max.is_max());
725        assert!(min < max);
726    }
727
728    #[test]
729    fn test_bounded_watermark_basic() {
730        let mut generator = BoundedOutOfOrdernessWatermark::new(
731            Duration::from_secs(5),
732            None,
733        );
734
735        // Process events
736        generator.on_event(10000, 0);
737        generator.on_event(15000, 0);
738        generator.on_event(20000, 0);
739
740        let wm = generator.current_watermark();
741        // Watermark should be 20000 - 5000 = 15000
742        assert_eq!(wm.timestamp, 15000);
743    }
744
745    #[test]
746    fn test_bounded_watermark_out_of_order() {
747        let mut generator = BoundedOutOfOrdernessWatermark::new(
748            Duration::from_secs(10),
749            None,
750        );
751
752        // Events arrive out of order
753        generator.on_event(30000, 0);
754        generator.on_event(20000, 0);
755        generator.on_event(25000, 0);
756
757        let wm = generator.current_watermark();
758        // Watermark should be based on max timestamp 30000 - 10000 = 20000
759        assert_eq!(wm.timestamp, 20000);
760    }
761
762    #[test]
763    fn test_bounded_watermark_multiple_partitions() {
764        let mut generator = BoundedOutOfOrdernessWatermark::new(
765            Duration::from_secs(5),
766            None,
767        );
768
769        // Events from different partitions
770        generator.on_event(10000, 0);
771        generator.on_event(20000, 1);
772        generator.on_event(15000, 2);
773
774        // Global watermark should be min of all partitions
775        // Partition 0: 10000 - 5000 = 5000
776        // Partition 1: 20000 - 5000 = 15000
777        // Partition 2: 15000 - 5000 = 10000
778        // Min = 5000
779        let wm = generator.current_watermark();
780        assert_eq!(wm.timestamp, 5000);
781    }
782
783    #[test]
784    fn test_bounded_watermark_late_event_detection() {
785        let mut generator = BoundedOutOfOrdernessWatermark::new(
786            Duration::from_secs(5),
787            None,
788        );
789
790        generator.on_event(20000, 0);
791
792        // Current watermark is 15000
793        assert!(generator.is_late_event(10000));
794        assert!(!generator.is_late_event(16000));
795    }
796
797    #[test]
798    fn test_bounded_watermark_lateness_calculation() {
799        let mut generator = BoundedOutOfOrdernessWatermark::new(
800            Duration::from_secs(5),
801            None,
802        );
803
804        generator.on_event(20000, 0);
805
806        // Current watermark is 15000
807        assert_eq!(generator.get_lateness(10000), 5000);
808        assert_eq!(generator.get_lateness(16000), 0);
809    }
810
811    #[test]
812    fn test_periodic_watermark() {
813        let config = PeriodicWatermarkConfig {
814            interval: Duration::from_millis(100),
815            max_out_of_orderness: Duration::from_secs(5),
816        };
817        let mut generator = PeriodicWatermark::new(config);
818
819        // Process events
820        generator.on_event(10000, 0);
821        generator.on_event(20000, 0);
822
823        // Should not emit on event
824        assert!(generator.on_event(30000, 0).is_none());
825
826        // Sleep and check periodically
827        std::thread::sleep(Duration::from_millis(150));
828        let wm = generator.on_periodic_check();
829        assert!(wm.is_some());
830    }
831
832    #[test]
833    fn test_punctuated_watermark_every_n() {
834        let mut generator = PunctuatedWatermark::every_n_events(3);
835
836        // First two events should not emit
837        assert!(generator.on_event(1000, 0).is_none());
838        assert!(generator.on_event(2000, 0).is_none());
839
840        // Third event should emit
841        let wm = generator.on_event(3000, 0);
842        assert!(wm.is_some());
843        assert_eq!(wm.unwrap().timestamp, 3000);
844    }
845
846    #[test]
847    fn test_punctuated_watermark_boundary() {
848        let mut generator = PunctuatedWatermark::on_timestamp_boundary(10000);
849
850        // Events within same boundary
851        assert!(generator.on_event(5000, 0).is_none());
852        assert!(generator.on_event(8000, 0).is_none());
853
854        // Event crossing boundary
855        let wm = generator.on_event(15000, 0);
856        assert!(wm.is_some());
857        assert_eq!(wm.unwrap().timestamp, 10000);
858
859        // Another event crossing boundary
860        let wm2 = generator.on_event(25000, 0);
861        assert!(wm2.is_some());
862        assert_eq!(wm2.unwrap().timestamp, 20000);
863    }
864
865    #[test]
866    fn test_late_event_handler_within_lateness() {
867        let config = LateEventConfig {
868            allowed_lateness: Duration::from_secs(10),
869            log_late_events: false,
870            emit_metrics: true,
871        };
872        let handler = LateEventHandler::new(config);
873
874        // Event with 5 seconds lateness (within allowed)
875        assert!(handler.handle_late_event(5000, 0));
876
877        let stats = handler.get_stats();
878        assert_eq!(stats.accepted_count, 1);
879        assert_eq!(stats.dropped_count, 0);
880    }
881
882    #[test]
883    fn test_late_event_handler_beyond_lateness() {
884        let config = LateEventConfig {
885            allowed_lateness: Duration::from_secs(10),
886            log_late_events: false,
887            emit_metrics: true,
888        };
889        let handler = LateEventHandler::new(config);
890
891        // Event with 15 seconds lateness (beyond allowed)
892        assert!(!handler.handle_late_event(15000, 0));
893
894        let stats = handler.get_stats();
895        assert_eq!(stats.accepted_count, 0);
896        assert_eq!(stats.dropped_count, 1);
897    }
898
899    #[test]
900    fn test_late_event_handler_stats() {
901        let config = LateEventConfig::default();
902        let handler = LateEventHandler::new(config);
903
904        handler.handle_late_event(5000, 0);
905        handler.handle_late_event(10000, 0);
906        handler.handle_late_event(80000, 0); // Should be dropped
907
908        let stats = handler.get_stats();
909        assert_eq!(stats.accepted_count, 2);
910        assert_eq!(stats.dropped_count, 1);
911        assert_eq!(stats.average_lateness_ms, 7500);
912    }
913
914    #[test]
915    fn test_watermark_generator_reset() {
916        let mut generator = BoundedOutOfOrdernessWatermark::new(
917            Duration::from_secs(5),
918            None,
919        );
920
921        generator.on_event(20000, 0);
922        assert_ne!(generator.current_watermark(), Watermark::min());
923
924        generator.reset();
925        assert_eq!(generator.current_watermark(), Watermark::min());
926    }
927
928    #[test]
929    fn test_partition_watermark_tracking() {
930        let mut generator = BoundedOutOfOrdernessWatermark::new(
931            Duration::from_secs(5),
932            None,
933        );
934
935        generator.on_event(10000, 0);
936        generator.on_event(20000, 1);
937
938        let wm0 = generator.partition_watermark(0).unwrap();
939        let wm1 = generator.partition_watermark(1).unwrap();
940
941        assert_eq!(wm0.timestamp, 5000);
942        assert_eq!(wm1.timestamp, 15000);
943    }
944
945    #[test]
946    fn test_watermark_datetime_conversion() {
947        let dt = DateTime::from_timestamp(1000, 0).unwrap();
948        let wm = Watermark::from_datetime(dt);
949        let _dt2 = wm.to_datetime();
950
951        assert_eq!(wm.timestamp, 1000000); // milliseconds
952    }
953
954    #[test]
955    fn test_idle_partition_detection() {
956        let mut generator = BoundedOutOfOrdernessWatermark::new(
957            Duration::from_secs(5),
958            Some(Duration::from_millis(100)),
959        );
960
961        generator.on_event(10000, 0);
962        generator.on_event(20000, 1);
963
964        // Initially no idle partitions
965        assert_eq!(generator.idle_partitions().len(), 0);
966
967        // Wait for idle timeout
968        std::thread::sleep(Duration::from_millis(150));
969
970        // Now both partitions should be idle
971        let idle = generator.idle_partitions();
972        assert_eq!(idle.len(), 2);
973    }
974
975    #[test]
976    fn test_watermark_monotonicity() {
977        let mut generator = BoundedOutOfOrdernessWatermark::new(
978            Duration::from_secs(5),
979            None,
980        );
981
982        generator.on_event(20000, 0);
983        let wm1 = generator.current_watermark();
984
985        // Try to advance with older event (should not go backwards)
986        generator.on_event(15000, 0);
987        let wm2 = generator.current_watermark();
988
989        assert!(wm2 >= wm1);
990    }
991}