1use std::future::Future;
2use std::path::PathBuf;
3use std::pin::Pin;
4use std::time::{Duration, SystemTime, UNIX_EPOCH};
5
6use time::OffsetDateTime;
7use time::format_description::well_known::Rfc3339;
8
9pub trait Clock: Send + Sync {
10 fn now_ms(&self) -> i64;
11 fn local_minute(&self, timestamp_ms: i64) -> u16;
12 fn sleep_until(&self, deadline_ms: i64) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>;
13}
14
15#[derive(Debug, Default)]
16pub struct SystemClock;
17
18impl Clock for SystemClock {
19 fn now_ms(&self) -> i64 {
20 SystemTime::now()
21 .duration_since(UNIX_EPOCH)
22 .map(|elapsed| elapsed.as_millis() as i64)
23 .unwrap_or(0)
24 }
25
26 fn local_minute(&self, timestamp_ms: i64) -> u16 {
27 local_minute(timestamp_ms)
28 }
29
30 fn sleep_until(&self, deadline_ms: i64) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
31 Box::pin(async move {
32 loop {
33 let delay_ms = deadline_ms.saturating_sub(self.now_ms());
34 if delay_ms <= 0 {
35 return;
36 }
37 tokio::time::sleep(Duration::from_millis(delay_ms.min(30_000) as u64)).await;
40 }
41 })
42 }
43}
44
45#[derive(Debug)]
48pub struct FileClock {
49 path: PathBuf,
50}
51
52impl FileClock {
53 pub fn new(path: PathBuf) -> Self {
54 Self { path }
55 }
56
57 fn read_now_ms(&self) -> i64 {
58 std::fs::read_to_string(&self.path)
59 .ok()
60 .and_then(|value| value.trim().parse().ok())
61 .unwrap_or(0)
62 }
63}
64
65impl Clock for FileClock {
66 fn now_ms(&self) -> i64 {
67 self.read_now_ms()
68 }
69
70 fn local_minute(&self, timestamp_ms: i64) -> u16 {
71 local_minute(timestamp_ms)
72 }
73
74 fn sleep_until(&self, deadline_ms: i64) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
75 Box::pin(async move {
76 while self.now_ms() < deadline_ms {
77 tokio::time::sleep(Duration::from_millis(10)).await;
78 }
79 })
80 }
81}
82
83pub fn format_timestamp(timestamp_ms: i64) -> Option<String> {
84 OffsetDateTime::from_unix_timestamp_nanos(i128::from(timestamp_ms) * 1_000_000)
85 .ok()?
86 .format(&Rfc3339)
87 .ok()
88}
89
90pub fn next_local_minute_ms(clock: &dyn Clock, now_ms: i64, minute: u16) -> Option<i64> {
94 let mut candidate = now_ms
95 .div_euclid(60_000)
96 .checked_add(1)?
97 .checked_mul(60_000)?;
98 for _ in 0..=(49 * 60) {
99 if clock.local_minute(candidate) == minute {
100 return Some(candidate);
101 }
102 candidate = candidate.checked_add(60_000)?;
103 }
104 None
105}
106
107fn local_minute(timestamp_ms: i64) -> u16 {
108 let seconds = timestamp_ms.div_euclid(1_000) as libc::time_t;
109 let mut local = unsafe { std::mem::zeroed::<libc::tm>() };
110 let result = unsafe { libc::localtime_r(&seconds, &mut local) };
111 if result.is_null() {
112 return 0;
113 }
114 (local.tm_hour as u16) * 60 + local.tm_min as u16
115}