Skip to main content

rocketmq_runtime/common/
time_utils.rs

1// Copyright 2023 The RocketMQ Rust Authors
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 at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Wall-clock time helpers.
16//!
17//! The `current_*` functions query [`std::time::SystemTime::now`] and return the
18//! elapsed duration since the Unix epoch in the requested unit. The remaining
19//! helpers format, parse and align timestamps in the local time zone, following
20//! the Java broker's formats.
21
22use std::time::SystemTime;
23use std::time::UNIX_EPOCH;
24
25use chrono::DateTime;
26use chrono::Datelike;
27use chrono::Local;
28use chrono::LocalResult;
29use chrono::NaiveDateTime;
30use chrono::TimeZone;
31use chrono::Timelike;
32
33/// The yyyy mm dd hh mm ss sss constant.
34pub const YYYY_MM_DD_HH_MM_SS_SSS: &str = "%Y-%m-%d#%H:%M:%S:%3f";
35/// The yyyymmddhhmmss constant.
36pub const YYYYMMDDHHMMSS: &str = "%Y%m%d%H%M%S";
37
38#[inline(always)]
39fn duration_since_unix_epoch(time: std::time::SystemTime) -> std::time::Duration {
40    time.duration_since(std::time::UNIX_EPOCH).unwrap_or_default()
41}
42
43/// Returns the current Unix timestamp in milliseconds.
44///
45/// Returns 0 if the system clock is set before the Unix epoch.
46#[inline(always)]
47pub fn current_millis() -> u64 {
48    duration_since_unix_epoch(std::time::SystemTime::now()).as_millis() as u64
49}
50
51/// Returns the current Unix timestamp in nanoseconds.
52///
53/// Returns 0 if the system clock is set before the Unix epoch.
54#[inline(always)]
55pub fn current_nano() -> u64 {
56    duration_since_unix_epoch(std::time::SystemTime::now()).as_nanos() as u64
57}
58
59/// Returns the current Unix timestamp in whole seconds.
60///
61/// Returns 0 if the system clock is set before the Unix epoch.
62#[inline(always)]
63pub fn current_secs() -> u64 {
64    duration_since_unix_epoch(std::time::SystemTime::now()).as_secs()
65}
66
67fn local_timestamp_millis_or_epoch(t: i64) -> DateTime<Local> {
68    Local
69        .timestamp_millis_opt(t)
70        .single()
71        .or_else(|| Local.timestamp_millis_opt(0).single())
72        .unwrap_or_else(Local::now)
73}
74
75/// Returns whether the current local hour is one of the `;`-separated hours
76/// in `when`, such as a `deleteWhen` setting of `"04;16"`.
77pub fn is_it_time_to_do(when: &str) -> bool {
78    let hours: Vec<&str> = when.split(";").collect();
79    if !hours.is_empty() {
80        let now = Local::now();
81        for hour in hours {
82            let now_hour: i32 = hour.parse().unwrap_or(0);
83            if now_hour == now.hour() as i32 {
84                return true;
85            }
86        }
87    }
88    false
89}
90
91/// Converts a timestamp in milliseconds to a human-readable string format.
92///
93/// The format is: yyyy-MM-dd HH:mm:ss,SSS
94///
95/// # Arguments
96///
97/// * `t` - Timestamp in milliseconds
98///
99/// # Returns
100///
101/// Formatted date-time string
102pub fn time_millis_to_human_string2(t: i64) -> String {
103    let dt = local_timestamp_millis_or_epoch(t);
104
105    format!(
106        "{:04}-{:02}-{:02} {:02}:{:02}:{:02},{:03}",
107        dt.year(),
108        dt.month(),
109        dt.day(),
110        dt.hour(),
111        dt.minute(),
112        dt.second(),
113        dt.timestamp_subsec_millis()
114    )
115}
116
117/// Converts a timestamp in milliseconds to a compact human-readable string format.
118///
119/// The format is: yyyyMMddHHmmss
120///
121/// # Arguments
122///
123/// * `t` - Timestamp in milliseconds
124///
125/// # Returns
126///
127/// Formatted date-time string
128pub fn time_millis_to_human_string3(t: i64) -> String {
129    let dt = local_timestamp_millis_or_epoch(t);
130
131    format!(
132        "{:04}{:02}{:02}{:02}{:02}{:02}",
133        dt.year(),
134        dt.month(),
135        dt.day(),
136        dt.hour(),
137        dt.minute(),
138        dt.second()
139    )
140}
141
142/// Converts a timestamp in milliseconds to a human-readable string format.
143///
144/// The format is: yyyyMMddHHmmssSSS (year, month, day, hour, minute, second, millisecond)
145///
146/// # Arguments
147///
148/// * `t` - Timestamp in milliseconds
149///
150/// # Returns
151///
152/// Formatted date-time string
153pub fn time_millis_to_human_string(t: i64) -> String {
154    let dt = local_timestamp_millis_or_epoch(t);
155
156    format!(
157        "{:04}{:02}{:02}{:02}{:02}{:02}{:03}",
158        dt.year(),
159        dt.month(),
160        dt.day(),
161        dt.hour(),
162        dt.minute(),
163        dt.second(),
164        dt.timestamp_subsec_millis()
165    )
166}
167
168/// Executes compute next minutes time millis.
169pub fn compute_next_minutes_time_millis() -> u64 {
170    let now = SystemTime::now();
171    let millis_since_epoch = now
172        .duration_since(UNIX_EPOCH)
173        .map_or(0, |duration| duration.as_millis() as u64);
174
175    let millis_in_minute = 60 * 1000;
176    ((millis_since_epoch / millis_in_minute) + 1) * millis_in_minute
177}
178
179/// Executes compute next morning time millis.
180pub fn compute_next_morning_time_millis() -> u64 {
181    let now = Local::now();
182    let tomorrow = now.date_naive().succ_opt().unwrap_or_else(|| now.date_naive());
183    let next_morning = match Local.with_ymd_and_hms(tomorrow.year(), tomorrow.month(), tomorrow.day(), 0, 0, 0) {
184        LocalResult::Single(value) => value,
185        LocalResult::Ambiguous(earliest, _) => earliest,
186        LocalResult::None => now,
187    };
188    next_morning.timestamp_millis().max(0) as u64
189}
190
191/// Parses date.
192pub fn parse_date(date: &str, pattern: &str) -> Option<NaiveDateTime> {
193    NaiveDateTime::parse_from_str(date, pattern).ok()
194}
195
196/// Parses date to millis.
197pub fn parse_date_to_millis(date: &str, pattern: &str) -> Option<i64> {
198    let parsed = parse_date(date, pattern)?;
199    let timestamp = match Local.from_local_datetime(&parsed) {
200        LocalResult::Single(timestamp) => timestamp,
201        LocalResult::Ambiguous(earliest, _) => earliest,
202        LocalResult::None => return None,
203    };
204    Some(timestamp.timestamp_millis())
205}
206
207#[cfg(test)]
208mod tests {
209    use std::time::Duration;
210    use std::time::SystemTime;
211    use std::time::UNIX_EPOCH;
212
213    use super::*;
214
215    #[test]
216    fn duration_since_unix_epoch_returns_zero_for_before_epoch() {
217        let before_epoch = UNIX_EPOCH - Duration::from_secs(1);
218
219        assert_eq!(duration_since_unix_epoch(before_epoch), Duration::ZERO);
220    }
221
222    #[test]
223    fn current_millis_returns_correct_value() {
224        let before = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64;
225        let current = current_millis();
226        let after = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64;
227        assert!(current >= before && current <= after);
228    }
229
230    #[test]
231    fn current_nano_returns_correct_value() {
232        let before = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos() as u64;
233        let current = current_nano();
234        let after = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos() as u64;
235        assert!(current >= before && current <= after);
236    }
237
238    #[test]
239    fn current_secs_returns_correct_value() {
240        let before = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
241        let current = current_secs();
242        let after = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
243        assert!(current >= before && current <= after);
244    }
245
246    #[test]
247    fn all_timestamps_are_positive() {
248        assert!(current_millis() > 0);
249        assert!(current_nano() > 0);
250        assert!(current_secs() > 0);
251    }
252
253    #[test]
254    fn millis_greater_than_secs() {
255        let secs = current_secs();
256        let millis = current_millis();
257        assert!(millis > secs);
258    }
259
260    #[test]
261    fn nanos_greater_than_millis() {
262        let millis = current_millis();
263        let nanos = current_nano();
264        assert!(nanos > millis);
265    }
266
267    #[test]
268    fn timestamps_are_non_decreasing() {
269        let t1 = current_millis();
270        std::thread::sleep(Duration::from_millis(5));
271        let t2 = current_millis();
272        assert!(t2 >= t1);
273    }
274
275    #[test]
276    fn secs_consistent_with_millis() {
277        let secs = current_secs();
278        let millis_as_secs = current_millis() / 1000;
279        // millis is sampled after secs; millis_as_secs is equal to secs or one ahead.
280        assert!(millis_as_secs == secs || millis_as_secs == secs + 1);
281    }
282
283    #[test]
284    fn is_it_time_to_do_returns_true_when_current_hour_is_in_input() {
285        let current_hour = Local::now().hour();
286        assert!(is_it_time_to_do(&current_hour.to_string()));
287    }
288
289    #[test]
290    fn is_it_time_to_do_returns_false_when_current_hour_is_not_in_input() {
291        let current_hour = (Local::now().hour() + 1) % 24;
292        assert!(!is_it_time_to_do(&current_hour.to_string()));
293    }
294
295    #[test]
296    fn time_millis_to_human_string_formats_correctly() {
297        let timestamp = 1743239631601;
298        let expected = Local
299            .timestamp_millis_opt(timestamp)
300            .unwrap()
301            .format("%Y%m%d%H%M%S%3f")
302            .to_string();
303        assert_eq!(time_millis_to_human_string(timestamp), expected);
304    }
305
306    #[test]
307    fn time_millis_to_human_string_falls_back_to_epoch_for_invalid_timestamp() {
308        assert_eq!(time_millis_to_human_string(i64::MAX), time_millis_to_human_string(0));
309        assert_eq!(time_millis_to_human_string2(i64::MAX), time_millis_to_human_string2(0));
310        assert_eq!(time_millis_to_human_string3(i64::MAX), time_millis_to_human_string3(0));
311    }
312
313    #[test]
314    fn test_compute_next_minutes_time_millis() {
315        let next_minute = compute_next_minutes_time_millis();
316        let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64;
317
318        assert!(next_minute > now);
319        assert_eq!(next_minute % (60 * 1000), 0);
320    }
321
322    #[test]
323    fn compute_next_morning_time_millis_returns_non_zero_timestamp() {
324        assert!(compute_next_morning_time_millis() > 0);
325    }
326
327    #[test]
328    fn time_millis_to_human_string2_formats_correctly_with_valid_timestamp() {
329        use chrono::TimeZone;
330
331        let timestamp = 1625140800000;
332        let expected = Local
333            .timestamp_millis_opt(timestamp)
334            .unwrap()
335            .format("%Y-%m-%d %H:%M:%S,%3f")
336            .to_string();
337        assert_eq!(time_millis_to_human_string2(timestamp), expected);
338    }
339
340    #[test]
341    fn time_millis_to_human_string3_formats_correctly_with_valid_timestamp() {
342        let timestamp = 1625140800000;
343        let expect = Local
344            .timestamp_millis_opt(timestamp)
345            .unwrap()
346            .format("%Y%m%d%H%M%S")
347            .to_string();
348        assert_eq!(time_millis_to_human_string3(timestamp), expect);
349    }
350
351    #[test]
352    fn java_date_format_constants_parse_java_shapes() {
353        assert!(parse_date("20250102030405", YYYYMMDDHHMMSS).is_some());
354
355        let parsed = parse_date("2025-01-02#03:04:05:006", YYYY_MM_DD_HH_MM_SS_SSS)
356            .expect("Java millisecond format should parse");
357        assert_eq!(parsed.and_utc().timestamp_subsec_millis(), 6);
358    }
359
360    #[test]
361    fn parse_date_to_millis_uses_local_timezone_like_java_simple_date_format() {
362        let expected = match Local.with_ymd_and_hms(2025, 1, 2, 3, 4, 5) {
363            LocalResult::Single(timestamp) => timestamp,
364            LocalResult::Ambiguous(earliest, _) => earliest,
365            LocalResult::None => panic!("test timestamp should be valid in the local timezone"),
366        };
367
368        assert_eq!(
369            parse_date_to_millis("20250102030405", YYYYMMDDHHMMSS),
370            Some(expected.timestamp_millis())
371        );
372    }
373}