Skip to main content

mz_ore/
now.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License in the LICENSE file at the
6// root of this repository, or online at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Now utilities.
17
18use std::fmt;
19use std::ops::Deref;
20use std::sync::Arc;
21use std::sync::LazyLock;
22use std::time::SystemTime;
23
24#[cfg(feature = "chrono")]
25use chrono::{DateTime, TimeZone, Utc};
26
27#[cfg(feature = "id_gen")]
28use uuid::{NoContext, Uuid};
29
30/// A type representing the number of milliseconds since the Unix epoch.
31pub type EpochMillis = u64;
32
33/// A timestamp that denotes a point in wall-clock time.
34///
35/// Implement this only for types whose values are milliseconds since the Unix
36/// epoch, so that they compare meaningfully against a [`NowFn`].
37pub trait AsEpochMillis {
38    /// The timestamp as milliseconds since the Unix epoch.
39    fn as_epoch_millis(&self) -> EpochMillis;
40}
41
42/// Converts epoch milliseconds to a DateTime.
43#[cfg(feature = "chrono")]
44// TODO(benesch): rewrite to avoid dangerous use of `as`.
45#[allow(clippy::as_conversions)]
46pub fn to_datetime(millis: EpochMillis) -> DateTime<Utc> {
47    let dur = std::time::Duration::from_millis(millis);
48    match Utc
49        .timestamp_opt(dur.as_secs() as i64, dur.subsec_nanos())
50        .single()
51    {
52        Some(single) => single,
53        None => {
54            panic!("Ambiguous timestamp: {millis} millis")
55        }
56    }
57}
58
59/// A function that converts an epoch timestamp to a UUID v7.
60#[cfg(feature = "id_gen")]
61pub fn epoch_to_uuid_v7(epoch: &EpochMillis) -> Uuid {
62    let remainder: u32 = (*epoch % 1000)
63        .try_into()
64        .expect("modulo 1000 of prepared at millis is always within a 32bit unsigned integer.");
65    Uuid::new_v7(uuid::Timestamp::from_unix(
66        NoContext,
67        *epoch / 1000,
68        remainder * 1_000_000,
69    ))
70}
71
72/// A function that returns system or mocked time.
73// This is a newtype so that it can implement `Debug`, as closures don't
74// implement `Debug` by default. It derefs to a callable so that it is
75// ergonomically equivalent to a closure.
76#[derive(Clone)]
77pub struct NowFn<T = EpochMillis>(Arc<dyn Fn() -> T + Send + Sync>);
78
79impl NowFn<EpochMillis> {
80    /// Returns now in seconds.
81    pub fn as_secs(&self) -> i64 {
82        let millis: u64 = (self)();
83        // Justification for `unwrap`:
84        // Any u64, when divided by 1000, is a valid i64.
85        i64::try_from(millis / 1_000).unwrap()
86    }
87}
88
89impl<T> fmt::Debug for NowFn<T> {
90    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
91        f.write_str("<now_fn>")
92    }
93}
94
95impl<T> Deref for NowFn<T> {
96    type Target = dyn Fn() -> T + Send + Sync;
97
98    fn deref(&self) -> &Self::Target {
99        &(*self.0)
100    }
101}
102
103impl<F, T> From<F> for NowFn<T>
104where
105    F: Fn() -> T + Send + Sync + 'static,
106{
107    fn from(f: F) -> NowFn<T> {
108        NowFn(Arc::new(f))
109    }
110}
111
112fn system_time() -> EpochMillis {
113    SystemTime::now()
114        .duration_since(SystemTime::UNIX_EPOCH)
115        .expect("failed to get millis since epoch")
116        .as_millis()
117        .try_into()
118        .expect("current time did not fit into u64")
119}
120fn now_zero() -> EpochMillis {
121    0
122}
123
124/// A [`NowFn`] that returns the actual system time.
125pub static SYSTEM_TIME: LazyLock<NowFn> = LazyLock::new(|| NowFn::from(system_time));
126
127/// A [`NowFn`] that always returns zero.
128///
129/// For use in tests.
130pub static NOW_ZERO: LazyLock<NowFn> = LazyLock::new(|| NowFn::from(now_zero));
131
132#[cfg(feature = "chrono")]
133#[cfg(test)]
134mod tests {
135    use chrono::NaiveDate;
136
137    use super::to_datetime;
138
139    #[crate::test]
140    fn test_to_datetime() {
141        let test_cases = [
142            (
143                0,
144                NaiveDate::from_ymd_opt(1970, 1, 1)
145                    .unwrap()
146                    .and_hms_nano_opt(0, 0, 0, 0)
147                    .unwrap(),
148            ),
149            (
150                1600000000000,
151                NaiveDate::from_ymd_opt(2020, 9, 13)
152                    .unwrap()
153                    .and_hms_nano_opt(12, 26, 40, 0)
154                    .unwrap(),
155            ),
156            (
157                1658323270293,
158                NaiveDate::from_ymd_opt(2022, 7, 20)
159                    .unwrap()
160                    .and_hms_nano_opt(13, 21, 10, 293_000_000)
161                    .unwrap(),
162            ),
163        ];
164        // to_datetime works properly and roundtrips
165        for (millis, datetime) in test_cases.into_iter() {
166            let converted_datetime = to_datetime(millis).naive_utc();
167            assert_eq!(datetime, converted_datetime);
168            assert_eq!(
169                millis,
170                u64::try_from(converted_datetime.and_utc().timestamp_millis()).unwrap()
171            )
172        }
173    }
174}