Skip to main content

oxigdal_streaming/windowing/
session.rs

1//! Session window implementation.
2
3use 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/// Configuration for session windows.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct SessionWindowConfig {
13    /// Gap duration between sessions
14    pub gap: Duration,
15
16    /// Maximum session duration (optional)
17    pub max_duration: Option<Duration>,
18}
19
20impl SessionWindowConfig {
21    /// Create a new session window configuration.
22    pub fn new(gap: Duration) -> Self {
23        Self {
24            gap,
25            max_duration: None,
26        }
27    }
28
29    /// Set the maximum session duration.
30    pub fn with_max_duration(mut self, max_duration: Duration) -> Self {
31        self.max_duration = Some(max_duration);
32        self
33    }
34}
35
36/// Session window (dynamic windows based on activity).
37#[derive(Debug)]
38pub struct SessionWindow {
39    config: SessionWindowConfig,
40    sessions: BTreeMap<DateTime<Utc>, Window>,
41}
42
43impl SessionWindow {
44    /// Create a new session window.
45    pub fn new(gap: Duration) -> Self {
46        Self {
47            config: SessionWindowConfig::new(gap),
48            sessions: BTreeMap::new(),
49        }
50    }
51
52    /// Create a new session window with maximum duration.
53    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    /// Assign an element to a session window.
61    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                // Backward extension: a late / out-of-order event arrives with a
88                // timestamp earlier than an existing session's start, but within
89                // the inactivity gap. The session must be extended backward to
90                // begin at the new (earlier) event rather than fragmenting into
91                // a disjoint overlapping session.
92                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    /// Get all active sessions.
128    pub fn active_sessions(&self) -> Vec<Window> {
129        self.sessions.values().cloned().collect()
130    }
131
132    /// Clear expired sessions.
133    pub fn clear_expired(&mut self, watermark: DateTime<Utc>) {
134        self.sessions.retain(|_, window| window.end > watermark);
135    }
136}
137
138/// Assigner for session windows.
139pub struct SessionAssigner {
140    config: SessionWindowConfig,
141}
142
143impl SessionAssigner {
144    /// Create a new session window assigner.
145    pub fn new(gap: Duration) -> Self {
146        Self {
147            config: SessionWindowConfig::new(gap),
148        }
149    }
150
151    /// Create a new session window assigner with maximum duration.
152    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        // Out-of-order (late) event earlier than an existing session start, but
229        // within the gap, must merge backward into a single session.
230        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        // Establishes session [1000, 1060).
235        window
236            .assign(ts1)
237            .expect("Session window assignment should succeed in test");
238
239        // A late event 10s before the session start, within the 60s gap.
240        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        // The merged session must cover both the late event and the original.
246        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        // There must be exactly one session, not two disjoint/overlapping ones.
256        assert_eq!(
257            window.active_sessions().len(),
258            1,
259            "late arrival within gap must not fragment into a new session"
260        );
261
262        // The single session must start at the late event.
263        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        // A late event further back than the gap must NOT merge; it stays a
271        // separate session (symmetric to test_session_window_separate).
272        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}