use super::{Window, WindowAssigner, WindowTrigger, TriggerContext, TriggerResult};
use crate::error::{ProcessorError, WindowError};
use chrono::{DateTime, Utc};
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
use std::sync::Arc;
use tracing::{debug, trace, warn};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct WindowStats {
pub event_count: u64,
pub created_at: DateTime<Utc>,
pub last_update: DateTime<Utc>,
pub closed_at: Option<DateTime<Utc>>,
pub min_event_time: Option<DateTime<Utc>>,
pub max_event_time: Option<DateTime<Utc>>,
}
impl WindowStats {
pub fn new() -> Self {
let now = Utc::now();
Self {
event_count: 0,
created_at: now,
last_update: now,
closed_at: None,
min_event_time: None,
max_event_time: None,
}
}
pub fn record_event(&mut self, event_time: DateTime<Utc>) {
self.event_count += 1;
self.last_update = Utc::now();
self.min_event_time = Some(
self.min_event_time
.map(|t| t.min(event_time))
.unwrap_or(event_time)
);
self.max_event_time = Some(
self.max_event_time
.map(|t| t.max(event_time))
.unwrap_or(event_time)
);
}
pub fn close(&mut self) {
self.closed_at = Some(Utc::now());
}
pub fn is_closed(&self) -> bool {
self.closed_at.is_some()
}
}
#[derive(Debug, Clone)]
pub struct WindowMetadata {
pub window: Window,
pub stats: WindowStats,
pub triggered: bool,
}
impl WindowMetadata {
pub fn new(window: Window) -> Self {
Self {
window,
stats: WindowStats::new(),
triggered: false,
}
}
pub fn record_event(&mut self, event_time: DateTime<Utc>) {
self.stats.record_event(event_time);
}
pub fn check_trigger<T: WindowTrigger>(
&mut self,
trigger: &mut T,
watermark: Option<DateTime<Utc>>,
) -> TriggerResult {
if self.triggered {
return TriggerResult::Continue;
}
let ctx = TriggerContext {
window: self.window.clone(),
event_time: self.stats.max_event_time,
processing_time: Utc::now(),
watermark,
element_count: self.stats.event_count as usize,
};
let result = if let Some(wm) = watermark {
trigger.on_watermark(&ctx)
} else {
trigger.on_element(&ctx)
};
if result == TriggerResult::Fire || result == TriggerResult::FireAndPurge {
self.triggered = true;
}
result
}
pub fn close(&mut self) {
self.stats.close();
}
}
pub struct WindowManager<A, T>
where
A: WindowAssigner,
T: WindowTrigger,
{
assigner: A,
trigger: T,
active_windows: Arc<DashMap<String, WindowMetadata>>,
windows_by_end_time: BTreeMap<i64, Vec<String>>,
current_watermark: Option<DateTime<Utc>>,
allow_late_events: bool,
late_event_threshold: chrono::Duration,
}
impl<A, T> WindowManager<A, T>
where
A: WindowAssigner,
T: WindowTrigger,
{
pub fn new(assigner: A, trigger: T) -> Self {
Self {
assigner,
trigger,
active_windows: Arc::new(DashMap::new()),
windows_by_end_time: BTreeMap::new(),
current_watermark: None,
allow_late_events: true,
late_event_threshold: chrono::Duration::minutes(5),
}
}
pub fn with_late_events(
assigner: A,
trigger: T,
allow_late_events: bool,
late_event_threshold: chrono::Duration,
) -> Self {
Self {
assigner,
trigger,
active_windows: Arc::new(DashMap::new()),
windows_by_end_time: BTreeMap::new(),
current_watermark: None,
allow_late_events,
late_event_threshold,
}
}
pub fn assign_event(
&mut self,
event_time: DateTime<Utc>,
) -> Result<Vec<Window>, ProcessorError> {
if let Some(watermark) = self.current_watermark {
if event_time < watermark {
let late_by = watermark - event_time;
if !self.allow_late_events {
return Err(WindowError::LateEvent {
event_time: event_time.timestamp_millis(),
watermark: watermark.timestamp_millis(),
late_by: late_by.num_milliseconds(),
}.into());
}
if late_by > self.late_event_threshold {
warn!(
event_time = %event_time,
watermark = %watermark,
late_by_ms = late_by.num_milliseconds(),
"Event is too late, dropping"
);
return Ok(Vec::new());
}
debug!(
event_time = %event_time,
watermark = %watermark,
late_by_ms = late_by.num_milliseconds(),
"Processing late event"
);
}
}
let windows = self.assigner.assign_windows(event_time);
trace!(
event_time = %event_time,
window_count = windows.len(),
"Assigned event to windows"
);
for window in &windows {
let mut metadata = self.active_windows
.entry(window.id.clone())
.or_insert_with(|| {
let end_millis = window.bounds.end.timestamp_millis();
self.windows_by_end_time
.entry(end_millis)
.or_insert_with(Vec::new)
.push(window.id.clone());
debug!(
window_id = %window.id,
window_start = %window.bounds.start,
window_end = %window.bounds.end,
"Created new window"
);
WindowMetadata::new(window.clone())
});
metadata.record_event(event_time);
}
Ok(windows)
}
pub fn advance_watermark(
&mut self,
watermark: DateTime<Utc>,
) -> Result<Vec<String>, ProcessorError> {
if let Some(current) = self.current_watermark {
if watermark < current {
warn!(
current = %current,
new_watermark = %watermark,
"Watermark went backwards, ignoring"
);
return Ok(Vec::new());
}
}
debug!(
old_watermark = ?self.current_watermark,
new_watermark = %watermark,
"Advancing watermark"
);
self.current_watermark = Some(watermark);
let mut ready_windows = Vec::new();
let watermark_millis = watermark.timestamp_millis();
let ended_windows: Vec<_> = self.windows_by_end_time
.range(..=watermark_millis)
.flat_map(|(_, window_ids)| window_ids.clone())
.collect();
for window_id in ended_windows {
if let Some(mut metadata) = self.active_windows.get_mut(&window_id) {
let result = metadata.check_trigger(&mut self.trigger, Some(watermark));
match result {
TriggerResult::Fire | TriggerResult::FireAndPurge => {
debug!(
window_id = %window_id,
trigger_result = ?result,
"Window ready to fire"
);
ready_windows.push(window_id.clone());
}
_ => {}
}
}
}
Ok(ready_windows)
}
pub fn get_window(&self, window_id: &str) -> Option<WindowMetadata> {
self.active_windows.get(window_id).map(|m| m.clone())
}
pub fn remove_window(&mut self, window_id: &str) -> Option<WindowMetadata> {
let metadata = self.active_windows.remove(window_id).map(|(_, m)| m)?;
let end_millis = metadata.window.bounds.end.timestamp_millis();
if let Some(window_ids) = self.windows_by_end_time.get_mut(&end_millis) {
window_ids.retain(|id| id != window_id);
if window_ids.is_empty() {
self.windows_by_end_time.remove(&end_millis);
}
}
debug!(
window_id = %window_id,
"Removed window"
);
Some(metadata)
}
pub fn active_window_ids(&self) -> Vec<String> {
self.active_windows.iter().map(|e| e.key().clone()).collect()
}
pub fn active_window_count(&self) -> usize {
self.active_windows.len()
}
pub fn current_watermark(&self) -> Option<DateTime<Utc>> {
self.current_watermark
}
pub fn cleanup_old_windows(
&mut self,
retention_duration: chrono::Duration,
) -> usize {
let cutoff = Utc::now() - retention_duration;
let mut removed_count = 0;
let to_remove: Vec<String> = self.active_windows
.iter()
.filter(|entry| {
let metadata = entry.value();
metadata.triggered && metadata.window.bounds.end < cutoff
})
.map(|entry| entry.key().clone())
.collect();
for window_id in to_remove {
if self.remove_window(&window_id).is_some() {
removed_count += 1;
}
}
if removed_count > 0 {
debug!(
removed_count,
cutoff = %cutoff,
"Cleaned up old windows"
);
}
removed_count
}
pub fn get_statistics(&self) -> HashMap<String, WindowStats> {
self.active_windows
.iter()
.map(|entry| (entry.key().clone(), entry.value().stats.clone()))
.collect()
}
pub fn reset(&mut self) {
self.active_windows.clear();
self.windows_by_end_time.clear();
self.current_watermark = None;
debug!("Window manager reset");
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::window::{TumblingWindowAssigner, SlidingWindowAssigner, OnWatermarkTrigger};
use chrono::{Duration, TimeZone};
fn create_timestamp(millis: i64) -> DateTime<Utc> {
Utc.timestamp_millis_opt(millis).unwrap()
}
#[test]
fn test_window_manager_basic() {
let assigner = TumblingWindowAssigner::new(Duration::milliseconds(1000));
let trigger = OnWatermarkTrigger::new();
let mut manager = WindowManager::new(assigner, trigger);
let windows = manager.assign_event(create_timestamp(500)).unwrap();
assert_eq!(windows.len(), 1);
assert_eq!(manager.active_window_count(), 1);
let window_id = &windows[0].id;
let metadata = manager.get_window(window_id).unwrap();
assert_eq!(metadata.stats.event_count, 1);
assert!(!metadata.triggered);
}
#[test]
fn test_window_manager_watermark_trigger() {
let assigner = TumblingWindowAssigner::new(Duration::milliseconds(1000));
let trigger = OnWatermarkTrigger::new();
let mut manager = WindowManager::new(assigner, trigger);
manager.assign_event(create_timestamp(100)).unwrap();
manager.assign_event(create_timestamp(500)).unwrap();
manager.assign_event(create_timestamp(900)).unwrap();
let ready = manager.advance_watermark(create_timestamp(1000)).unwrap();
assert_eq!(ready.len(), 1);
let window_id = &ready[0];
let metadata = manager.get_window(window_id).unwrap();
assert!(metadata.triggered);
assert_eq!(metadata.stats.event_count, 3);
}
#[test]
fn test_window_manager_multiple_windows() {
let assigner = TumblingWindowAssigner::new(Duration::milliseconds(1000));
let trigger = OnWatermarkTrigger::new();
let mut manager = WindowManager::new(assigner, trigger);
manager.assign_event(create_timestamp(500)).unwrap();
manager.assign_event(create_timestamp(1500)).unwrap();
manager.assign_event(create_timestamp(2500)).unwrap();
assert_eq!(manager.active_window_count(), 3);
let ready = manager.advance_watermark(create_timestamp(1000)).unwrap();
assert_eq!(ready.len(), 1);
let ready = manager.advance_watermark(create_timestamp(2000)).unwrap();
assert_eq!(ready.len(), 1);
}
#[test]
fn test_window_manager_sliding_windows() {
let assigner = SlidingWindowAssigner::new(
Duration::milliseconds(1000),
Duration::milliseconds(500),
);
let trigger = OnWatermarkTrigger::new();
let mut manager = WindowManager::new(assigner, trigger);
let windows = manager.assign_event(create_timestamp(700)).unwrap();
assert_eq!(windows.len(), 2);
assert_eq!(manager.active_window_count(), 2);
for window in windows {
let metadata = manager.get_window(&window.id).unwrap();
assert_eq!(metadata.stats.event_count, 1);
}
}
#[test]
fn test_window_manager_late_events() {
let assigner = TumblingWindowAssigner::new(Duration::milliseconds(1000));
let trigger = OnWatermarkTrigger::new();
let mut manager = WindowManager::new(assigner, trigger);
manager.advance_watermark(create_timestamp(1000)).unwrap();
let windows = manager.assign_event(create_timestamp(900)).unwrap();
assert_eq!(windows.len(), 1);
manager.advance_watermark(create_timestamp(10000)).unwrap();
let windows = manager.assign_event(create_timestamp(500)).unwrap();
assert_eq!(windows.len(), 0); }
#[test]
fn test_window_manager_late_events_disallowed() {
let assigner = TumblingWindowAssigner::new(Duration::milliseconds(1000));
let trigger = OnWatermarkTrigger::new();
let mut manager = WindowManager::with_late_events(
assigner,
trigger,
false, Duration::minutes(5),
);
manager.advance_watermark(create_timestamp(1000)).unwrap();
let result = manager.assign_event(create_timestamp(900));
assert!(result.is_err());
}
#[test]
fn test_window_manager_cleanup() {
let assigner = TumblingWindowAssigner::new(Duration::milliseconds(1000));
let trigger = OnWatermarkTrigger::new();
let mut manager = WindowManager::new(assigner, trigger);
manager.assign_event(create_timestamp(500)).unwrap();
manager.assign_event(create_timestamp(1500)).unwrap();
manager.advance_watermark(create_timestamp(1000)).unwrap();
manager.advance_watermark(create_timestamp(2000)).unwrap();
assert_eq!(manager.active_window_count(), 2);
let removed = manager.cleanup_old_windows(Duration::milliseconds(0));
assert_eq!(removed, 2);
assert_eq!(manager.active_window_count(), 0);
}
#[test]
fn test_window_manager_remove_window() {
let assigner = TumblingWindowAssigner::new(Duration::milliseconds(1000));
let trigger = OnWatermarkTrigger::new();
let mut manager = WindowManager::new(assigner, trigger);
let windows = manager.assign_event(create_timestamp(500)).unwrap();
let window_id = &windows[0].id;
assert_eq!(manager.active_window_count(), 1);
let removed = manager.remove_window(window_id);
assert!(removed.is_some());
assert_eq!(manager.active_window_count(), 0);
let removed = manager.remove_window(window_id);
assert!(removed.is_none());
}
#[test]
fn test_window_stats() {
let mut stats = WindowStats::new();
assert_eq!(stats.event_count, 0);
assert!(stats.min_event_time.is_none());
assert!(stats.max_event_time.is_none());
assert!(!stats.is_closed());
stats.record_event(create_timestamp(500));
stats.record_event(create_timestamp(1000));
stats.record_event(create_timestamp(300));
assert_eq!(stats.event_count, 3);
assert_eq!(stats.min_event_time, Some(create_timestamp(300)));
assert_eq!(stats.max_event_time, Some(create_timestamp(1000)));
stats.close();
assert!(stats.is_closed());
}
#[test]
fn test_window_manager_statistics() {
let assigner = TumblingWindowAssigner::new(Duration::milliseconds(1000));
let trigger = OnWatermarkTrigger::new();
let mut manager = WindowManager::new(assigner, trigger);
manager.assign_event(create_timestamp(100)).unwrap();
manager.assign_event(create_timestamp(200)).unwrap();
manager.assign_event(create_timestamp(1500)).unwrap();
let stats = manager.get_statistics();
assert_eq!(stats.len(), 2);
let first_window_stats = stats.values()
.find(|s| s.event_count == 2)
.unwrap();
assert_eq!(first_window_stats.min_event_time, Some(create_timestamp(100)));
assert_eq!(first_window_stats.max_event_time, Some(create_timestamp(200)));
}
#[test]
fn test_window_manager_reset() {
let assigner = TumblingWindowAssigner::new(Duration::milliseconds(1000));
let trigger = OnWatermarkTrigger::new();
let mut manager = WindowManager::new(assigner, trigger);
manager.assign_event(create_timestamp(500)).unwrap();
manager.advance_watermark(create_timestamp(1000)).unwrap();
assert_eq!(manager.active_window_count(), 1);
assert!(manager.current_watermark().is_some());
manager.reset();
assert_eq!(manager.active_window_count(), 0);
assert!(manager.current_watermark().is_none());
}
}