use reifydb_value::value::{datetime::DateTime, duration::Duration};
use crate::{operator::state::seal::rule::SealRule, window::coord::EventCoord};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct SessionTracker {
pub session_id: u64,
pub last: DateTime,
pub start: DateTime,
opened: bool,
}
impl SessionTracker {
pub fn resumed(session_id: u64, last: DateTime, start: DateTime) -> Self {
Self {
session_id,
last,
start,
opened: true,
}
}
fn is_unopened(&self) -> bool {
!self.opened
}
fn adopt(&mut self, coord: DateTime) {
self.last = coord;
self.start = coord;
self.opened = true;
}
fn extend(&mut self, coord: DateTime) {
self.last = self.last.max(coord);
self.start = self.start.min(coord);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionAssignment {
Opened(u64),
Extended(u64),
Rotated {
closed: u64,
opened: u64,
},
Refused,
}
impl SessionAssignment {
pub fn session_id(self) -> Option<u64> {
match self {
SessionAssignment::Opened(session_id) | SessionAssignment::Extended(session_id) => {
Some(session_id)
}
SessionAssignment::Rotated {
opened,
..
} => Some(opened),
SessionAssignment::Refused => None,
}
}
pub fn closed(self) -> Option<u64> {
match self {
SessionAssignment::Rotated {
closed,
..
} => Some(closed),
_ => None,
}
}
}
pub struct SessionKind {
gap: Duration,
}
impl SessionKind {
pub fn with_gap(gap: Duration) -> Self {
Self {
gap,
}
}
pub fn seal_rule(&self, lateness: Duration) -> SealRule {
SealRule::session(self.gap, lateness)
}
pub fn assign(&self, tracker: &mut SessionTracker, coord: EventCoord) -> SessionAssignment {
let coord = coord.at();
if tracker.is_unopened() {
tracker.adopt(coord);
return SessionAssignment::Opened(tracker.session_id);
}
if coord > tracker.last && coord - tracker.last > self.gap {
let closed = tracker.session_id;
tracker.session_id += 1;
tracker.adopt(coord);
return SessionAssignment::Rotated {
closed,
opened: tracker.session_id,
};
}
if coord < tracker.start && tracker.start - coord > self.gap {
return SessionAssignment::Refused;
}
tracker.extend(coord);
SessionAssignment::Extended(tracker.session_id)
}
}
#[cfg(test)]
mod tests {
use reifydb_value::factory::time::at_millis;
use super::*;
use crate::factory::coord::event_coord_at_millis;
fn ms(millis: u64) -> Duration {
Duration::from_milliseconds_const(millis as i64)
}
fn kind() -> SessionKind {
SessionKind::with_gap(ms(1_000))
}
#[test]
fn a_quiet_period_longer_than_the_gap_rotates_to_a_new_session() {
let mut tracker = SessionTracker::default();
assert_eq!(kind().assign(&mut tracker, event_coord_at_millis(5_000)), SessionAssignment::Opened(0));
assert_eq!(
kind().assign(&mut tracker, event_coord_at_millis(6_001)),
SessionAssignment::Rotated {
closed: 0,
opened: 1
}
);
assert_eq!(tracker.session_id, 1);
}
#[test]
fn a_quiet_period_of_exactly_the_gap_stays_in_the_same_session() {
let mut tracker = SessionTracker::default();
kind().assign(&mut tracker, event_coord_at_millis(5_000));
assert_eq!(kind().assign(&mut tracker, event_coord_at_millis(6_000)), SessionAssignment::Extended(0));
}
#[test]
fn a_late_row_inside_the_gap_extends_the_session_backwards() {
let mut tracker = SessionTracker::default();
kind().assign(&mut tracker, event_coord_at_millis(5_000));
assert_eq!(kind().assign(&mut tracker, event_coord_at_millis(4_500)), SessionAssignment::Extended(0));
assert_eq!(tracker.start, at_millis(4_500));
assert_eq!(tracker.last, at_millis(5_000), "reaching backwards must not drag the high end down");
}
#[test]
fn a_row_far_before_the_session_start_is_refused_rather_than_misfiled() {
let mut tracker = SessionTracker::default();
kind().assign(&mut tracker, event_coord_at_millis(5_000));
assert_eq!(kind().assign(&mut tracker, event_coord_at_millis(3_999)), SessionAssignment::Refused);
assert_eq!(tracker.start, at_millis(5_000), "a refused row must leave the tracker untouched");
assert_eq!(tracker.last, at_millis(5_000));
}
#[test]
fn a_fresh_tracker_adopts_its_first_coordinate_without_closing_anything() {
let mut tracker = SessionTracker::default();
let assignment = kind().assign(&mut tracker, event_coord_at_millis(9_000));
assert_eq!(assignment, SessionAssignment::Opened(0));
assert_eq!(assignment.closed(), None);
assert_eq!(tracker, SessionTracker::resumed(0, at_millis(9_000), at_millis(9_000)));
}
#[test]
fn a_session_opened_at_the_epoch_still_rotates_across_its_gap() {
let mut tracker = SessionTracker::default();
assert_eq!(kind().assign(&mut tracker, event_coord_at_millis(0)), SessionAssignment::Opened(0));
assert_eq!(tracker.last, at_millis(0), "the tracker must keep the epoch coordinate it adopted");
assert_eq!(
kind().assign(&mut tracker, event_coord_at_millis(1_001)),
SessionAssignment::Rotated {
closed: 0,
opened: 1
}
);
}
#[test]
fn a_tracker_that_has_adopted_the_epoch_is_distinguishable_from_a_fresh_one() {
let mut tracker = SessionTracker::default();
kind().assign(&mut tracker, event_coord_at_millis(0));
assert_ne!(tracker, SessionTracker::default());
assert_eq!(tracker, SessionTracker::resumed(0, at_millis(0), at_millis(0)));
}
#[test]
fn a_resumed_tracker_continues_the_session_it_was_persisted_with() {
let mut tracker = SessionTracker::resumed(7, at_millis(5_000), at_millis(4_000));
assert_eq!(kind().assign(&mut tracker, event_coord_at_millis(5_500)), SessionAssignment::Extended(7));
assert_eq!(tracker, SessionTracker::resumed(7, at_millis(5_500), at_millis(4_000)));
}
#[test]
fn a_refused_row_reports_no_session_and_a_rotation_reports_the_new_one() {
assert_eq!(SessionAssignment::Refused.session_id(), None);
assert_eq!(
SessionAssignment::Rotated {
closed: 3,
opened: 4
}
.session_id(),
Some(4)
);
assert_eq!(SessionAssignment::Opened(2).session_id(), Some(2));
assert_eq!(SessionAssignment::Extended(2).session_id(), Some(2));
}
#[test]
fn a_zero_gap_puts_every_distinct_instant_in_its_own_session() {
let kind = SessionKind::with_gap(ms(0));
let mut tracker = SessionTracker::default();
assert_eq!(kind.assign(&mut tracker, event_coord_at_millis(5_000)), SessionAssignment::Opened(0));
assert_eq!(kind.assign(&mut tracker, event_coord_at_millis(5_000)), SessionAssignment::Extended(0));
assert_eq!(
kind.assign(&mut tracker, event_coord_at_millis(5_001)),
SessionAssignment::Rotated {
closed: 0,
opened: 1
}
);
}
}