Skip to main content

llm_optimizer_processor/window/
mod.rs

1//! Windowing module for stream processing
2//!
3//! This module provides comprehensive windowing functionality for time-based
4//! aggregations and event processing. It supports three main types of windows:
5//!
6//! # Window Types
7//!
8//! ## Tumbling Windows
9//! Fixed-size, non-overlapping windows. Each event belongs to exactly one window.
10//!
11//! ```text
12//! Time:     0----5----10---15---20---25---30
13//! Windows:  [----][----][----][----][----]
14//! ```
15//!
16//! **Use cases:**
17//! - Periodic aggregations (e.g., metrics every 5 minutes)
18//! - Non-overlapping batch processing
19//! - Fixed-interval reporting
20//!
21//! ## Sliding Windows
22//! Fixed-size, overlapping windows. Events can belong to multiple windows.
23//!
24//! ```text
25//! Time:     0----5----10---15---20---25---30
26//! Windows:  [----------]
27//!                [----------]
28//!                     [----------]
29//! ```
30//!
31//! **Use cases:**
32//! - Moving averages
33//! - Smoothed metrics
34//! - Trend detection
35//! - Rate calculations over time
36//!
37//! ## Session Windows
38//! Variable-size windows based on inactivity gaps. Events are grouped into
39//! sessions separated by periods of inactivity.
40//!
41//! ```text
42//! Time:     0-2--5----------12-14----20
43//! Sessions: [----]           [---]   [-]
44//! Gap:           ^^^^^^^^^^^^     ^^^^
45//! ```
46//!
47//! **Use cases:**
48//! - User session analysis
49//! - Burst detection
50//! - Activity clustering
51//! - Pattern recognition in irregular events
52//!
53//! # Window Assignment
54//!
55//! Windows are assigned using the [`WindowAssigner`] trait, which determines
56//! which window(s) an event belongs to based on its timestamp.
57//!
58//! # Window Triggers
59//!
60//! Triggers determine when a window should be evaluated and emit results.
61//! Multiple trigger types are supported:
62//!
63//! - **OnWatermarkTrigger**: Fires when watermark passes window end (event-time)
64//! - **ProcessingTimeTrigger**: Fires based on processing time
65//! - **CountTrigger**: Fires after N elements
66//! - **CompositeTrigger**: Combines multiple triggers with AND/OR logic
67//! - **ImmediateTrigger**: Fires on every element
68//! - **NeverTrigger**: Never fires automatically
69//!
70//! # Example Usage
71//!
72//! ```rust
73//! use processor::window::{
74//!     assigner::{TumblingWindowAssigner, SlidingWindowAssigner, SessionWindowAssigner},
75//!     trigger::{OnWatermarkTrigger, ProcessingTimeTrigger, CountTrigger},
76//! };
77//! use chrono::Duration;
78//!
79//! // Tumbling window: 5-minute non-overlapping windows
80//! let tumbling = TumblingWindowAssigner::new(Duration::minutes(5));
81//!
82//! // Sliding window: 10-minute windows, sliding every 1 minute
83//! let sliding = SlidingWindowAssigner::new(
84//!     Duration::minutes(10),
85//!     Duration::minutes(1),
86//! );
87//!
88//! // Session window: 30-second inactivity gap
89//! let session = SessionWindowAssigner::new(Duration::seconds(30));
90//!
91//! // Triggers
92//! let watermark_trigger = OnWatermarkTrigger::new();
93//! let processing_trigger = ProcessingTimeTrigger::with_delay(Duration::seconds(5));
94//! let count_trigger = CountTrigger::new(100); // Fire after 100 elements
95//! ```
96//!
97//! # Window Merging
98//!
99//! Session windows support merging when events arrive within the session gap.
100//! The window module handles this automatically:
101//!
102//! ```text
103//! Initial state:    Window A [0, 5)    Window B [10, 15)
104//! Event at t=7:     Window A [0, 12)   (merged, extends to cover gap)
105//! Event at t=12:    Window A [0, 17)   (merged with B)
106//! ```
107
108pub mod types;
109pub mod assigner;
110pub mod trigger;
111pub mod manager;
112
113pub use types::{Window, WindowBounds, WindowType};
114pub use assigner::{
115    WindowAssigner,
116    TumblingWindowAssigner,
117    SlidingWindowAssigner,
118    SessionWindowAssigner,
119};
120pub use trigger::{
121    WindowTrigger,
122    TriggerResult,
123    TriggerContext,
124    OnWatermarkTrigger,
125    ProcessingTimeTrigger,
126    CountTrigger,
127    CompositeTrigger,
128    CompositeMode,
129    ImmediateTrigger,
130    NeverTrigger,
131};
132pub use manager::{WindowManager, WindowMetadata, WindowStats};
133
134use chrono::DateTime;
135use std::collections::HashMap;
136
137/// Window merger for session windows
138///
139/// Handles merging of session windows when events arrive within the session gap.
140#[derive(Debug)]
141pub struct WindowMerger {
142    /// Active windows indexed by their ID
143    windows: HashMap<String, Window>,
144    /// Session gap for merging
145    session_gap: chrono::Duration,
146}
147
148impl WindowMerger {
149    /// Create a new window merger with the specified session gap
150    pub fn new(session_gap: chrono::Duration) -> Self {
151        Self {
152            windows: HashMap::new(),
153            session_gap,
154        }
155    }
156
157    /// Add a window and merge with overlapping/nearby windows
158    ///
159    /// Returns the final merged window and a list of windows that were merged
160    pub fn add_and_merge(&mut self, window: Window) -> (Window, Vec<Window>) {
161        let mut merged_windows = Vec::new();
162        let mut result_window = window.clone();
163
164        // Find all windows that can be merged
165        let mut to_remove = Vec::new();
166        for (id, existing_window) in &self.windows {
167            if result_window.can_merge(existing_window, self.session_gap) {
168                result_window = result_window.merge(existing_window);
169                merged_windows.push(existing_window.clone());
170                to_remove.push(id.clone());
171            }
172        }
173
174        // Remove merged windows
175        for id in to_remove {
176            self.windows.remove(&id);
177        }
178
179        // Add the new merged window
180        self.windows.insert(result_window.id.clone(), result_window.clone());
181
182        (result_window, merged_windows)
183    }
184
185    /// Get all active windows
186    pub fn windows(&self) -> Vec<&Window> {
187        self.windows.values().collect()
188    }
189
190    /// Remove a window by ID
191    pub fn remove(&mut self, window_id: &str) -> Option<Window> {
192        self.windows.remove(window_id)
193    }
194
195    /// Remove windows older than the given watermark
196    pub fn remove_old_windows(&mut self, watermark: DateTime<chrono::Utc>) -> Vec<Window> {
197        let mut removed = Vec::new();
198        self.windows.retain(|_, window| {
199            if window.bounds.end <= watermark {
200                removed.push(window.clone());
201                false
202            } else {
203                true
204            }
205        });
206        removed
207    }
208
209    /// Clear all windows
210    pub fn clear(&mut self) {
211        self.windows.clear();
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use chrono::{Duration, TimeZone, Utc};
219
220    fn create_timestamp(millis: i64) -> DateTime<Utc> {
221        Utc.timestamp_millis_opt(millis).unwrap()
222    }
223
224    fn create_window(start_millis: i64, end_millis: i64) -> Window {
225        Window::new(WindowBounds::new(
226            create_timestamp(start_millis),
227            create_timestamp(end_millis),
228        ))
229    }
230
231    #[test]
232    fn test_window_merger_no_overlap() {
233        let mut merger = WindowMerger::new(Duration::milliseconds(1000));
234
235        // Add first window
236        let window1 = create_window(0, 1000);
237        let (result, merged) = merger.add_and_merge(window1.clone());
238        assert_eq!(result, window1);
239        assert_eq!(merged.len(), 0);
240
241        // Add non-overlapping window beyond gap
242        let window2 = create_window(3000, 4000);
243        let (result, merged) = merger.add_and_merge(window2.clone());
244        assert_eq!(result, window2);
245        assert_eq!(merged.len(), 0);
246
247        // Should have 2 separate windows
248        assert_eq!(merger.windows().len(), 2);
249    }
250
251    #[test]
252    fn test_window_merger_with_overlap() {
253        let mut merger = WindowMerger::new(Duration::milliseconds(1000));
254
255        // Add first window [0, 1000)
256        let window1 = create_window(0, 1000);
257        merger.add_and_merge(window1);
258
259        // Add overlapping window [500, 1500)
260        let window2 = create_window(500, 1500);
261        let (result, merged) = merger.add_and_merge(window2);
262
263        // Should merge into [0, 1500)
264        assert_eq!(result.bounds.start, create_timestamp(0));
265        assert_eq!(result.bounds.end, create_timestamp(1500));
266        assert_eq!(merged.len(), 1);
267
268        // Should have 1 merged window
269        assert_eq!(merger.windows().len(), 1);
270    }
271
272    #[test]
273    fn test_window_merger_within_gap() {
274        let mut merger = WindowMerger::new(Duration::milliseconds(1000));
275
276        // Add first window [0, 1000)
277        let window1 = create_window(0, 1000);
278        merger.add_and_merge(window1);
279
280        // Add window [1500, 2500) within gap
281        let window2 = create_window(1500, 2500);
282        let (result, merged) = merger.add_and_merge(window2);
283
284        // Should merge into [0, 2500)
285        assert_eq!(result.bounds.start, create_timestamp(0));
286        assert_eq!(result.bounds.end, create_timestamp(2500));
287        assert_eq!(merged.len(), 1);
288    }
289
290    #[test]
291    fn test_window_merger_multiple_merges() {
292        let mut merger = WindowMerger::new(Duration::milliseconds(500));
293
294        // Add multiple windows that won't initially merge (gap > 500ms)
295        merger.add_and_merge(create_window(0, 1000));
296        merger.add_and_merge(create_window(2000, 3000));
297        merger.add_and_merge(create_window(5000, 6000)); // Beyond gap from previous window
298
299        // Verify we have 3 separate windows
300        assert_eq!(merger.windows().len(), 3);
301
302        // Add window that bridges first two windows (fills the gap)
303        let window = create_window(1000, 2000);
304        let (result, merged) = merger.add_and_merge(window);
305
306        // Should merge [0, 1000) and [2000, 3000) via the bridge window
307        assert_eq!(result.bounds.start, create_timestamp(0));
308        assert_eq!(result.bounds.end, create_timestamp(3000));
309        assert_eq!(merged.len(), 2);
310
311        // Should have 2 windows now: merged one [0, 3000) and [5000, 6000)
312        assert_eq!(merger.windows().len(), 2);
313    }
314
315    #[test]
316    fn test_window_merger_remove_old() {
317        let mut merger = WindowMerger::new(Duration::milliseconds(500));
318
319        // Add windows that won't merge (gap > 500ms between them)
320        merger.add_and_merge(create_window(0, 1000));
321        merger.add_and_merge(create_window(2000, 3000));
322        merger.add_and_merge(create_window(4000, 5000));
323
324        // Remove windows older than 3000
325        let removed = merger.remove_old_windows(create_timestamp(3000));
326        assert_eq!(removed.len(), 2); // [0, 1000) and [2000, 3000)
327        assert_eq!(merger.windows().len(), 1); // Only [4000, 5000) remains
328    }
329
330    #[test]
331    fn test_tumbling_window_complete_flow() {
332        use assigner::TumblingWindowAssigner;
333        use trigger::OnWatermarkTrigger;
334
335        let assigner = TumblingWindowAssigner::new(Duration::milliseconds(1000));
336        let mut trigger = OnWatermarkTrigger::new();
337
338        // Assign event at 500ms to window
339        let windows = assigner.assign_windows(create_timestamp(500));
340        assert_eq!(windows.len(), 1);
341        assert_eq!(windows[0].bounds.start, create_timestamp(0));
342        assert_eq!(windows[0].bounds.end, create_timestamp(1000));
343
344        // Check trigger with watermark before window end
345        let ctx = TriggerContext {
346            window: windows[0].clone(),
347            event_time: Some(create_timestamp(500)),
348            processing_time: create_timestamp(1500),
349            watermark: Some(create_timestamp(900)),
350            element_count: 1,
351        };
352        assert_eq!(trigger.on_watermark(&ctx), TriggerResult::Continue);
353
354        // Check trigger with watermark past window end
355        let ctx = TriggerContext {
356            window: windows[0].clone(),
357            event_time: Some(create_timestamp(500)),
358            processing_time: create_timestamp(1500),
359            watermark: Some(create_timestamp(1000)),
360            element_count: 1,
361        };
362        assert_eq!(trigger.on_watermark(&ctx), TriggerResult::Fire);
363    }
364
365    #[test]
366    fn test_sliding_window_complete_flow() {
367        use assigner::SlidingWindowAssigner;
368
369        let assigner = SlidingWindowAssigner::new(
370            Duration::milliseconds(1000),
371            Duration::milliseconds(500),
372        );
373
374        // Event at 700ms should be in 2 windows
375        let windows = assigner.assign_windows(create_timestamp(700));
376        assert_eq!(windows.len(), 2);
377
378        // Verify windows
379        assert_eq!(windows[0].bounds.start, create_timestamp(0));
380        assert_eq!(windows[0].bounds.end, create_timestamp(1000));
381        assert_eq!(windows[1].bounds.start, create_timestamp(500));
382        assert_eq!(windows[1].bounds.end, create_timestamp(1500));
383    }
384
385    #[test]
386    fn test_session_window_complete_flow() {
387        use assigner::SessionWindowAssigner;
388
389        let assigner = SessionWindowAssigner::new(Duration::milliseconds(1000));
390        let mut merger = WindowMerger::new(Duration::milliseconds(1000));
391
392        // First event at 0ms -> window [0, 1000)
393        let windows = assigner.assign_windows(create_timestamp(0));
394        let (_window1, _) = merger.add_and_merge(windows[0].clone());
395
396        // Second event at 500ms -> window [500, 1500)
397        let windows = assigner.assign_windows(create_timestamp(500));
398        let (window2, merged) = merger.add_and_merge(windows[0].clone());
399
400        // Should merge with first window
401        assert_eq!(merged.len(), 1);
402        assert_eq!(window2.bounds.start, create_timestamp(0));
403        assert_eq!(window2.bounds.end, create_timestamp(1500));
404
405        // Third event at 3000ms -> window [3000, 4000)
406        let windows = assigner.assign_windows(create_timestamp(3000));
407        let (window3, merged) = merger.add_and_merge(windows[0].clone());
408
409        // Should not merge (gap too large)
410        assert_eq!(merged.len(), 0);
411        assert_eq!(window3.bounds.start, create_timestamp(3000));
412        assert_eq!(window3.bounds.end, create_timestamp(4000));
413    }
414}