oxigdal_streaming/windowing/
session.rs1use super::window::{Window, WindowAssigner};
4use crate::core::stream::StreamElement;
5use crate::error::Result;
6use chrono::{DateTime, Duration, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct SessionWindowConfig {
13 pub gap: Duration,
15
16 pub max_duration: Option<Duration>,
18}
19
20impl SessionWindowConfig {
21 pub fn new(gap: Duration) -> Self {
23 Self {
24 gap,
25 max_duration: None,
26 }
27 }
28
29 pub fn with_max_duration(mut self, max_duration: Duration) -> Self {
31 self.max_duration = Some(max_duration);
32 self
33 }
34}
35
36#[derive(Debug)]
38pub struct SessionWindow {
39 config: SessionWindowConfig,
40 sessions: BTreeMap<DateTime<Utc>, Window>,
41}
42
43impl SessionWindow {
44 pub fn new(gap: Duration) -> Self {
46 Self {
47 config: SessionWindowConfig::new(gap),
48 sessions: BTreeMap::new(),
49 }
50 }
51
52 pub fn with_max_duration(gap: Duration, max_duration: Duration) -> Self {
54 Self {
55 config: SessionWindowConfig::new(gap).with_max_duration(max_duration),
56 sessions: BTreeMap::new(),
57 }
58 }
59
60 pub fn assign(&mut self, timestamp: DateTime<Utc>) -> Result<Window> {
62 let mut merged_window = None;
63 let mut windows_to_remove = Vec::new();
64
65 for (start, window) in &self.sessions {
66 if timestamp >= window.start && timestamp <= window.end {
67 merged_window = Some(window.clone());
68 windows_to_remove.push(*start);
69 } else if timestamp > window.end && timestamp - window.end < self.config.gap {
70 let new_end = timestamp + self.config.gap;
71 let mut new_window = Window::new(window.start, new_end)?;
72
73 if let Some(max_dur) = self.config.max_duration
74 && new_window.duration() > max_dur
75 {
76 new_window = Window::new(new_window.end - max_dur, new_window.end)?;
77 }
78
79 if let Some(existing) = merged_window {
80 merged_window = existing.merge(&new_window);
81 } else {
82 merged_window = Some(new_window);
83 }
84
85 windows_to_remove.push(*start);
86 } else if timestamp < window.start && window.start - timestamp < self.config.gap {
87 let new_start = timestamp;
93 let mut new_window = Window::new(new_start, window.end)?;
94
95 if let Some(max_dur) = self.config.max_duration
96 && new_window.duration() > max_dur
97 {
98 new_window = Window::new(new_window.end - max_dur, new_window.end)?;
99 }
100
101 if let Some(existing) = merged_window {
102 merged_window = existing.merge(&new_window);
103 } else {
104 merged_window = Some(new_window);
105 }
106
107 windows_to_remove.push(*start);
108 }
109 }
110
111 for start in windows_to_remove {
112 self.sessions.remove(&start);
113 }
114
115 let result_window = if let Some(window) = merged_window {
116 window
117 } else {
118 Window::new(timestamp, timestamp + self.config.gap)?
119 };
120
121 self.sessions
122 .insert(result_window.start, result_window.clone());
123
124 Ok(result_window)
125 }
126
127 pub fn active_sessions(&self) -> Vec<Window> {
129 self.sessions.values().cloned().collect()
130 }
131
132 pub fn clear_expired(&mut self, watermark: DateTime<Utc>) {
134 self.sessions.retain(|_, window| window.end > watermark);
135 }
136}
137
138pub struct SessionAssigner {
140 config: SessionWindowConfig,
141}
142
143impl SessionAssigner {
144 pub fn new(gap: Duration) -> Self {
146 Self {
147 config: SessionWindowConfig::new(gap),
148 }
149 }
150
151 pub fn with_max_duration(gap: Duration, max_duration: Duration) -> Self {
153 Self {
154 config: SessionWindowConfig::new(gap).with_max_duration(max_duration),
155 }
156 }
157}
158
159impl WindowAssigner for SessionAssigner {
160 fn assign_windows(&self, element: &StreamElement) -> Result<Vec<Window>> {
161 let start = element.event_time;
162 let end = start + self.config.gap;
163 Ok(vec![Window::new(start, end)?])
164 }
165
166 fn assigner_type(&self) -> &str {
167 "SessionAssigner"
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn test_session_window() {
177 let mut window = SessionWindow::new(Duration::seconds(60));
178 let ts1 =
179 DateTime::from_timestamp(1000, 0).expect("Test timestamp creation should succeed");
180
181 let w1 = window
182 .assign(ts1)
183 .expect("Session window assignment should succeed in test");
184 assert!(w1.contains(&ts1));
185 assert_eq!(w1.duration(), Duration::seconds(60));
186 }
187
188 #[test]
189 fn test_session_window_merge() {
190 let mut window = SessionWindow::new(Duration::seconds(60));
191
192 let ts1 =
193 DateTime::from_timestamp(1000, 0).expect("Test timestamp creation should succeed");
194 let ts2 = ts1 + Duration::seconds(30);
195
196 let _w1 = window
197 .assign(ts1)
198 .expect("Session window assignment should succeed in test");
199 let w2 = window
200 .assign(ts2)
201 .expect("Session window assignment should succeed in test");
202
203 assert!(w2.contains(&ts1));
204 assert!(w2.contains(&ts2));
205 }
206
207 #[test]
208 fn test_session_window_separate() {
209 let mut window = SessionWindow::new(Duration::seconds(60));
210
211 let ts1 =
212 DateTime::from_timestamp(1000, 0).expect("Test timestamp creation should succeed");
213 let ts2 = ts1 + Duration::seconds(120);
214
215 let w1 = window
216 .assign(ts1)
217 .expect("Session window assignment should succeed in test");
218 let w2 = window
219 .assign(ts2)
220 .expect("Session window assignment should succeed in test");
221
222 assert!(!w1.contains(&ts2));
223 assert!(!w2.contains(&ts1));
224 }
225
226 #[test]
227 fn test_session_window_late_arrival() {
228 let mut window = SessionWindow::new(Duration::seconds(60));
231
232 let ts1 =
233 DateTime::from_timestamp(1000, 0).expect("Test timestamp creation should succeed");
234 window
236 .assign(ts1)
237 .expect("Session window assignment should succeed in test");
238
239 let ts_late = ts1 - Duration::seconds(10);
241 let merged = window
242 .assign(ts_late)
243 .expect("Session window assignment should succeed in test");
244
245 assert!(
247 merged.contains(&ts_late),
248 "merged session must contain the late event"
249 );
250 assert!(
251 merged.contains(&ts1),
252 "merged session must contain the original event"
253 );
254
255 assert_eq!(
257 window.active_sessions().len(),
258 1,
259 "late arrival within gap must not fragment into a new session"
260 );
261
262 let sessions = window.active_sessions();
264 assert_eq!(sessions[0].start, ts_late);
265 assert_eq!(sessions[0].end, ts1 + Duration::seconds(60));
266 }
267
268 #[test]
269 fn test_session_window_late_arrival_beyond_gap() {
270 let mut window = SessionWindow::new(Duration::seconds(60));
273
274 let ts1 =
275 DateTime::from_timestamp(1000, 0).expect("Test timestamp creation should succeed");
276 window
277 .assign(ts1)
278 .expect("Session window assignment should succeed in test");
279
280 let ts_far = ts1 - Duration::seconds(120);
281 let w = window
282 .assign(ts_far)
283 .expect("Session window assignment should succeed in test");
284
285 assert!(!w.contains(&ts1));
286 assert_eq!(window.active_sessions().len(), 2);
287 }
288
289 #[test]
290 fn test_session_window_max_duration() {
291 let mut window =
292 SessionWindow::with_max_duration(Duration::seconds(10), Duration::seconds(100));
293
294 let ts1 =
295 DateTime::from_timestamp(1000, 0).expect("Test timestamp creation should succeed");
296 window
297 .assign(ts1)
298 .expect("Session window assignment should succeed in test");
299
300 let ts2 = ts1 + Duration::seconds(200);
301 let w = window
302 .assign(ts2)
303 .expect("Session window assignment should succeed in test");
304
305 assert!(w.duration() <= Duration::seconds(100));
306 }
307
308 #[test]
309 fn test_session_assigner() {
310 let assigner = SessionAssigner::new(Duration::seconds(60));
311 let elem = StreamElement::new(
312 vec![1, 2, 3],
313 DateTime::from_timestamp(1000, 0).expect("Test timestamp creation should succeed"),
314 );
315
316 let windows = assigner
317 .assign_windows(&elem)
318 .expect("Session window assigner should succeed in test");
319 assert_eq!(windows.len(), 1);
320 assert!(windows[0].contains(&elem.event_time));
321 }
322
323 #[test]
324 fn test_clear_expired() {
325 let mut window = SessionWindow::new(Duration::seconds(60));
326
327 let ts1 =
328 DateTime::from_timestamp(1000, 0).expect("Test timestamp creation should succeed");
329 let ts2 = ts1 + Duration::seconds(200);
330
331 window
332 .assign(ts1)
333 .expect("Session window assignment should succeed in test");
334 window
335 .assign(ts2)
336 .expect("Session window assignment should succeed in test");
337
338 assert_eq!(window.active_sessions().len(), 2);
339
340 let watermark = ts1 + Duration::seconds(100);
341 window.clear_expired(watermark);
342
343 assert_eq!(window.active_sessions().len(), 1);
344 }
345}