architect_sdk/
discrete_clock.rs1use anyhow::{bail, Result};
5use chrono::{DateTime, DurationRound, Utc};
6use std::future::Future;
7use thiserror::Error;
8use tokio::time::{Instant, Interval, MissedTickBehavior};
9
10pub trait Clock {
11 type Error;
12
13 fn tick(&mut self)
14 -> impl Future<Output = Result<DateTime<Utc>, Self::Error>> + Send;
15}
16
17pub struct DiscreteClock {
18 interval: Interval,
19 interval_duration: chrono::Duration,
20 tolerance: chrono::Duration,
21}
22
23impl DiscreteClock {
24 pub fn from_period(period: chrono::Duration) -> Result<Self> {
25 if period.subsec_nanos() != 0 {
26 bail!("period must be a whole number of seconds");
27 }
28 let interval_duration = period;
29 let dur = interval_duration.to_std()?;
30 let now = Utc::now();
31 let now_aliased = (now + interval_duration).duration_trunc(interval_duration)?;
32 let mut start_at = Instant::now();
33 if now_aliased > now {
34 start_at = Instant::now() + (now_aliased - now).to_std()?;
35 }
36 let mut interval = tokio::time::interval_at(start_at, dur);
37 interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
38 Ok(Self { interval, interval_duration, tolerance: interval_duration / 100 })
39 }
40
41 pub fn from_secs(interval_secs: u64) -> Result<Self> {
42 let interval_duration = chrono::Duration::seconds(interval_secs as i64);
43 Self::from_period(interval_duration)
44 }
45}
46
47impl Clock for DiscreteClock {
48 type Error = DiscreteClockError;
49
50 async fn tick(&mut self) -> Result<DateTime<Utc>, DiscreteClockError> {
55 self.interval.tick().await;
56 let now = Utc::now();
57 let now_aliased = now
58 .duration_trunc(self.interval_duration)
59 .map_err(DiscreteClockError::TickRoundingError)?;
60 if (now - now_aliased).abs() > self.tolerance {
61 *self = Self::from_period(self.interval_duration)
62 .map_err(DiscreteClockError::ResetError)?;
63 return Err(DiscreteClockError::TickSkew {
64 actual: now,
65 expected: now_aliased,
66 })?;
67 }
68 Ok(now_aliased)
69 }
70}
71
72#[derive(Error, Debug)]
73pub enum DiscreteClockError {
74 #[error("tick rounding error: {0}")]
75 TickRoundingError(#[from] chrono::RoundingError),
76 #[error("{actual} is too far off the grid time {expected}")]
77 TickSkew { actual: DateTime<Utc>, expected: DateTime<Utc> },
78 #[error("failed to reset clock to recover from tick skew: {0}")]
79 ResetError(anyhow::Error),
80}
81
82impl Clock for Option<DiscreteClock> {
83 type Error = DiscreteClockError;
84
85 async fn tick(&mut self) -> Result<DateTime<Utc>, DiscreteClockError> {
86 if let Some(clock) = self.as_mut() {
87 clock.tick().await
88 } else {
89 std::future::pending().await
90 }
91 }
92}