reifydb_flow/window/kind/
session.rs1use reifydb_value::value::{datetime::DateTime, duration::Duration};
5
6use crate::{operator::state::seal::rule::SealRule, window::coord::EventCoord};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub struct SessionTracker {
10 pub session_id: u64,
11 pub last: DateTime,
12 pub start: DateTime,
13 opened: bool,
14}
15
16impl SessionTracker {
17 pub fn resumed(session_id: u64, last: DateTime, start: DateTime) -> Self {
18 Self {
19 session_id,
20 last,
21 start,
22 opened: true,
23 }
24 }
25
26 fn is_unopened(&self) -> bool {
27 !self.opened
28 }
29
30 fn adopt(&mut self, coord: DateTime) {
31 self.last = coord;
32 self.start = coord;
33 self.opened = true;
34 }
35
36 fn extend(&mut self, coord: DateTime) {
37 self.last = self.last.max(coord);
38 self.start = self.start.min(coord);
39 }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum SessionAssignment {
44 Opened(u64),
45 Extended(u64),
46 Rotated {
47 closed: u64,
48 opened: u64,
49 },
50 Refused,
51}
52
53impl SessionAssignment {
54 pub fn session_id(self) -> Option<u64> {
55 match self {
56 SessionAssignment::Opened(session_id) | SessionAssignment::Extended(session_id) => {
57 Some(session_id)
58 }
59 SessionAssignment::Rotated {
60 opened,
61 ..
62 } => Some(opened),
63 SessionAssignment::Refused => None,
64 }
65 }
66
67 pub fn closed(self) -> Option<u64> {
68 match self {
69 SessionAssignment::Rotated {
70 closed,
71 ..
72 } => Some(closed),
73 _ => None,
74 }
75 }
76}
77
78pub struct SessionKind {
79 gap: Duration,
80}
81
82impl SessionKind {
83 pub fn with_gap(gap: Duration) -> Self {
84 Self {
85 gap,
86 }
87 }
88
89 pub fn seal_rule(&self, lateness: Duration) -> SealRule {
90 SealRule::session(self.gap, lateness)
91 }
92
93 pub fn assign(&self, tracker: &mut SessionTracker, coord: EventCoord) -> SessionAssignment {
94 let coord = coord.at();
95
96 if tracker.is_unopened() {
97 tracker.adopt(coord);
98 return SessionAssignment::Opened(tracker.session_id);
99 }
100 if coord > tracker.last && coord - tracker.last > self.gap {
101 let closed = tracker.session_id;
102 tracker.session_id += 1;
103 tracker.adopt(coord);
104 return SessionAssignment::Rotated {
105 closed,
106 opened: tracker.session_id,
107 };
108 }
109 if coord < tracker.start && tracker.start - coord > self.gap {
110 return SessionAssignment::Refused;
111 }
112 tracker.extend(coord);
113 SessionAssignment::Extended(tracker.session_id)
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use reifydb_value::factory::time::at_millis;
120
121 use super::*;
122 use crate::factory::coord::event_coord_at_millis;
123
124 fn ms(millis: u64) -> Duration {
125 Duration::from_milliseconds_const(millis as i64)
126 }
127
128 fn kind() -> SessionKind {
129 SessionKind::with_gap(ms(1_000))
130 }
131
132 #[test]
133 fn a_quiet_period_longer_than_the_gap_rotates_to_a_new_session() {
134 let mut tracker = SessionTracker::default();
138
139 assert_eq!(kind().assign(&mut tracker, event_coord_at_millis(5_000)), SessionAssignment::Opened(0));
140 assert_eq!(
141 kind().assign(&mut tracker, event_coord_at_millis(6_001)),
142 SessionAssignment::Rotated {
143 closed: 0,
144 opened: 1
145 }
146 );
147 assert_eq!(tracker.session_id, 1);
148 }
149
150 #[test]
151 fn a_quiet_period_of_exactly_the_gap_stays_in_the_same_session() {
152 let mut tracker = SessionTracker::default();
155
156 kind().assign(&mut tracker, event_coord_at_millis(5_000));
157
158 assert_eq!(kind().assign(&mut tracker, event_coord_at_millis(6_000)), SessionAssignment::Extended(0));
159 }
160
161 #[test]
162 fn a_late_row_inside_the_gap_extends_the_session_backwards() {
163 let mut tracker = SessionTracker::default();
166
167 kind().assign(&mut tracker, event_coord_at_millis(5_000));
168
169 assert_eq!(kind().assign(&mut tracker, event_coord_at_millis(4_500)), SessionAssignment::Extended(0));
170 assert_eq!(tracker.start, at_millis(4_500));
171 assert_eq!(tracker.last, at_millis(5_000), "reaching backwards must not drag the high end down");
172 }
173
174 #[test]
175 fn a_row_far_before_the_session_start_is_refused_rather_than_misfiled() {
176 let mut tracker = SessionTracker::default();
180
181 kind().assign(&mut tracker, event_coord_at_millis(5_000));
182
183 assert_eq!(kind().assign(&mut tracker, event_coord_at_millis(3_999)), SessionAssignment::Refused);
184 assert_eq!(tracker.start, at_millis(5_000), "a refused row must leave the tracker untouched");
185 assert_eq!(tracker.last, at_millis(5_000));
186 }
187
188 #[test]
189 fn a_fresh_tracker_adopts_its_first_coordinate_without_closing_anything() {
190 let mut tracker = SessionTracker::default();
194
195 let assignment = kind().assign(&mut tracker, event_coord_at_millis(9_000));
196
197 assert_eq!(assignment, SessionAssignment::Opened(0));
198 assert_eq!(assignment.closed(), None);
199 assert_eq!(tracker, SessionTracker::resumed(0, at_millis(9_000), at_millis(9_000)));
200 }
201
202 #[test]
203 fn a_session_opened_at_the_epoch_still_rotates_across_its_gap() {
204 let mut tracker = SessionTracker::default();
208
209 assert_eq!(kind().assign(&mut tracker, event_coord_at_millis(0)), SessionAssignment::Opened(0));
210 assert_eq!(tracker.last, at_millis(0), "the tracker must keep the epoch coordinate it adopted");
211 assert_eq!(
212 kind().assign(&mut tracker, event_coord_at_millis(1_001)),
213 SessionAssignment::Rotated {
214 closed: 0,
215 opened: 1
216 }
217 );
218 }
219
220 #[test]
221 fn a_tracker_that_has_adopted_the_epoch_is_distinguishable_from_a_fresh_one() {
222 let mut tracker = SessionTracker::default();
226
227 kind().assign(&mut tracker, event_coord_at_millis(0));
228
229 assert_ne!(tracker, SessionTracker::default());
230 assert_eq!(tracker, SessionTracker::resumed(0, at_millis(0), at_millis(0)));
231 }
232
233 #[test]
234 fn a_resumed_tracker_continues_the_session_it_was_persisted_with() {
235 let mut tracker = SessionTracker::resumed(7, at_millis(5_000), at_millis(4_000));
239
240 assert_eq!(kind().assign(&mut tracker, event_coord_at_millis(5_500)), SessionAssignment::Extended(7));
241 assert_eq!(tracker, SessionTracker::resumed(7, at_millis(5_500), at_millis(4_000)));
242 }
243
244 #[test]
245 fn a_refused_row_reports_no_session_and_a_rotation_reports_the_new_one() {
246 assert_eq!(SessionAssignment::Refused.session_id(), None);
249 assert_eq!(
250 SessionAssignment::Rotated {
251 closed: 3,
252 opened: 4
253 }
254 .session_id(),
255 Some(4)
256 );
257 assert_eq!(SessionAssignment::Opened(2).session_id(), Some(2));
258 assert_eq!(SessionAssignment::Extended(2).session_id(), Some(2));
259 }
260
261 #[test]
262 fn a_zero_gap_puts_every_distinct_instant_in_its_own_session() {
263 let kind = SessionKind::with_gap(ms(0));
267 let mut tracker = SessionTracker::default();
268
269 assert_eq!(kind.assign(&mut tracker, event_coord_at_millis(5_000)), SessionAssignment::Opened(0));
270 assert_eq!(kind.assign(&mut tracker, event_coord_at_millis(5_000)), SessionAssignment::Extended(0));
271 assert_eq!(
272 kind.assign(&mut tracker, event_coord_at_millis(5_001)),
273 SessionAssignment::Rotated {
274 closed: 0,
275 opened: 1
276 }
277 );
278 }
279}