use chrono::{DateTime, TimeDelta, Utc};
use std::time::Duration;
use tokio::time::Instant;
const DRIFT_DIVISOR: u32 = 10_000;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct VenueClock {
sample: Option<Sample>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HostMoment {
pub time: DateTime<Utc>,
pub instant: Instant,
}
impl HostMoment {
pub const fn new(time: DateTime<Utc>, instant: Instant) -> Self {
Self { time, instant }
}
pub fn now() -> Self {
Self::new(Utc::now(), Instant::now())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Sample {
stamp: DateTime<Utc>,
received: Instant,
}
impl VenueClock {
pub const fn new() -> Self {
Self { sample: None }
}
pub fn observe(&mut self, stamp: DateTime<Utc>, received: Instant) {
if self
.earliest_at(received)
.is_none_or(|earliest| stamp > earliest)
{
self.sample = Some(Sample { stamp, received });
}
}
#[cfg(feature = "binance")]
pub(crate) fn merge(&mut self, other: &VenueClock) {
if let Some(Sample { stamp, received }) = other.sample {
self.observe(stamp, received);
}
}
pub fn earliest_at(&self, at: Instant) -> Option<DateTime<Utc>> {
let Sample { stamp, received } = self.sample?;
Some(if at >= received {
let elapsed = at - received;
to_delta(elapsed - elapsed / DRIFT_DIVISOR)
.and_then(|delta| stamp.checked_add_signed(delta))
.unwrap_or(stamp)
} else {
let before = received - at;
to_delta(before.saturating_add(before / DRIFT_DIVISOR))
.and_then(|delta| stamp.checked_sub_signed(delta))
.unwrap_or(DateTime::<Utc>::MIN_UTC)
})
}
pub fn host_lead(&self, host_time: DateTime<Utc>, at: Instant) -> Option<TimeDelta> {
self.earliest_at(at)
.map(|earliest| host_time.signed_duration_since(earliest))
}
pub fn span_start(&self, moment: HostMoment) -> Option<DateTime<Utc>> {
self.earliest_at(moment.instant)
.map(|venue| venue.min(moment.time))
}
pub fn span_end(&self, since: HostMoment, now: HostMoment) -> DateTime<Utc> {
let counted_on = to_delta(now.instant.saturating_duration_since(since.instant))
.and_then(|delta| since.time.checked_add_signed(delta))
.unwrap_or(DateTime::<Utc>::MAX_UTC);
let venue = self.earliest_at(now.instant).unwrap_or(now.time);
venue.max(now.time).max(counted_on)
}
}
fn to_delta(duration: Duration) -> Option<TimeDelta> {
TimeDelta::from_std(duration).ok()
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
fn at(secs: i64) -> DateTime<Utc> {
DateTime::<Utc>::from_timestamp(1_800_000_000 + secs, 0).unwrap()
}
fn secs(secs: u64) -> Duration {
Duration::from_secs(secs)
}
#[test]
fn a_clock_with_no_sample_gives_no_bound() {
let clock = VenueClock::new();
assert_eq!(clock.earliest_at(Instant::now()), None);
assert_eq!(clock.host_lead(at(0), Instant::now()), None);
}
#[test]
fn the_bound_counts_on_from_the_sample_less_the_drift_allowance() {
let base = Instant::now();
let mut clock = VenueClock::new();
clock.observe(at(100), base);
assert_eq!(clock.earliest_at(base), Some(at(100)));
assert_eq!(clock.earliest_at(base + secs(10_000)), Some(at(10_099)));
}
#[test]
fn the_bound_before_the_sample_counts_back_with_the_drift_allowance_added() {
let base = Instant::now();
let mut clock = VenueClock::new();
clock.observe(at(20_000), base + secs(10_000));
assert_eq!(clock.earliest_at(base), Some(at(9_999)));
}
#[test]
fn a_sample_that_bounds_the_venue_later_replaces_the_one_held() {
let base = Instant::now();
let mut clock = VenueClock::new();
clock.observe(at(100), base);
let received = base + secs(10) - Duration::from_millis(1);
clock.observe(at(110), received);
assert_eq!(clock.earliest_at(received), Some(at(110)));
}
#[test]
fn a_late_old_event_does_not_displace_a_tighter_sample() {
let base = Instant::now();
let mut clock = VenueClock::new();
clock.observe(at(100), base);
clock.observe(at(100 - 3_600), base + secs(1));
assert_eq!(clock.earliest_at(base), Some(at(100)));
}
#[test]
fn a_sample_equal_to_the_bound_held_keeps_the_older_one() {
let base = Instant::now();
let mut clock = VenueClock::new();
clock.observe(at(100), base);
clock.observe(at(10_099), base + secs(10_000));
assert_eq!(clock.earliest_at(base), Some(at(100)));
}
#[test]
#[cfg(feature = "binance")]
fn a_merged_clock_keeps_whichever_sample_bounds_the_venue_later() {
let base = Instant::now();
let mut tighter = VenueClock::new();
tighter.observe(at(110), base);
let mut looser = VenueClock::new();
looser.observe(at(100), base);
let mut clock = looser;
clock.merge(&tighter);
assert_eq!(clock.earliest_at(base), Some(at(110)));
let mut clock = tighter;
clock.merge(&looser);
clock.merge(&VenueClock::new());
assert_eq!(clock.earliest_at(base), Some(at(110)));
}
#[test]
fn a_span_with_no_sample_has_no_start_and_ends_at_the_later_host_time() {
let base = Instant::now();
let clock = VenueClock::new();
assert_eq!(clock.span_start(HostMoment::new(at(100), base)), None);
let since = HostMoment::new(at(100), base);
let now = HostMoment::new(at(110 - 3_600), base + secs(10));
assert_eq!(clock.span_end(since, now), at(110));
}
#[test]
fn a_span_from_a_host_clock_ahead_of_the_venue_starts_at_the_venues_time() {
let base = Instant::now();
let mut clock = VenueClock::new();
clock.observe(at(100), base);
assert_eq!(
clock.span_start(HostMoment::new(at(160), base)),
Some(at(100))
);
}
#[test]
fn a_span_from_a_host_clock_behind_the_venue_starts_at_the_hosts_time() {
let base = Instant::now();
let mut clock = VenueClock::new();
clock.observe(at(100), base);
assert_eq!(
clock.span_start(HostMoment::new(at(40), base)),
Some(at(40))
);
}
#[test]
fn a_span_ends_at_the_latest_of_the_host_the_venue_and_the_start_counted_on() {
let base = Instant::now();
let mut clock = VenueClock::new();
clock.observe(at(100), base);
let since = HostMoment::new(at(100), base);
let behind = HostMoment::new(at(50), base + secs(10));
assert_eq!(clock.span_end(since, behind), at(110));
let ahead = HostMoment::new(at(200), base + secs(10));
assert_eq!(clock.span_end(since, ahead), at(200));
}
#[test]
fn a_span_end_counts_on_from_its_start_when_the_venue_estimate_is_earlier() {
let base = Instant::now();
let mut clock = VenueClock::new();
clock.observe(at(50), base);
let since = HostMoment::new(at(100), base);
let now = HostMoment::new(at(0), base + secs(10));
assert_eq!(clock.span_end(since, now), at(110));
}
#[test]
fn the_host_lead_is_the_host_time_less_the_bound() {
let base = Instant::now();
let mut clock = VenueClock::new();
clock.observe(at(100), base);
assert_eq!(clock.host_lead(at(160), base), Some(TimeDelta::seconds(60)));
assert_eq!(clock.host_lead(at(40), base), Some(TimeDelta::seconds(-60)));
}
}