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/// Converts epoch milliseconds to a DateTime.
34#[cfg(feature = "chrono")]
35// TODO(benesch): rewrite to avoid dangerous use of `as`.
36#[allow(clippy::as_conversions)]
37pub fn to_datetime(millis: EpochMillis) -> DateTime<Utc> {
38    let dur = std::time::Duration::from_millis(millis);
39    match Utc
40        .timestamp_opt(dur.as_secs() as i64, dur.subsec_nanos())
41        .single()
42    {
43        Some(single) => single,
44        None => {
45            panic!("Ambiguous timestamp: {millis} millis")
46        }
47    }
48}
49
50/// A function that converts an epoch timestamp to a UUID v7.
51#[cfg(feature = "id_gen")]
52pub fn epoch_to_uuid_v7(epoch: &EpochMillis) -> Uuid {
53    let remainder: u32 = (*epoch % 1000)
54        .try_into()
55        .expect("modulo 1000 of prepared at millis is always within a 32bit unsigned integer.");
56    Uuid::new_v7(uuid::Timestamp::from_unix(
57        NoContext,
58        *epoch / 1000,
59        remainder * 1_000_000,
60    ))
61}
62
63/// A function that returns system or mocked time.
64// This is a newtype so that it can implement `Debug`, as closures don't
65// implement `Debug` by default. It derefs to a callable so that it is
66// ergonomically equivalent to a closure.
67#[derive(Clone)]
68pub struct NowFn<T = EpochMillis>(Arc<dyn Fn() -> T + Send + Sync>);
69
70impl NowFn<EpochMillis> {
71    /// Returns now in seconds.
72    pub fn as_secs(&self) -> i64 {
73        let millis: u64 = (self)();
74        // Justification for `unwrap`:
75        // Any u64, when divided by 1000, is a valid i64.
76        i64::try_from(millis / 1_000).unwrap()
77    }
78}
79
80impl<T> fmt::Debug for NowFn<T> {
81    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
82        f.write_str("<now_fn>")
83    }
84}
85
86impl<T> Deref for NowFn<T> {
87    type Target = dyn Fn() -> T + Send + Sync;
88
89    fn deref(&self) -> &Self::Target {
90        &(*self.0)
91    }
92}
93
94impl<F, T> From<F> for NowFn<T>
95where
96    F: Fn() -> T + Send + Sync + 'static,
97{
98    fn from(f: F) -> NowFn<T> {
99        NowFn(Arc::new(f))
100    }
101}
102
103fn system_time() -> EpochMillis {
104    SystemTime::now()
105        .duration_since(SystemTime::UNIX_EPOCH)
106        .expect("failed to get millis since epoch")
107        .as_millis()
108        .try_into()
109        .expect("current time did not fit into u64")
110}
111fn now_zero() -> EpochMillis {
112    0
113}
114
115/// A [`NowFn`] that returns the actual system time.
116pub static SYSTEM_TIME: LazyLock<NowFn> = LazyLock::new(|| NowFn::from(system_time));
117
118/// A [`NowFn`] that always returns zero.
119///
120/// For use in tests.
121pub static NOW_ZERO: LazyLock<NowFn> = LazyLock::new(|| NowFn::from(now_zero));
122
123#[cfg(feature = "chrono")]
124#[cfg(test)]
125mod tests {
126    use chrono::NaiveDate;
127
128    use super::to_datetime;
129
130    #[crate::test]
131    fn test_to_datetime() {
132        let test_cases = [
133            (
134                0,
135                NaiveDate::from_ymd_opt(1970, 1, 1)
136                    .unwrap()
137                    .and_hms_nano_opt(0, 0, 0, 0)
138                    .unwrap(),
139            ),
140            (
141                1600000000000,
142                NaiveDate::from_ymd_opt(2020, 9, 13)
143                    .unwrap()
144                    .and_hms_nano_opt(12, 26, 40, 0)
145                    .unwrap(),
146            ),
147            (
148                1658323270293,
149                NaiveDate::from_ymd_opt(2022, 7, 20)
150                    .unwrap()
151                    .and_hms_nano_opt(13, 21, 10, 293_000_000)
152                    .unwrap(),
153            ),
154        ];
155        // to_datetime works properly and roundtrips
156        for (millis, datetime) in test_cases.into_iter() {
157            let converted_datetime = to_datetime(millis).naive_utc();
158            assert_eq!(datetime, converted_datetime);
159            assert_eq!(
160                millis,
161                u64::try_from(converted_datetime.and_utc().timestamp_millis()).unwrap()
162            )
163        }
164    }
165}