rocketmq_model/common/message/
timer_request.rs1use std::collections::HashMap;
16
17use cheetah_string::CheetahString;
18
19use crate::common::message::MessageConst;
20use crate::ModelContractViolation;
21
22pub const JAVA_COMPAT_TIMER_PRECISIONS_MS: [u64; 4] = [100, 200, 500, 1_000];
24
25#[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 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 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
87pub enum TimerRequestKind {
88 DelaySeconds,
89 DelayMilliseconds,
90 DeliverAtMilliseconds,
91}
92
93#[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
101pub 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
131pub 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}