Skip to main content

rust_rule_engine/streaming/
window.rs

1//! Time Window Management for Stream Processing
2//!
3//! Provides time-based windows for event aggregation and analysis.
4
5use crate::streaming::event::StreamEvent;
6use std::collections::VecDeque;
7use std::time::Duration;
8
9/// Type of time window
10#[derive(Debug, Clone, PartialEq)]
11pub enum WindowType {
12    /// Sliding window - continuously moves forward
13    Sliding,
14    /// Tumbling window - non-overlapping fixed intervals
15    Tumbling,
16    /// Session window - based on inactivity gaps
17    Session { timeout: Duration },
18}
19
20/// Time-based window for event processing
21#[derive(Debug)]
22pub struct TimeWindow {
23    /// Window type
24    pub window_type: WindowType,
25    /// Window duration
26    pub duration: Duration,
27    /// Events in this window
28    events: VecDeque<StreamEvent>,
29    /// Window start time (milliseconds since epoch)
30    pub start_time: u64,
31    /// Window end time (milliseconds since epoch)
32    pub end_time: u64,
33    /// Maximum number of events to retain
34    max_events: usize,
35}
36
37impl TimeWindow {
38    /// Create a new time window
39    pub fn new(
40        window_type: WindowType,
41        duration: Duration,
42        start_time: u64,
43        max_events: usize,
44    ) -> Self {
45        let end_time = start_time + duration.as_millis() as u64;
46
47        Self {
48            window_type,
49            duration,
50            events: VecDeque::new(),
51            start_time,
52            end_time,
53            max_events,
54        }
55    }
56
57    /// Add event to window if it fits
58    pub fn add_event(&mut self, event: StreamEvent) -> bool {
59        if self.contains_timestamp(event.metadata.timestamp) {
60            self.events.push_back(event);
61
62            // Keep window size under limit
63            while self.events.len() > self.max_events {
64                self.events.pop_front();
65            }
66
67            true
68        } else {
69            false
70        }
71    }
72
73    /// Records an event into a *continuously* sliding window: advances the
74    /// trailing boundary to the event's own timestamp and evicts anything
75    /// older than `duration`, instead of rejecting events the way
76    /// [`add_event`](Self::add_event) does once `[start_time, end_time)` is
77    /// stale.
78    ///
79    /// `add_event` and [`WindowManager`] are built around a *history* of
80    /// many fixed, non-advancing windows (useful for looking back at past
81    /// windows later). `record` is for the opposite case: maintaining a
82    /// single live value — "how many X in the trailing N seconds, as of
83    /// right now" — which is what most rate/threshold rules actually need.
84    ///
85    /// No-op on the boundary advance for non-`Sliding` window types (the
86    /// event is still recorded); tumbling/session semantics don't have a
87    /// meaningful "trailing" window to advance.
88    pub fn record(&mut self, event: StreamEvent) {
89        if self.window_type == WindowType::Sliding {
90            let now = event.metadata.timestamp;
91            self.start_time = now.saturating_sub(self.duration.as_millis() as u64);
92            self.end_time = now + 1; // inclusive of `now` itself
93        }
94
95        self.events.push_back(event);
96
97        while self
98            .events
99            .front()
100            .is_some_and(|e| e.metadata.timestamp < self.start_time)
101        {
102            self.events.pop_front();
103        }
104        while self.events.len() > self.max_events {
105            self.events.pop_front();
106        }
107    }
108
109    /// Check if timestamp falls within this window
110    pub fn contains_timestamp(&self, timestamp: u64) -> bool {
111        timestamp >= self.start_time && timestamp < self.end_time
112    }
113
114    /// Get all events in window
115    pub fn events(&self) -> &VecDeque<StreamEvent> {
116        &self.events
117    }
118
119    /// Get event count
120    pub fn count(&self) -> usize {
121        self.events.len()
122    }
123
124    /// Check if window is expired
125    pub fn is_expired(&self, current_time: u64) -> bool {
126        current_time >= self.end_time
127    }
128
129    /// Get window duration in milliseconds
130    pub fn duration_ms(&self) -> u64 {
131        self.duration.as_millis() as u64
132    }
133
134    /// Clear all events from window
135    pub fn clear(&mut self) {
136        self.events.clear();
137    }
138
139    /// Get events filtered by type
140    pub fn events_by_type(&self, event_type: &str) -> Vec<&StreamEvent> {
141        self.events
142            .iter()
143            .filter(|e| e.event_type == event_type)
144            .collect()
145    }
146
147    /// Calculate sum of numeric field across events
148    pub fn sum(&self, field: &str) -> f64 {
149        self.events
150            .iter()
151            .filter_map(|e| e.get_numeric(field))
152            .sum()
153    }
154
155    /// Calculate average of numeric field across events
156    pub fn average(&self, field: &str) -> Option<f64> {
157        let values: Vec<f64> = self
158            .events
159            .iter()
160            .filter_map(|e| e.get_numeric(field))
161            .collect();
162
163        if values.is_empty() {
164            None
165        } else {
166            Some(values.iter().sum::<f64>() / values.len() as f64)
167        }
168    }
169
170    /// Find minimum value of numeric field
171    pub fn min(&self, field: &str) -> Option<f64> {
172        self.events
173            .iter()
174            .filter_map(|e| e.get_numeric(field))
175            .fold(None, |acc, x| match acc {
176                None => Some(x),
177                Some(min) => Some(min.min(x)),
178            })
179    }
180
181    /// Find maximum value of numeric field
182    pub fn max(&self, field: &str) -> Option<f64> {
183        self.events
184            .iter()
185            .filter_map(|e| e.get_numeric(field))
186            .fold(None, |acc, x| match acc {
187                None => Some(x),
188                Some(max) => Some(max.max(x)),
189            })
190    }
191
192    /// Get the latest event timestamp
193    pub fn latest_timestamp(&self) -> Option<u64> {
194        self.events.iter().map(|e| e.metadata.timestamp).max()
195    }
196
197    /// Get events within a sub-window
198    pub fn events_in_range(&self, start: u64, end: u64) -> Vec<&StreamEvent> {
199        self.events
200            .iter()
201            .filter(|e| e.metadata.timestamp >= start && e.metadata.timestamp < end)
202            .collect()
203    }
204}
205
206/// Manages multiple time windows for stream processing
207#[derive(Debug)]
208pub struct WindowManager {
209    /// Active windows
210    windows: Vec<TimeWindow>,
211    /// Window configuration
212    window_type: WindowType,
213    /// Window duration
214    duration: Duration,
215    /// Maximum events per window
216    max_events_per_window: usize,
217    /// Maximum number of windows to keep
218    max_windows: usize,
219}
220
221impl WindowManager {
222    /// Create a new window manager
223    pub fn new(
224        window_type: WindowType,
225        duration: Duration,
226        max_events_per_window: usize,
227        max_windows: usize,
228    ) -> Self {
229        Self {
230            windows: Vec::new(),
231            window_type,
232            duration,
233            max_events_per_window,
234            max_windows,
235        }
236    }
237
238    /// Process a new event through the window system
239    pub fn process_event(&mut self, event: StreamEvent) {
240        let event_time = event.metadata.timestamp;
241
242        // Find or create appropriate window
243        let mut added = false;
244
245        for window in &mut self.windows {
246            if window.add_event(event.clone()) {
247                added = true;
248                break;
249            }
250        }
251
252        if !added {
253            // Create new window for this event
254            let window_start = self.calculate_window_start(event_time);
255            let mut new_window = TimeWindow::new(
256                self.window_type.clone(),
257                self.duration,
258                window_start,
259                self.max_events_per_window,
260            );
261
262            new_window.add_event(event);
263            self.windows.push(new_window);
264        }
265
266        // Clean up expired windows
267        self.cleanup_expired_windows(event_time);
268
269        // Limit total number of windows
270        while self.windows.len() > self.max_windows {
271            self.windows.remove(0);
272        }
273
274        // Sort windows by start time
275        self.windows.sort_by_key(|w| w.start_time);
276    }
277
278    /// Calculate window start time based on window type
279    fn calculate_window_start(&self, event_time: u64) -> u64 {
280        match self.window_type {
281            WindowType::Tumbling => {
282                let window_ms = self.duration.as_millis() as u64;
283                (event_time / window_ms) * window_ms
284            }
285            WindowType::Sliding | WindowType::Session { .. } => event_time,
286        }
287    }
288
289    /// Remove expired windows
290    fn cleanup_expired_windows(&mut self, current_time: u64) {
291        self.windows
292            .retain(|window| !window.is_expired(current_time));
293    }
294
295    /// Get all active windows
296    pub fn active_windows(&self) -> &[TimeWindow] {
297        &self.windows
298    }
299
300    /// Get the latest window
301    pub fn latest_window(&self) -> Option<&TimeWindow> {
302        self.windows.last()
303    }
304
305    /// Get total event count across all windows
306    pub fn total_event_count(&self) -> usize {
307        self.windows.iter().map(|w| w.count()).sum()
308    }
309
310    /// Get windows that contain events of a specific type
311    pub fn windows_with_event_type(&self, event_type: &str) -> Vec<&TimeWindow> {
312        self.windows
313            .iter()
314            .filter(|w| w.events().iter().any(|e| e.event_type == event_type))
315            .collect()
316    }
317
318    /// Calculate aggregate across all windows
319    pub fn aggregate_across_windows<F>(&self, aggregator: F) -> f64
320    where
321        F: Fn(&TimeWindow) -> f64,
322    {
323        self.windows.iter().map(aggregator).sum()
324    }
325
326    /// Get window statistics
327    pub fn get_statistics(&self) -> WindowStatistics {
328        WindowStatistics {
329            total_windows: self.windows.len(),
330            total_events: self.total_event_count(),
331            oldest_window_start: self.windows.first().map(|w| w.start_time),
332            newest_window_start: self.windows.last().map(|w| w.start_time),
333            average_events_per_window: if self.windows.is_empty() {
334                0.0
335            } else {
336                self.total_event_count() as f64 / self.windows.len() as f64
337            },
338        }
339    }
340}
341
342/// Statistics about window manager state
343#[derive(Debug, Clone)]
344pub struct WindowStatistics {
345    /// Total number of active windows
346    pub total_windows: usize,
347    /// Total events across all windows
348    pub total_events: usize,
349    /// Start time of oldest window
350    pub oldest_window_start: Option<u64>,
351    /// Start time of newest window
352    pub newest_window_start: Option<u64>,
353    /// Average events per window
354    pub average_events_per_window: f64,
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360    use crate::types::Value;
361    use std::collections::HashMap;
362
363    #[test]
364    fn test_time_window_creation() {
365        let window = TimeWindow::new(WindowType::Sliding, Duration::from_secs(60), 1000, 100);
366
367        assert_eq!(window.start_time, 1000);
368        assert_eq!(window.end_time, 61000);
369        assert_eq!(window.count(), 0);
370    }
371
372    #[test]
373    fn test_window_event_addition() {
374        let mut window = TimeWindow::new(WindowType::Sliding, Duration::from_secs(60), 1000, 100);
375
376        let mut data = HashMap::new();
377        data.insert("value".to_string(), Value::Number(10.0));
378
379        let event = StreamEvent::with_timestamp("TestEvent", data, "test", 30000);
380
381        assert!(window.add_event(event));
382        assert_eq!(window.count(), 1);
383    }
384
385    #[test]
386    fn test_record_slides_boundary_forward() {
387        // duration=60s, first event at t=1000 → window covers [0, 1001).
388        let mut window = TimeWindow::new(WindowType::Sliding, Duration::from_secs(60), 1000, 100);
389
390        window.record(StreamEvent::with_timestamp(
391            "Hit",
392            HashMap::new(),
393            "test",
394            1000,
395        ));
396        assert_eq!(window.count(), 1);
397
398        // t=310000 is way past the original end_time (61000) — `add_event`
399        // would reject this outright; `record` instead advances the window
400        // to trail 60s behind the new event and evicts the now-stale one.
401        window.record(StreamEvent::with_timestamp(
402            "Hit",
403            HashMap::new(),
404            "test",
405            310_000,
406        ));
407
408        assert_eq!(
409            window.count(),
410            1,
411            "event older than the trailing duration must be evicted"
412        );
413        assert_eq!(window.start_time, 250_000); // 310000 - 60000
414        assert_eq!(window.end_time, 310_001);
415    }
416
417    #[test]
418    fn test_record_keeps_events_still_within_duration() {
419        let mut window = TimeWindow::new(WindowType::Sliding, Duration::from_secs(60), 0, 100);
420
421        window.record(StreamEvent::with_timestamp(
422            "Hit",
423            HashMap::new(),
424            "test",
425            1_000,
426        ));
427        window.record(StreamEvent::with_timestamp(
428            "Hit",
429            HashMap::new(),
430            "test",
431            30_000,
432        ));
433        // 30_000 - 1_000 = 29s, still inside the 60s trailing window.
434        assert_eq!(window.count(), 2);
435    }
436
437    #[test]
438    fn test_window_aggregations() {
439        let mut window = TimeWindow::new(WindowType::Sliding, Duration::from_secs(60), 1000, 100);
440
441        // Add test events
442        for i in 0..5 {
443            let mut data = HashMap::new();
444            data.insert("value".to_string(), Value::Number(i as f64));
445
446            let event = StreamEvent::with_timestamp("TestEvent", data, "test", 30000 + i);
447            window.add_event(event);
448        }
449
450        assert_eq!(window.sum("value"), 10.0); // 0+1+2+3+4
451        assert_eq!(window.average("value"), Some(2.0));
452        assert_eq!(window.min("value"), Some(0.0));
453        assert_eq!(window.max("value"), Some(4.0));
454    }
455
456    #[test]
457    fn test_window_manager() {
458        let mut manager = WindowManager::new(WindowType::Sliding, Duration::from_secs(60), 100, 10);
459
460        let mut data = HashMap::new();
461        data.insert("value".to_string(), Value::Number(1.0));
462
463        let event = StreamEvent::with_timestamp("TestEvent", data, "test", 30000);
464        manager.process_event(event);
465
466        assert_eq!(manager.active_windows().len(), 1);
467        assert_eq!(manager.total_event_count(), 1);
468    }
469}