use chrono::{DateTime, Duration as ChronoDuration, Utc};
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use std::cmp::max;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tracing::{debug, trace, warn};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Watermark {
pub timestamp: i64,
}
impl Watermark {
pub fn new(timestamp: i64) -> Self {
Self { timestamp }
}
pub fn from_datetime(dt: DateTime<Utc>) -> Self {
Self {
timestamp: dt.timestamp_millis(),
}
}
pub fn to_datetime(&self) -> DateTime<Utc> {
DateTime::from_timestamp_millis(self.timestamp)
.unwrap_or_else(|| Utc::now())
}
pub fn min() -> Self {
Self { timestamp: i64::MIN }
}
pub fn max() -> Self {
Self { timestamp: i64::MAX }
}
pub fn is_before(&self, timestamp: i64) -> bool {
self.timestamp < timestamp
}
pub fn is_after(&self, timestamp: i64) -> bool {
self.timestamp > timestamp
}
pub fn is_min(&self) -> bool {
self.timestamp == i64::MIN
}
pub fn is_max(&self) -> bool {
self.timestamp == i64::MAX
}
}
impl Default for Watermark {
fn default() -> Self {
Self::min()
}
}
impl std::fmt::Display for Watermark {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Watermark({})", self.to_datetime())
}
}
pub trait WatermarkGenerator: Send + Sync {
fn on_event(&mut self, timestamp: i64, partition: u32) -> Option<Watermark>;
fn on_periodic_check(&mut self) -> Option<Watermark>;
fn current_watermark(&self) -> Watermark;
fn reset(&mut self);
fn partition_watermark(&self, partition: u32) -> Option<Watermark>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BoundedOutOfOrdernessConfig {
pub max_out_of_orderness: Duration,
pub idle_timeout: Option<Duration>,
pub emit_per_event: bool,
}
impl Default for BoundedOutOfOrdernessConfig {
fn default() -> Self {
Self {
max_out_of_orderness: Duration::from_secs(5),
idle_timeout: Some(Duration::from_secs(60)),
emit_per_event: false,
}
}
}
pub struct BoundedOutOfOrdernessWatermark {
max_timestamp: i64,
partition_max_timestamps: DashMap<u32, i64>,
partition_last_activity: DashMap<u32, DateTime<Utc>>,
config: BoundedOutOfOrdernessConfig,
current_watermark: Arc<Mutex<Watermark>>,
partition_watermarks: DashMap<u32, Watermark>,
}
impl BoundedOutOfOrdernessWatermark {
pub fn new(max_out_of_orderness: Duration, idle_timeout: Option<Duration>) -> Self {
Self {
max_timestamp: i64::MIN,
partition_max_timestamps: DashMap::new(),
partition_last_activity: DashMap::new(),
config: BoundedOutOfOrdernessConfig {
max_out_of_orderness,
idle_timeout,
emit_per_event: false,
},
current_watermark: Arc::new(Mutex::new(Watermark::min())),
partition_watermarks: DashMap::new(),
}
}
pub fn with_config(config: BoundedOutOfOrdernessConfig) -> Self {
Self {
max_timestamp: i64::MIN,
partition_max_timestamps: DashMap::new(),
partition_last_activity: DashMap::new(),
config,
current_watermark: Arc::new(Mutex::new(Watermark::min())),
partition_watermarks: DashMap::new(),
}
}
fn compute_watermark(&self, max_ts: i64) -> Watermark {
let delay_ms = self.config.max_out_of_orderness.as_millis() as i64;
let watermark_ts = max_ts.saturating_sub(delay_ms);
Watermark::new(watermark_ts)
}
fn update_partition_activity(&self, partition: u32) {
self.partition_last_activity.insert(partition, Utc::now());
}
fn is_partition_idle(&self, partition: u32) -> bool {
if let Some(timeout) = self.config.idle_timeout {
if let Some(last_activity) = self.partition_last_activity.get(&partition) {
let elapsed = Utc::now().signed_duration_since(*last_activity);
return elapsed > ChronoDuration::from_std(timeout).unwrap_or_default();
}
}
false
}
pub fn active_partitions(&self) -> Vec<u32> {
self.partition_max_timestamps
.iter()
.filter(|entry| !self.is_partition_idle(*entry.key()))
.map(|entry| *entry.key())
.collect()
}
pub fn idle_partitions(&self) -> Vec<u32> {
self.partition_max_timestamps
.iter()
.filter(|entry| self.is_partition_idle(*entry.key()))
.map(|entry| *entry.key())
.collect()
}
fn merge_partition_watermarks(&self) -> Option<Watermark> {
let active_partitions: Vec<_> = self.active_partitions();
if active_partitions.is_empty() {
return None;
}
let min_watermark = active_partitions
.iter()
.filter_map(|&partition| self.partition_watermarks.get(&partition).map(|w| *w))
.min()?;
Some(min_watermark)
}
pub fn is_late_event(&self, timestamp: i64) -> bool {
let current = self.current_watermark.lock().unwrap();
timestamp < current.timestamp
}
pub fn get_lateness(&self, timestamp: i64) -> i64 {
let current = self.current_watermark.lock().unwrap();
if timestamp < current.timestamp {
current.timestamp - timestamp
} else {
0
}
}
}
impl WatermarkGenerator for BoundedOutOfOrdernessWatermark {
fn on_event(&mut self, timestamp: i64, partition: u32) -> Option<Watermark> {
trace!(
timestamp = timestamp,
partition = partition,
"Processing event for watermark"
);
self.update_partition_activity(partition);
self.partition_max_timestamps
.entry(partition)
.and_modify(|max_ts| *max_ts = max(*max_ts, timestamp))
.or_insert(timestamp);
self.max_timestamp = max(self.max_timestamp, timestamp);
let partition_watermark = self.compute_watermark(timestamp);
self.partition_watermarks.insert(partition, partition_watermark);
if let Some(new_watermark) = self.merge_partition_watermarks() {
let mut current = self.current_watermark.lock().unwrap();
if new_watermark > *current {
*current = new_watermark;
debug!(
watermark = %new_watermark,
"Advanced watermark"
);
return if self.config.emit_per_event {
Some(new_watermark)
} else {
None
};
}
}
None
}
fn on_periodic_check(&mut self) -> Option<Watermark> {
let idle = self.idle_partitions();
if !idle.is_empty() {
warn!(partitions = ?idle, "Detected idle partitions");
}
if let Some(new_watermark) = self.merge_partition_watermarks() {
let mut current = self.current_watermark.lock().unwrap();
if new_watermark > *current {
*current = new_watermark;
debug!(watermark = %new_watermark, "Periodic watermark update");
return Some(new_watermark);
}
}
None
}
fn current_watermark(&self) -> Watermark {
*self.current_watermark.lock().unwrap()
}
fn reset(&mut self) {
self.max_timestamp = i64::MIN;
self.partition_max_timestamps.clear();
self.partition_last_activity.clear();
self.partition_watermarks.clear();
*self.current_watermark.lock().unwrap() = Watermark::min();
debug!("Watermark generator reset");
}
fn partition_watermark(&self, partition: u32) -> Option<Watermark> {
self.partition_watermarks.get(&partition).map(|w| *w)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PeriodicWatermarkConfig {
pub interval: Duration,
pub max_out_of_orderness: Duration,
}
impl Default for PeriodicWatermarkConfig {
fn default() -> Self {
Self {
interval: Duration::from_secs(1),
max_out_of_orderness: Duration::from_secs(5),
}
}
}
pub struct PeriodicWatermark {
last_emission: DateTime<Utc>,
config: PeriodicWatermarkConfig,
bounded_generator: BoundedOutOfOrdernessWatermark,
}
impl PeriodicWatermark {
pub fn new(config: PeriodicWatermarkConfig) -> Self {
Self {
last_emission: Utc::now(),
config: config.clone(),
bounded_generator: BoundedOutOfOrdernessWatermark::new(
config.max_out_of_orderness,
None,
),
}
}
fn should_emit(&self) -> bool {
let elapsed = Utc::now().signed_duration_since(self.last_emission);
elapsed >= ChronoDuration::from_std(self.config.interval).unwrap_or_default()
}
}
impl WatermarkGenerator for PeriodicWatermark {
fn on_event(&mut self, timestamp: i64, partition: u32) -> Option<Watermark> {
self.bounded_generator.on_event(timestamp, partition);
None
}
fn on_periodic_check(&mut self) -> Option<Watermark> {
if self.should_emit() {
self.last_emission = Utc::now();
let watermark = self.bounded_generator.current_watermark();
if !watermark.is_min() {
debug!(watermark = %watermark, "Periodic watermark emission");
return Some(watermark);
}
}
None
}
fn current_watermark(&self) -> Watermark {
self.bounded_generator.current_watermark()
}
fn reset(&mut self) {
self.bounded_generator.reset();
self.last_emission = Utc::now();
}
fn partition_watermark(&self, partition: u32) -> Option<Watermark> {
self.bounded_generator.partition_watermark(partition)
}
}
pub struct PunctuatedWatermark {
extractor: Box<dyn Fn(i64, u32) -> Option<i64> + Send + Sync>,
current_watermark: Watermark,
partition_watermarks: HashMap<u32, Watermark>,
}
impl PunctuatedWatermark {
pub fn new<F>(extractor: F) -> Self
where
F: Fn(i64, u32) -> Option<i64> + Send + Sync + 'static,
{
Self {
extractor: Box::new(extractor),
current_watermark: Watermark::min(),
partition_watermarks: HashMap::new(),
}
}
pub fn every_n_events(n: u64) -> Self {
let counter = Arc::new(Mutex::new(0u64));
Self::new(move |timestamp, _partition| {
let mut count = counter.lock().unwrap();
*count += 1;
if *count % n == 0 {
Some(timestamp)
} else {
None
}
})
}
pub fn on_timestamp_boundary(boundary_ms: i64) -> Self {
let last_boundary = Arc::new(Mutex::new(0i64));
Self::new(move |timestamp, _partition| {
let mut last = last_boundary.lock().unwrap();
let current_boundary = (timestamp / boundary_ms) * boundary_ms;
if current_boundary > *last {
*last = current_boundary;
Some(current_boundary)
} else {
None
}
})
}
}
impl WatermarkGenerator for PunctuatedWatermark {
fn on_event(&mut self, timestamp: i64, partition: u32) -> Option<Watermark> {
if let Some(watermark_ts) = (self.extractor)(timestamp, partition) {
let new_watermark = Watermark::new(watermark_ts);
self.partition_watermarks.insert(partition, new_watermark);
if new_watermark > self.current_watermark {
self.current_watermark = new_watermark;
debug!(
watermark = %new_watermark,
partition = partition,
"Punctuated watermark emission"
);
return Some(new_watermark);
}
}
None
}
fn on_periodic_check(&mut self) -> Option<Watermark> {
None }
fn current_watermark(&self) -> Watermark {
self.current_watermark
}
fn reset(&mut self) {
self.current_watermark = Watermark::min();
self.partition_watermarks.clear();
}
fn partition_watermark(&self, partition: u32) -> Option<Watermark> {
self.partition_watermarks.get(&partition).copied()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LateEventConfig {
pub allowed_lateness: Duration,
pub log_late_events: bool,
pub emit_metrics: bool,
}
impl Default for LateEventConfig {
fn default() -> Self {
Self {
allowed_lateness: Duration::from_secs(60),
log_late_events: true,
emit_metrics: true,
}
}
}
pub struct LateEventHandler {
config: LateEventConfig,
dropped_count: Arc<Mutex<u64>>,
accepted_count: Arc<Mutex<u64>>,
total_lateness: Arc<Mutex<i64>>,
}
impl LateEventHandler {
pub fn new(config: LateEventConfig) -> Self {
Self {
config,
dropped_count: Arc::new(Mutex::new(0)),
accepted_count: Arc::new(Mutex::new(0)),
total_lateness: Arc::new(Mutex::new(0)),
}
}
pub fn handle_late_event(&self, lateness: i64, partition: u32) -> bool {
let lateness_duration = Duration::from_millis(lateness as u64);
if lateness_duration > self.config.allowed_lateness {
*self.dropped_count.lock().unwrap() += 1;
if self.config.log_late_events {
warn!(
lateness_ms = lateness,
partition = partition,
"Dropping late event beyond allowed lateness"
);
}
false
} else {
*self.accepted_count.lock().unwrap() += 1;
*self.total_lateness.lock().unwrap() += lateness;
if self.config.log_late_events {
debug!(
lateness_ms = lateness,
partition = partition,
"Accepting late event within allowed lateness"
);
}
true
}
}
pub fn get_stats(&self) -> LateEventStats {
let dropped = *self.dropped_count.lock().unwrap();
let accepted = *self.accepted_count.lock().unwrap();
let total_lateness = *self.total_lateness.lock().unwrap();
let avg_lateness = if accepted > 0 {
total_lateness / accepted as i64
} else {
0
};
LateEventStats {
dropped_count: dropped,
accepted_count: accepted,
average_lateness_ms: avg_lateness,
}
}
pub fn reset_stats(&self) {
*self.dropped_count.lock().unwrap() = 0;
*self.accepted_count.lock().unwrap() = 0;
*self.total_lateness.lock().unwrap() = 0;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LateEventStats {
pub dropped_count: u64,
pub accepted_count: u64,
pub average_lateness_ms: i64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_watermark_creation() {
let wm = Watermark::new(1000);
assert_eq!(wm.timestamp, 1000);
assert!(wm.is_before(2000));
assert!(wm.is_after(500));
}
#[test]
fn test_watermark_ordering() {
let wm1 = Watermark::new(1000);
let wm2 = Watermark::new(2000);
assert!(wm1 < wm2);
assert!(wm2 > wm1);
}
#[test]
fn test_watermark_min_max() {
let min = Watermark::min();
let max = Watermark::max();
assert!(min.is_min());
assert!(max.is_max());
assert!(min < max);
}
#[test]
fn test_bounded_watermark_basic() {
let mut generator = BoundedOutOfOrdernessWatermark::new(
Duration::from_secs(5),
None,
);
generator.on_event(10000, 0);
generator.on_event(15000, 0);
generator.on_event(20000, 0);
let wm = generator.current_watermark();
assert_eq!(wm.timestamp, 15000);
}
#[test]
fn test_bounded_watermark_out_of_order() {
let mut generator = BoundedOutOfOrdernessWatermark::new(
Duration::from_secs(10),
None,
);
generator.on_event(30000, 0);
generator.on_event(20000, 0);
generator.on_event(25000, 0);
let wm = generator.current_watermark();
assert_eq!(wm.timestamp, 20000);
}
#[test]
fn test_bounded_watermark_multiple_partitions() {
let mut generator = BoundedOutOfOrdernessWatermark::new(
Duration::from_secs(5),
None,
);
generator.on_event(10000, 0);
generator.on_event(20000, 1);
generator.on_event(15000, 2);
let wm = generator.current_watermark();
assert_eq!(wm.timestamp, 5000);
}
#[test]
fn test_bounded_watermark_late_event_detection() {
let mut generator = BoundedOutOfOrdernessWatermark::new(
Duration::from_secs(5),
None,
);
generator.on_event(20000, 0);
assert!(generator.is_late_event(10000));
assert!(!generator.is_late_event(16000));
}
#[test]
fn test_bounded_watermark_lateness_calculation() {
let mut generator = BoundedOutOfOrdernessWatermark::new(
Duration::from_secs(5),
None,
);
generator.on_event(20000, 0);
assert_eq!(generator.get_lateness(10000), 5000);
assert_eq!(generator.get_lateness(16000), 0);
}
#[test]
fn test_periodic_watermark() {
let config = PeriodicWatermarkConfig {
interval: Duration::from_millis(100),
max_out_of_orderness: Duration::from_secs(5),
};
let mut generator = PeriodicWatermark::new(config);
generator.on_event(10000, 0);
generator.on_event(20000, 0);
assert!(generator.on_event(30000, 0).is_none());
std::thread::sleep(Duration::from_millis(150));
let wm = generator.on_periodic_check();
assert!(wm.is_some());
}
#[test]
fn test_punctuated_watermark_every_n() {
let mut generator = PunctuatedWatermark::every_n_events(3);
assert!(generator.on_event(1000, 0).is_none());
assert!(generator.on_event(2000, 0).is_none());
let wm = generator.on_event(3000, 0);
assert!(wm.is_some());
assert_eq!(wm.unwrap().timestamp, 3000);
}
#[test]
fn test_punctuated_watermark_boundary() {
let mut generator = PunctuatedWatermark::on_timestamp_boundary(10000);
assert!(generator.on_event(5000, 0).is_none());
assert!(generator.on_event(8000, 0).is_none());
let wm = generator.on_event(15000, 0);
assert!(wm.is_some());
assert_eq!(wm.unwrap().timestamp, 10000);
let wm2 = generator.on_event(25000, 0);
assert!(wm2.is_some());
assert_eq!(wm2.unwrap().timestamp, 20000);
}
#[test]
fn test_late_event_handler_within_lateness() {
let config = LateEventConfig {
allowed_lateness: Duration::from_secs(10),
log_late_events: false,
emit_metrics: true,
};
let handler = LateEventHandler::new(config);
assert!(handler.handle_late_event(5000, 0));
let stats = handler.get_stats();
assert_eq!(stats.accepted_count, 1);
assert_eq!(stats.dropped_count, 0);
}
#[test]
fn test_late_event_handler_beyond_lateness() {
let config = LateEventConfig {
allowed_lateness: Duration::from_secs(10),
log_late_events: false,
emit_metrics: true,
};
let handler = LateEventHandler::new(config);
assert!(!handler.handle_late_event(15000, 0));
let stats = handler.get_stats();
assert_eq!(stats.accepted_count, 0);
assert_eq!(stats.dropped_count, 1);
}
#[test]
fn test_late_event_handler_stats() {
let config = LateEventConfig::default();
let handler = LateEventHandler::new(config);
handler.handle_late_event(5000, 0);
handler.handle_late_event(10000, 0);
handler.handle_late_event(80000, 0);
let stats = handler.get_stats();
assert_eq!(stats.accepted_count, 2);
assert_eq!(stats.dropped_count, 1);
assert_eq!(stats.average_lateness_ms, 7500);
}
#[test]
fn test_watermark_generator_reset() {
let mut generator = BoundedOutOfOrdernessWatermark::new(
Duration::from_secs(5),
None,
);
generator.on_event(20000, 0);
assert_ne!(generator.current_watermark(), Watermark::min());
generator.reset();
assert_eq!(generator.current_watermark(), Watermark::min());
}
#[test]
fn test_partition_watermark_tracking() {
let mut generator = BoundedOutOfOrdernessWatermark::new(
Duration::from_secs(5),
None,
);
generator.on_event(10000, 0);
generator.on_event(20000, 1);
let wm0 = generator.partition_watermark(0).unwrap();
let wm1 = generator.partition_watermark(1).unwrap();
assert_eq!(wm0.timestamp, 5000);
assert_eq!(wm1.timestamp, 15000);
}
#[test]
fn test_watermark_datetime_conversion() {
let dt = DateTime::from_timestamp(1000, 0).unwrap();
let wm = Watermark::from_datetime(dt);
let _dt2 = wm.to_datetime();
assert_eq!(wm.timestamp, 1000000); }
#[test]
fn test_idle_partition_detection() {
let mut generator = BoundedOutOfOrdernessWatermark::new(
Duration::from_secs(5),
Some(Duration::from_millis(100)),
);
generator.on_event(10000, 0);
generator.on_event(20000, 1);
assert_eq!(generator.idle_partitions().len(), 0);
std::thread::sleep(Duration::from_millis(150));
let idle = generator.idle_partitions();
assert_eq!(idle.len(), 2);
}
#[test]
fn test_watermark_monotonicity() {
let mut generator = BoundedOutOfOrdernessWatermark::new(
Duration::from_secs(5),
None,
);
generator.on_event(20000, 0);
let wm1 = generator.current_watermark();
generator.on_event(15000, 0);
let wm2 = generator.current_watermark();
assert!(wm2 >= wm1);
}
}