rust_rule_engine/streaming/
window.rs1use crate::streaming::event::StreamEvent;
6use std::collections::VecDeque;
7use std::time::Duration;
8
9#[derive(Debug, Clone, PartialEq)]
11pub enum WindowType {
12 Sliding,
14 Tumbling,
16 Session { timeout: Duration },
18}
19
20#[derive(Debug)]
22pub struct TimeWindow {
23 pub window_type: WindowType,
25 pub duration: Duration,
27 events: VecDeque<StreamEvent>,
29 pub start_time: u64,
31 pub end_time: u64,
33 max_events: usize,
35}
36
37impl TimeWindow {
38 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 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 while self.events.len() > self.max_events {
64 self.events.pop_front();
65 }
66
67 true
68 } else {
69 false
70 }
71 }
72
73 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; }
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 pub fn contains_timestamp(&self, timestamp: u64) -> bool {
111 timestamp >= self.start_time && timestamp < self.end_time
112 }
113
114 pub fn events(&self) -> &VecDeque<StreamEvent> {
116 &self.events
117 }
118
119 pub fn count(&self) -> usize {
121 self.events.len()
122 }
123
124 pub fn is_expired(&self, current_time: u64) -> bool {
126 current_time >= self.end_time
127 }
128
129 pub fn duration_ms(&self) -> u64 {
131 self.duration.as_millis() as u64
132 }
133
134 pub fn clear(&mut self) {
136 self.events.clear();
137 }
138
139 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 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 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 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 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 pub fn latest_timestamp(&self) -> Option<u64> {
194 self.events.iter().map(|e| e.metadata.timestamp).max()
195 }
196
197 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#[derive(Debug)]
208pub struct WindowManager {
209 windows: Vec<TimeWindow>,
211 window_type: WindowType,
213 duration: Duration,
215 max_events_per_window: usize,
217 max_windows: usize,
219}
220
221impl WindowManager {
222 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 pub fn process_event(&mut self, event: StreamEvent) {
240 let event_time = event.metadata.timestamp;
241
242 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 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 self.cleanup_expired_windows(event_time);
268
269 while self.windows.len() > self.max_windows {
271 self.windows.remove(0);
272 }
273
274 self.windows.sort_by_key(|w| w.start_time);
276 }
277
278 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 fn cleanup_expired_windows(&mut self, current_time: u64) {
291 self.windows
292 .retain(|window| !window.is_expired(current_time));
293 }
294
295 pub fn active_windows(&self) -> &[TimeWindow] {
297 &self.windows
298 }
299
300 pub fn latest_window(&self) -> Option<&TimeWindow> {
302 self.windows.last()
303 }
304
305 pub fn total_event_count(&self) -> usize {
307 self.windows.iter().map(|w| w.count()).sum()
308 }
309
310 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 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 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#[derive(Debug, Clone)]
344pub struct WindowStatistics {
345 pub total_windows: usize,
347 pub total_events: usize,
349 pub oldest_window_start: Option<u64>,
351 pub newest_window_start: Option<u64>,
353 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 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 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); 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 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 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); 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}