Skip to main content

rocketmq_model/common/message/
timer_request.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
15use std::collections::HashMap;
16
17use cheetah_string::CheetahString;
18
19use crate::common::message::MessageConst;
20use crate::ModelContractViolation;
21
22/// Timer precisions supported by the Java-compatible timer engine.
23pub const JAVA_COMPAT_TIMER_PRECISIONS_MS: [u64; 4] = [100, 200, 500, 1_000];
24
25/// Immutable admission policy used while normalizing one timer request.
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub struct TimerPolicySnapshot {
28    precision_ms: u64,
29    max_delay_ms: u64,
30}
31
32impl TimerPolicySnapshot {
33    const FINGERPRINT_NAMESPACE: u64 = 0x524D_5154_504F_4C31;
34    /// Creates a validated Java-compatible timer policy.
35    ///
36    /// # Errors
37    ///
38    /// Returns [`ModelContractViolation::UnsupportedTimerPrecision`] when the precision is not one of the
39    /// values supported by the Java-compatible timer engine.
40    pub fn try_new(precision_ms: u64, max_delay_ms: u64) -> Result<Self, ModelContractViolation> {
41        if !JAVA_COMPAT_TIMER_PRECISIONS_MS.contains(&precision_ms) {
42            return Err(ModelContractViolation::UnsupportedTimerPrecision { precision_ms });
43        }
44        Ok(Self {
45            precision_ms,
46            max_delay_ms,
47        })
48    }
49
50    #[inline]
51    pub const fn precision_ms(self) -> u64 {
52        self.precision_ms
53    }
54
55    #[inline]
56    pub const fn max_delay_ms(self) -> u64 {
57        self.max_delay_ms
58    }
59
60    /// Returns the stable fingerprint persisted with normalized timer requests.
61    ///
62    /// Only admission semantics participate. Physical storage parameters such as segment or page
63    /// size deliberately do not change ownership of already accepted records.
64    pub const fn fingerprint(self) -> u64 {
65        const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
66        const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
67
68        let values = [Self::FINGERPRINT_NAMESPACE, self.precision_ms, self.max_delay_ms];
69        let mut hash = FNV_OFFSET;
70        let mut value_index = 0;
71        while value_index < values.len() {
72            let bytes = values[value_index].to_be_bytes();
73            let mut byte_index = 0;
74            while byte_index < bytes.len() {
75                hash ^= bytes[byte_index] as u64;
76                hash = hash.wrapping_mul(FNV_PRIME);
77                byte_index += 1;
78            }
79            value_index += 1;
80        }
81        hash
82    }
83}
84
85/// Property selected according to RocketMQ's timer-property precedence.
86#[derive(Clone, Copy, Debug, PartialEq, Eq)]
87pub enum TimerRequestKind {
88    DelaySeconds,
89    DelayMilliseconds,
90    DeliverAtMilliseconds,
91}
92
93/// Canonical result shared by Broker, remoting, and Proxy admission adapters.
94#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub struct NormalizedTimerRequest {
96    pub kind: TimerRequestKind,
97    pub original_deliver_ms: u64,
98    pub timer_out_ms: u64,
99}
100
101/// Normalizes RocketMQ timer properties without reading the system clock.
102///
103/// Property precedence matches Java RocketMQ: `TIMER_DELAY_SEC`, then `TIMER_DELAY_MS`, then
104/// `TIMER_DELIVER_MS`. The caller must sample `now_ms` once and pass that value to every adapter
105/// involved in the same admission decision.
106///
107/// # Errors
108///
109/// Returns a structured error for malformed input, unsupported precision, arithmetic overflow,
110/// past delivery times, and requests beyond the configured horizon.
111pub fn normalize_timer_request(
112    properties: &HashMap<CheetahString, CheetahString>,
113    now_ms: u64,
114    policy: TimerPolicySnapshot,
115) -> Result<NormalizedTimerRequest, ModelContractViolation> {
116    normalize_timer_request_fields(
117        properties
118            .get(MessageConst::PROPERTY_TIMER_DELAY_SEC)
119            .map(CheetahString::as_str),
120        properties
121            .get(MessageConst::PROPERTY_TIMER_DELAY_MS)
122            .map(CheetahString::as_str),
123        properties
124            .get(MessageConst::PROPERTY_TIMER_DELIVER_MS)
125            .map(CheetahString::as_str),
126        now_ms,
127        policy,
128    )
129}
130
131/// Normalizes timer fields for adapters that do not use RocketMQ's internal property-map type.
132///
133/// # Errors
134///
135/// Has the same validation behavior as [`normalize_timer_request`].
136pub fn normalize_timer_request_fields(
137    delay_seconds: Option<&str>,
138    delay_milliseconds: Option<&str>,
139    deliver_at_milliseconds: Option<&str>,
140    now_ms: u64,
141    policy: TimerPolicySnapshot,
142) -> Result<NormalizedTimerRequest, ModelContractViolation> {
143    let (kind, original_deliver_ms) = if let Some(value) = delay_seconds {
144        let delay_seconds = parse_property(MessageConst::PROPERTY_TIMER_DELAY_SEC, value)?;
145        let delay_ms = delay_seconds
146            .checked_mul(1_000)
147            .ok_or(ModelContractViolation::TimerDeliveryArithmeticOverflow)?;
148        (
149            TimerRequestKind::DelaySeconds,
150            now_ms
151                .checked_add(delay_ms)
152                .ok_or(ModelContractViolation::TimerDeliveryArithmeticOverflow)?,
153        )
154    } else if let Some(value) = delay_milliseconds {
155        let delay_ms = parse_property(MessageConst::PROPERTY_TIMER_DELAY_MS, value)?;
156        (
157            TimerRequestKind::DelayMilliseconds,
158            now_ms
159                .checked_add(delay_ms)
160                .ok_or(ModelContractViolation::TimerDeliveryArithmeticOverflow)?,
161        )
162    } else if let Some(value) = deliver_at_milliseconds {
163        (
164            TimerRequestKind::DeliverAtMilliseconds,
165            parse_property(MessageConst::PROPERTY_TIMER_DELIVER_MS, value)?,
166        )
167    } else {
168        return Err(ModelContractViolation::MissingTimerDeliveryProperty);
169    };
170
171    let delay_ms =
172        original_deliver_ms
173            .checked_sub(now_ms)
174            .ok_or(ModelContractViolation::TimerDeliveryTimeIsNotInFuture {
175                deliver_ms: original_deliver_ms,
176                now_ms,
177            })?;
178    if delay_ms == 0 {
179        return Err(ModelContractViolation::TimerDeliveryTimeIsNotInFuture {
180            deliver_ms: original_deliver_ms,
181            now_ms,
182        });
183    }
184    if delay_ms > policy.max_delay_ms() {
185        return Err(ModelContractViolation::TimerDelayExceedsMaximum {
186            delay_ms,
187            max_delay_ms: policy.max_delay_ms(),
188        });
189    }
190
191    let precision_ms = policy.precision_ms();
192    let timer_out_ms = if original_deliver_ms.is_multiple_of(precision_ms) {
193        original_deliver_ms
194            .checked_sub(precision_ms)
195            .ok_or(ModelContractViolation::TimerDeliveryArithmeticOverflow)?
196    } else {
197        original_deliver_ms / precision_ms * precision_ms
198    };
199
200    Ok(NormalizedTimerRequest {
201        kind,
202        original_deliver_ms,
203        timer_out_ms,
204    })
205}
206
207fn parse_property(property: &'static str, value: &str) -> Result<u64, ModelContractViolation> {
208    value
209        .parse::<u64>()
210        .map_err(|_| ModelContractViolation::InvalidTimerProperty { property })
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    fn properties(values: &[(&'static str, &'static str)]) -> HashMap<CheetahString, CheetahString> {
218        values
219            .iter()
220            .map(|(key, value)| {
221                (
222                    CheetahString::from_static_str(key),
223                    CheetahString::from_static_str(value),
224                )
225            })
226            .collect()
227    }
228
229    #[test]
230    fn timer_request_uses_java_property_precedence() {
231        let input = properties(&[
232            (MessageConst::PROPERTY_TIMER_DELIVER_MS, "999999"),
233            (MessageConst::PROPERTY_TIMER_DELAY_MS, "2500"),
234            (MessageConst::PROPERTY_TIMER_DELAY_SEC, "2"),
235        ]);
236        let policy = TimerPolicySnapshot::try_new(1_000, 10_000).unwrap();
237
238        let normalized = normalize_timer_request(&input, 10_000, policy).unwrap();
239
240        assert_eq!(normalized.kind, TimerRequestKind::DelaySeconds);
241        assert_eq!(normalized.original_deliver_ms, 12_000);
242        assert_eq!(normalized.timer_out_ms, 11_000);
243    }
244
245    #[test]
246    fn timer_request_preserves_exact_target_minus_one_tick_encoding() {
247        for precision_ms in JAVA_COMPAT_TIMER_PRECISIONS_MS {
248            let policy = TimerPolicySnapshot::try_new(precision_ms, 10_000).unwrap();
249            let exact = properties(&[(MessageConst::PROPERTY_TIMER_DELIVER_MS, "12000")]);
250            let non_exact = properties(&[(MessageConst::PROPERTY_TIMER_DELIVER_MS, "12001")]);
251
252            assert_eq!(
253                normalize_timer_request(&exact, 10_000, policy).unwrap().timer_out_ms,
254                12_000 - precision_ms
255            );
256            assert_eq!(
257                normalize_timer_request(&non_exact, 10_000, policy)
258                    .unwrap()
259                    .timer_out_ms,
260                12_001 / precision_ms * precision_ms
261            );
262        }
263    }
264
265    #[test]
266    fn timer_request_rejects_invalid_configuration_and_arithmetic() {
267        assert_eq!(
268            TimerPolicySnapshot::try_new(0, 1_000),
269            Err(ModelContractViolation::UnsupportedTimerPrecision { precision_ms: 0 })
270        );
271        assert_eq!(
272            TimerPolicySnapshot::try_new(250, 1_000),
273            Err(ModelContractViolation::UnsupportedTimerPrecision { precision_ms: 250 })
274        );
275
276        let policy = TimerPolicySnapshot::try_new(1_000, u64::MAX).unwrap();
277        let overflow = properties(&[(MessageConst::PROPERTY_TIMER_DELAY_SEC, "18446744073709552")]);
278        assert_eq!(
279            normalize_timer_request(&overflow, 1, policy),
280            Err(ModelContractViolation::TimerDeliveryArithmeticOverflow)
281        );
282    }
283
284    #[test]
285    fn timer_property_violation_keeps_only_its_static_schema_identifier() {
286        let rejected_value = "untrusted-timer-value";
287        let policy = TimerPolicySnapshot::try_new(1_000, 10_000).unwrap();
288
289        let error = normalize_timer_request_fields(Some(rejected_value), None, None, 1, policy)
290            .expect_err("non-numeric timer property should be rejected");
291
292        assert_eq!(
293            error,
294            ModelContractViolation::InvalidTimerProperty {
295                property: MessageConst::PROPERTY_TIMER_DELAY_SEC,
296            }
297        );
298        assert_eq!(error.condition(), rocketmq_error::CanonicalCondition::InvalidArgument);
299        assert_eq!(
300            error.to_string(),
301            "timer property has an invalid unsigned integer value"
302        );
303        assert!(!format!("{error:?}").contains(rejected_value));
304    }
305
306    #[test]
307    fn timer_request_enforces_future_and_maximum_boundaries() {
308        let policy = TimerPolicySnapshot::try_new(1_000, 3_000).unwrap();
309        let at_max = properties(&[(MessageConst::PROPERTY_TIMER_DELIVER_MS, "13000")]);
310        let beyond_max = properties(&[(MessageConst::PROPERTY_TIMER_DELIVER_MS, "13001")]);
311        let past = properties(&[(MessageConst::PROPERTY_TIMER_DELIVER_MS, "9999")]);
312
313        assert!(normalize_timer_request(&at_max, 10_000, policy).is_ok());
314        assert!(matches!(
315            normalize_timer_request(&beyond_max, 10_000, policy),
316            Err(ModelContractViolation::TimerDelayExceedsMaximum { .. })
317        ));
318        assert!(matches!(
319            normalize_timer_request(&past, 10_000, policy),
320            Err(ModelContractViolation::TimerDeliveryTimeIsNotInFuture { .. })
321        ));
322    }
323
324    #[test]
325    fn timer_policy_fingerprint_changes_only_with_admission_semantics() {
326        let baseline = TimerPolicySnapshot::try_new(1_000, 3_000).unwrap();
327        assert_eq!(baseline.fingerprint(), baseline.fingerprint());
328        assert_eq!(baseline.fingerprint(), 0xef50_c0cf_fbb6_187f);
329        assert_ne!(
330            baseline.fingerprint(),
331            TimerPolicySnapshot::try_new(500, 3_000).unwrap().fingerprint()
332        );
333        assert_ne!(
334            baseline.fingerprint(),
335            TimerPolicySnapshot::try_new(1_000, 4_000).unwrap().fingerprint()
336        );
337    }
338}