1use std::time::{Duration, SystemTime};
15
16use crate::error::StreamingError;
17
18#[derive(Debug, Clone)]
23pub struct StreamEvent<T> {
24 pub timestamp: SystemTime,
26 pub payload: T,
28 pub sequence: u64,
30}
31
32impl<T> StreamEvent<T> {
33 pub fn new(timestamp: SystemTime, payload: T, sequence: u64) -> Self {
35 Self {
36 timestamp,
37 payload,
38 sequence,
39 }
40 }
41}
42
43#[derive(Debug, Clone)]
47pub struct SessionWindow<T> {
48 pub start: SystemTime,
50 pub end: SystemTime,
52 pub events: Vec<StreamEvent<T>>,
54 pub session_id: u64,
56}
57
58impl<T> SessionWindow<T> {
59 pub fn duration(&self) -> Duration {
63 self.end
64 .duration_since(self.start)
65 .unwrap_or(Duration::ZERO)
66 }
67
68 pub fn event_count(&self) -> usize {
70 self.events.len()
71 }
72
73 pub fn is_empty(&self) -> bool {
75 self.events.is_empty()
76 }
77}
78
79#[derive(Debug, Clone)]
83pub struct SessionWindowConfig {
84 pub gap_duration: Duration,
86 pub min_events: usize,
89 pub max_session_duration: Option<Duration>,
91 pub allowed_lateness: Duration,
100}
101
102impl Default for SessionWindowConfig {
103 fn default() -> Self {
104 Self {
105 gap_duration: Duration::from_secs(30),
106 min_events: 1,
107 max_session_duration: None,
108 allowed_lateness: Duration::ZERO,
109 }
110 }
111}
112
113pub struct SessionWindowProcessor<T: Clone> {
121 config: SessionWindowConfig,
122 current_session: Option<Vec<StreamEvent<T>>>,
124 session_start: Option<SystemTime>,
126 last_event_time: Option<SystemTime>,
128 next_session_id: u64,
130 closed_sessions: Vec<SessionWindow<T>>,
132}
133
134impl<T: Clone> SessionWindowProcessor<T> {
135 pub fn new(config: SessionWindowConfig) -> Self {
137 Self {
138 config,
139 current_session: None,
140 session_start: None,
141 last_event_time: None,
142 next_session_id: 0,
143 closed_sessions: Vec::new(),
144 }
145 }
146
147 pub fn process(&mut self, event: StreamEvent<T>) -> Result<(), StreamingError> {
160 let event_time = event.timestamp;
161
162 if let Some(last) = self.last_event_time
167 && let Ok(behind) = last.duration_since(event_time)
168 && behind > self.config.allowed_lateness
169 {
170 return Err(StreamingError::InvalidState(format!(
171 "out-of-order event (sequence {}): timestamp is {:?} behind the last processed \
172 event, exceeding allowed_lateness {:?}",
173 event.sequence, behind, self.config.allowed_lateness
174 )));
175 }
176
177 let gap_exceeded = self.last_event_time.map(|last| {
179 event_time.duration_since(last).unwrap_or(Duration::ZERO) > self.config.gap_duration
180 });
181
182 let max_exceeded = self
183 .session_start
184 .zip(self.config.max_session_duration)
185 .map(|(start, max)| event_time.duration_since(start).unwrap_or(Duration::ZERO) > max);
186
187 let should_close = gap_exceeded.unwrap_or(false) || max_exceeded.unwrap_or(false);
188
189 if should_close {
190 self.close_current_session();
191 }
192
193 if self.current_session.is_none() {
195 self.current_session = Some(Vec::new());
196 self.session_start = Some(event_time);
197 }
198
199 self.last_event_time = Some(event_time);
200 if let Some(ref mut session) = self.current_session {
201 session.push(event);
202 }
203
204 Ok(())
205 }
206
207 pub fn flush(&mut self) {
212 self.close_current_session();
213 }
214
215 pub fn drain_sessions(&mut self) -> Vec<SessionWindow<T>> {
220 std::mem::take(&mut self.closed_sessions)
221 }
222
223 pub fn pending_event_count(&self) -> usize {
225 self.current_session.as_ref().map(|s| s.len()).unwrap_or(0)
226 }
227
228 pub fn total_sessions_closed(&self) -> u64 {
230 self.next_session_id
231 }
232
233 fn close_current_session(&mut self) {
236 if let (Some(events), Some(start)) =
237 (self.current_session.take(), self.session_start.take())
238 {
239 let session_id = self.next_session_id;
240 self.next_session_id += 1;
241
242 if events.len() >= self.config.min_events {
243 let end = self.last_event_time.unwrap_or(start);
244 self.closed_sessions.push(SessionWindow {
245 start,
246 end,
247 events,
248 session_id,
249 });
250 }
251 }
253 self.last_event_time = None;
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260 use std::time::UNIX_EPOCH;
261
262 fn ts(secs: u64) -> SystemTime {
263 UNIX_EPOCH + Duration::from_secs(secs)
264 }
265
266 fn event(secs: u64, seq: u64) -> StreamEvent<u32> {
267 StreamEvent::new(ts(secs), seq as u32, seq)
268 }
269
270 #[test]
271 fn test_single_session_from_close_events() {
272 let cfg = SessionWindowConfig {
273 gap_duration: Duration::from_secs(60),
274 min_events: 1,
275 max_session_duration: None,
276 allowed_lateness: Duration::ZERO,
277 };
278 let mut proc = SessionWindowProcessor::new(cfg);
279 proc.process(event(0, 0)).expect("process ok");
280 proc.process(event(10, 1)).expect("process ok");
281 proc.process(event(20, 2)).expect("process ok");
282 proc.flush();
283 let sessions = proc.drain_sessions();
284 assert_eq!(sessions.len(), 1);
285 assert_eq!(sessions[0].event_count(), 3);
286 }
287
288 #[test]
289 fn test_gap_detection_closes_session() {
290 let cfg = SessionWindowConfig {
291 gap_duration: Duration::from_secs(30),
292 min_events: 1,
293 max_session_duration: None,
294 allowed_lateness: Duration::ZERO,
295 };
296 let mut proc = SessionWindowProcessor::new(cfg);
297 proc.process(event(0, 0)).expect("process ok");
298 proc.process(event(60, 1)).expect("process ok");
300 proc.flush();
301 let sessions = proc.drain_sessions();
302 assert_eq!(sessions.len(), 2);
303 }
304
305 #[test]
306 fn test_min_events_filter_drops_small_sessions() {
307 let cfg = SessionWindowConfig {
308 gap_duration: Duration::from_secs(5),
309 min_events: 3,
310 max_session_duration: None,
311 allowed_lateness: Duration::ZERO,
312 };
313 let mut proc = SessionWindowProcessor::new(cfg);
314 proc.process(event(0, 0)).expect("process ok");
315 proc.process(event(1, 1)).expect("process ok");
316 proc.flush();
318 let sessions = proc.drain_sessions();
319 assert_eq!(sessions.len(), 0);
320 }
321
322 #[test]
323 fn test_max_session_duration_force_closes() {
324 let cfg = SessionWindowConfig {
325 gap_duration: Duration::from_secs(100),
326 min_events: 1,
327 max_session_duration: Some(Duration::from_secs(50)),
328 allowed_lateness: Duration::ZERO,
329 };
330 let mut proc = SessionWindowProcessor::new(cfg);
331 proc.process(event(0, 0)).expect("process ok");
332 proc.process(event(60, 1)).expect("process ok");
334 proc.flush();
335 let sessions = proc.drain_sessions();
336 assert_eq!(sessions.len(), 2);
338 }
339
340 #[test]
341 fn test_flush_closes_open_session() {
342 let cfg = SessionWindowConfig::default();
343 let mut proc = SessionWindowProcessor::new(cfg);
344 proc.process(event(0, 0)).expect("process ok");
345 assert_eq!(proc.pending_event_count(), 1);
346 proc.flush();
347 assert_eq!(proc.pending_event_count(), 0);
348 let sessions = proc.drain_sessions();
349 assert_eq!(sessions.len(), 1);
350 }
351
352 #[test]
353 fn test_multiple_sessions_from_gapped_stream() {
354 let cfg = SessionWindowConfig {
355 gap_duration: Duration::from_secs(10),
356 min_events: 1,
357 max_session_duration: None,
358 allowed_lateness: Duration::ZERO,
359 };
360 let mut proc = SessionWindowProcessor::new(cfg);
361 proc.process(event(0, 0)).expect("ok");
363 proc.process(event(5, 1)).expect("ok");
364 proc.process(event(35, 2)).expect("ok");
367 proc.process(event(40, 3)).expect("ok");
368 proc.process(event(100, 4)).expect("ok");
371 proc.flush();
372 let sessions = proc.drain_sessions();
373 assert_eq!(sessions.len(), 3);
374 }
375
376 #[test]
377 fn test_session_id_increments() {
378 let cfg = SessionWindowConfig {
379 gap_duration: Duration::from_secs(5),
380 min_events: 1,
381 max_session_duration: None,
382 allowed_lateness: Duration::ZERO,
383 };
384 let mut proc = SessionWindowProcessor::new(cfg);
385 proc.process(event(0, 0)).expect("ok");
386 proc.process(event(20, 1)).expect("ok"); proc.flush(); let sessions = proc.drain_sessions();
389 assert_eq!(sessions[0].session_id, 0);
390 assert_eq!(sessions[1].session_id, 1);
391 }
392
393 #[test]
394 fn test_session_duration_computation() {
395 let cfg = SessionWindowConfig::default();
396 let mut proc = SessionWindowProcessor::new(cfg);
397 proc.process(event(100, 0)).expect("ok");
398 proc.process(event(110, 1)).expect("ok");
399 proc.flush();
400 let sessions = proc.drain_sessions();
401 assert_eq!(sessions[0].duration(), Duration::from_secs(10));
402 }
403
404 #[test]
405 fn test_empty_processor_has_no_sessions() {
406 let mut proc: SessionWindowProcessor<u32> = SessionWindowProcessor::new(Default::default());
407 proc.flush();
408 assert_eq!(proc.drain_sessions().len(), 0);
409 }
410
411 #[test]
412 fn test_events_within_gap_stay_in_same_session() {
413 let cfg = SessionWindowConfig {
414 gap_duration: Duration::from_secs(60),
415 min_events: 1,
416 max_session_duration: None,
417 allowed_lateness: Duration::ZERO,
418 };
419 let mut proc = SessionWindowProcessor::new(cfg);
420 for i in 0..10u64 {
421 proc.process(event(i * 5, i)).expect("ok"); }
423 proc.flush();
424 let sessions = proc.drain_sessions();
425 assert_eq!(sessions.len(), 1);
426 assert_eq!(sessions[0].event_count(), 10);
427 }
428
429 #[test]
430 fn test_out_of_order_event_rejected() {
431 let cfg = SessionWindowConfig::default(); let mut proc = SessionWindowProcessor::new(cfg);
433 proc.process(event(100, 0)).expect("in-order ok");
434 let err = proc
436 .process(event(95, 1))
437 .expect_err("out-of-order should error");
438 assert!(matches!(err, StreamingError::InvalidState(_)));
439 }
440
441 #[test]
442 fn test_out_of_order_within_allowed_lateness_accepted() {
443 let cfg = SessionWindowConfig {
444 gap_duration: Duration::from_secs(60),
445 allowed_lateness: Duration::from_secs(10),
446 ..Default::default()
447 };
448 let mut proc = SessionWindowProcessor::new(cfg);
449 proc.process(event(100, 0)).expect("ok");
450 proc.process(event(95, 1)).expect("within lateness ok");
452 let err = proc
454 .process(event(80, 2))
455 .expect_err("beyond lateness should error");
456 assert!(matches!(err, StreamingError::InvalidState(_)));
457 }
458
459 #[test]
460 fn test_equal_timestamps_accepted() {
461 let cfg = SessionWindowConfig::default();
463 let mut proc = SessionWindowProcessor::new(cfg);
464 proc.process(event(100, 0)).expect("ok");
465 proc.process(event(100, 1)).expect("equal ts ok");
466 }
467
468 #[test]
469 fn test_pending_event_count_resets_after_flush() {
470 let cfg = SessionWindowConfig::default();
471 let mut proc = SessionWindowProcessor::new(cfg);
472 proc.process(event(0, 0)).expect("ok");
473 proc.process(event(1, 1)).expect("ok");
474 assert_eq!(proc.pending_event_count(), 2);
475 proc.flush();
476 assert_eq!(proc.pending_event_count(), 0);
477 }
478}