offline_protocol_reliability/
retry_queue.rs1use crate::constants::{
4 DEFAULT_BACKOFF_MULTIPLIER, DEFAULT_INITIAL_DELAY_MS, DEFAULT_MAX_DELAY_MS,
5 DEFAULT_MAX_RETRIES, DEFAULT_OUTBOX_LIFETIME_MS, DEFAULT_PENDING_MESSAGE_LIFETIME_MS,
6};
7use chrono::{DateTime, Utc};
8use offline_protocol_core::{Message, MessagePriority};
9use std::cmp::Ordering;
10use std::collections::{BinaryHeap, HashMap};
11
12#[derive(Debug, Clone)]
14pub struct RetryConfig {
15 pub max_retries: u32,
17
18 pub initial_delay_ms: u64,
20
21 pub max_delay_ms: u64,
23
24 pub backoff_multiplier: f32,
26
27 pub outbox_max_lifetime_ms: u64,
29
30 pub pending_message_max_lifetime_ms: u64,
32}
33
34impl Default for RetryConfig {
35 fn default() -> Self {
36 Self {
37 max_retries: DEFAULT_MAX_RETRIES,
38 initial_delay_ms: DEFAULT_INITIAL_DELAY_MS,
39 max_delay_ms: DEFAULT_MAX_DELAY_MS,
40 backoff_multiplier: DEFAULT_BACKOFF_MULTIPLIER,
41 outbox_max_lifetime_ms: DEFAULT_OUTBOX_LIFETIME_MS,
42 pending_message_max_lifetime_ms: DEFAULT_PENDING_MESSAGE_LIFETIME_MS,
43 }
44 }
45}
46
47impl RetryConfig {
48 pub fn delay_for_retry(&self, retry_count: u32) -> u64 {
57 let exponent = i32::try_from(retry_count).unwrap_or(i32::MAX);
58 let factor = f64::from(self.backoff_multiplier).powi(exponent);
59 let delay_ms = self.initial_delay_ms as f64 * factor;
60 delay_ms.min(self.max_delay_ms as f64) as u64
61 }
62}
63
64#[derive(Debug, Clone)]
66pub struct RetryEntry {
67 pub message: Message,
69
70 pub retry_count: u32,
72
73 pub added_at: DateTime<Utc>,
75
76 pub retry_at: DateTime<Utc>,
78}
79
80impl RetryEntry {
81 pub fn is_ready(&self) -> bool {
83 Utc::now() >= self.retry_at
84 }
85
86 pub fn is_expired(&self, max_lifetime_ms: u64) -> bool {
88 let elapsed = Utc::now().signed_duration_since(self.added_at);
89 elapsed.num_milliseconds() >= max_lifetime_ms as i64
90 }
91}
92
93impl Ord for RetryEntry {
95 fn cmp(&self, other: &Self) -> Ordering {
96 other.retry_at.cmp(&self.retry_at).then_with(|| {
98 self.message.priority.cmp(&other.message.priority)
101 })
102 }
103}
104
105impl PartialOrd for RetryEntry {
106 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
107 Some(self.cmp(other))
108 }
109}
110
111impl Eq for RetryEntry {}
112
113impl PartialEq for RetryEntry {
114 fn eq(&self, other: &Self) -> bool {
115 self.retry_at == other.retry_at && self.message.priority == other.message.priority
116 }
117}
118
119pub struct RetryQueue {
121 config: RetryConfig,
122 queue: BinaryHeap<RetryEntry>,
123 index: HashMap<String, ()>, }
125
126impl RetryQueue {
127 pub fn new() -> Self {
129 Self::with_config(RetryConfig::default())
130 }
131
132 pub fn with_config(config: RetryConfig) -> Self {
134 Self {
135 config,
136 queue: BinaryHeap::new(),
137 index: HashMap::new(),
138 }
139 }
140
141 pub fn enqueue(&mut self, message: Message, retry_count: u32) -> Option<DateTime<Utc>> {
159 let delay_ms = self.config.delay_for_retry(retry_count);
160 self.enqueue_with_delay(message, retry_count, delay_ms)
161 }
162
163 pub fn enqueue_with_delay(
173 &mut self,
174 message: Message,
175 retry_count: u32,
176 delay_ms: u64,
177 ) -> Option<DateTime<Utc>> {
178 if self.index.contains_key(&message.id.as_str()) {
180 return None;
181 }
182
183 let retry_at =
184 Utc::now() + chrono::Duration::milliseconds(delay_ms.min(i64::MAX as u64) as i64);
185
186 let entry = RetryEntry {
187 message: message.clone(),
188 retry_count,
189 added_at: Utc::now(),
190 retry_at,
191 };
192
193 self.index.insert(message.id.as_str(), ());
194 self.queue.push(entry);
195 Some(retry_at)
196 }
197
198 pub fn dequeue_ready(&mut self) -> Option<RetryEntry> {
204 let is_ready = self.queue.peek().is_some_and(|entry| entry.is_ready());
206 if is_ready {
207 if let Some(entry) = self.queue.pop() {
208 self.index.remove(&entry.message.id.as_str());
209 return Some(entry);
210 }
211 }
212 None
213 }
214
215 pub fn peek_ready(&self) -> Vec<&RetryEntry> {
217 self.queue.iter().filter(|entry| entry.is_ready()).collect()
218 }
219
220 pub fn remove(&mut self, message_id: &str) -> bool {
222 if self.index.remove(message_id).is_some() {
223 self.queue
225 .retain(|entry| entry.message.id.as_str() != message_id);
226 true
227 } else {
228 false
229 }
230 }
231
232 pub fn cleanup_expired(&mut self) -> usize {
238 let max_lifetime = self.config.outbox_max_lifetime_ms;
239
240 let before_count = self.queue.len();
241
242 let new_queue: BinaryHeap<RetryEntry> = self
244 .queue
245 .iter()
246 .filter(|entry| !entry.is_expired(max_lifetime))
247 .cloned()
248 .collect();
249
250 self.index.clear();
252 for entry in &new_queue {
253 self.index.insert(entry.message.id.as_str(), ());
254 }
255
256 self.queue = new_queue;
257
258 before_count - self.queue.len()
259 }
260
261 pub fn len(&self) -> usize {
263 self.queue.len()
264 }
265
266 pub fn is_empty(&self) -> bool {
268 self.queue.is_empty()
269 }
270
271 pub fn contains(&self, message_id: &str) -> bool {
273 self.index.contains_key(message_id)
274 }
275
276 pub fn drain_all(&mut self) -> Vec<RetryEntry> {
282 self.index.clear();
283 let mut entries = Vec::with_capacity(self.queue.len());
284 while let Some(entry) = self.queue.pop() {
285 entries.push(entry);
286 }
287 entries
288 }
289
290 pub fn time_until_next_retry(&self) -> Option<chrono::Duration> {
296 self.queue.peek().map(|entry| {
297 let now = Utc::now();
298 if entry.retry_at > now {
299 entry.retry_at - now
300 } else {
301 chrono::Duration::zero()
302 }
303 })
304 }
305
306 pub fn stats(&self) -> RetryQueueStats {
308 let ready_count = self.peek_ready().len();
309 let priority_counts = self.count_by_priority();
310
311 RetryQueueStats {
312 total_count: self.len(),
313 ready_count,
314 critical_priority_count: priority_counts[&MessagePriority::Critical],
315 high_priority_count: priority_counts[&MessagePriority::High],
316 medium_priority_count: priority_counts[&MessagePriority::Medium],
317 low_priority_count: priority_counts[&MessagePriority::Low],
318 }
319 }
320
321 fn count_by_priority(&self) -> HashMap<MessagePriority, usize> {
322 let mut counts = HashMap::new();
323 counts.insert(MessagePriority::Low, 0);
324 counts.insert(MessagePriority::Medium, 0);
325 counts.insert(MessagePriority::High, 0);
326 counts.insert(MessagePriority::Critical, 0);
327
328 for entry in &self.queue {
329 *counts.entry(entry.message.priority).or_insert(0) += 1;
330 }
331
332 counts
333 }
334}
335
336impl Default for RetryQueue {
337 fn default() -> Self {
338 Self::new()
339 }
340}
341
342#[derive(Debug, Clone)]
344pub struct RetryQueueStats {
345 pub total_count: usize,
347 pub ready_count: usize,
349 pub critical_priority_count: usize,
351 pub high_priority_count: usize,
353 pub medium_priority_count: usize,
355 pub low_priority_count: usize,
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362 use offline_protocol_core::{AppId, UserId};
363 use std::thread;
364 use std::time::Duration;
365
366 fn create_test_message(priority: MessagePriority) -> Message {
367 Message::builder(
368 UserId::new("alice").unwrap(),
369 UserId::new("bob").unwrap(),
370 AppId::new("test").unwrap(),
371 )
372 .content("Test message")
373 .priority(priority)
374 .build()
375 }
376
377 #[test]
378 fn test_enqueue_and_dequeue() {
379 let config = RetryConfig {
380 initial_delay_ms: 50, ..Default::default()
382 };
383 let mut queue = RetryQueue::with_config(config);
384
385 let msg = create_test_message(MessagePriority::Medium);
386 queue.enqueue(msg.clone(), 0);
387
388 assert_eq!(queue.len(), 1);
389 assert!(queue.contains(&msg.id.as_str()));
390
391 assert!(queue.dequeue_ready().is_none());
393
394 thread::sleep(Duration::from_millis(60));
396
397 let dequeued = queue.dequeue_ready().unwrap();
399 assert_eq!(dequeued.message.id, msg.id);
400 assert_eq!(queue.len(), 0);
401 }
402
403 #[test]
404 fn test_exponential_backoff() {
405 let config = RetryConfig {
406 initial_delay_ms: 100,
407 backoff_multiplier: 2.0,
408 max_delay_ms: 1000,
409 ..Default::default()
410 };
411
412 assert_eq!(config.delay_for_retry(0), 100);
413 assert_eq!(config.delay_for_retry(1), 200);
414 assert_eq!(config.delay_for_retry(2), 400);
415 assert_eq!(config.delay_for_retry(3), 800);
416 assert_eq!(config.delay_for_retry(4), 1000);
418 assert_eq!(config.delay_for_retry(100), 1000);
419 }
420
421 #[test]
422 fn test_backoff_extreme_multiplier_clamps_not_overflows() {
423 for multiplier in [7.0, 1000.0, f32::MAX] {
426 let config = RetryConfig {
427 initial_delay_ms: 1000,
428 backoff_multiplier: multiplier,
429 max_delay_ms: 30_000,
430 ..Default::default()
431 };
432
433 for retry_count in [1, 20, 1000, u32::MAX] {
434 let delay = config.delay_for_retry(retry_count);
435 assert!(
436 delay <= 30_000,
437 "multiplier {multiplier} retry {retry_count} gave delay {delay}"
438 );
439 }
440 }
441 }
442
443 #[test]
444 fn test_backoff_non_finite_multiplier_collapses_to_max_delay() {
445 for multiplier in [f32::NAN, f32::INFINITY] {
448 let config = RetryConfig {
449 initial_delay_ms: 1000,
450 backoff_multiplier: multiplier,
451 max_delay_ms: 30_000,
452 ..Default::default()
453 };
454
455 for retry_count in [1, 20, u32::MAX] {
456 assert_eq!(
457 config.delay_for_retry(retry_count),
458 30_000,
459 "multiplier {multiplier} retry {retry_count}"
460 );
461 }
462 }
463 }
464
465 #[test]
466 fn test_backoff_retry_zero_clamped_to_max_delay() {
467 let config = RetryConfig {
471 initial_delay_ms: 60_000,
472 backoff_multiplier: 2.0,
473 max_delay_ms: 30_000,
474 ..Default::default()
475 };
476
477 assert_eq!(config.delay_for_retry(0), 30_000);
478 }
479
480 #[test]
481 fn test_enqueue_backoff_clamped_to_max_delay() {
482 let config = RetryConfig {
485 initial_delay_ms: 1000,
486 backoff_multiplier: 10.0,
487 max_delay_ms: 30_000,
488 ..Default::default()
489 };
490 let mut queue = RetryQueue::with_config(config);
491
492 let msg = create_test_message(MessagePriority::Medium);
493 queue.enqueue(msg, 20);
494
495 let until_retry = queue.time_until_next_retry().unwrap();
496 assert!(until_retry <= chrono::Duration::milliseconds(30_000));
497 assert!(until_retry > chrono::Duration::zero());
498 }
499
500 #[test]
501 fn test_enqueue_accepts_high_retry_count() {
502 let config = RetryConfig {
503 max_retries: 2,
504 ..Default::default()
505 };
506 let mut queue = RetryQueue::with_config(config);
507
508 let msg = create_test_message(MessagePriority::Medium);
509
510 queue.enqueue(msg.clone(), 0);
513 queue.dequeue_ready(); queue.enqueue(msg.clone(), 1);
516 queue.dequeue_ready();
517
518 queue.enqueue(msg.clone(), 2);
520 queue.dequeue_ready();
521
522 queue.enqueue(msg.clone(), 100);
523 }
524
525 #[test]
526 fn test_drain_all() {
527 let config = RetryConfig {
528 initial_delay_ms: 60_000, ..Default::default()
530 };
531 let mut queue = RetryQueue::with_config(config);
532
533 let msg1 = create_test_message(MessagePriority::High);
534 let msg2 = create_test_message(MessagePriority::Low);
535 let msg3 = create_test_message(MessagePriority::Medium);
536
537 queue.enqueue(msg1.clone(), 0);
538 queue.enqueue(msg2.clone(), 0);
539 queue.enqueue(msg3.clone(), 0);
540
541 assert!(queue.dequeue_ready().is_none());
543 assert_eq!(queue.len(), 3);
544
545 let entries = queue.drain_all();
548 assert_eq!(entries.len(), 3);
549 assert!(queue.is_empty());
550 assert!(!queue.contains(&msg1.id.as_str()));
551 }
552
553 #[test]
554 fn test_priority_ordering() {
555 let config = RetryConfig {
556 initial_delay_ms: 50,
557 ..Default::default()
558 };
559 let mut queue = RetryQueue::with_config(config);
560
561 let low = create_test_message(MessagePriority::Low);
563 let high = create_test_message(MessagePriority::High);
564 let medium = create_test_message(MessagePriority::Medium);
565
566 queue.enqueue(low.clone(), 0);
567 queue.enqueue(high.clone(), 0);
568 queue.enqueue(medium.clone(), 0);
569
570 thread::sleep(Duration::from_millis(60));
571
572 let ready = queue.peek_ready();
574 assert_eq!(ready.len(), 3);
575
576 let has_high = ready
578 .iter()
579 .any(|e| e.message.priority == MessagePriority::High);
580 assert!(has_high);
581 }
582
583 #[test]
584 fn test_priority_tiebreaker_ordering() {
585 let now = Utc::now();
588 let make_entry = |priority: MessagePriority| {
589 let mut msg = create_test_message(priority);
590 msg.priority = priority;
592 RetryEntry {
593 message: msg,
594 retry_count: 0,
595 added_at: now,
596 retry_at: now,
597 }
598 };
599
600 let mut heap = BinaryHeap::new();
601 heap.push(make_entry(MessagePriority::Low));
602 heap.push(make_entry(MessagePriority::High));
603 heap.push(make_entry(MessagePriority::Medium));
604
605 assert_eq!(heap.pop().unwrap().message.priority, MessagePriority::High);
606 assert_eq!(
607 heap.pop().unwrap().message.priority,
608 MessagePriority::Medium
609 );
610 assert_eq!(heap.pop().unwrap().message.priority, MessagePriority::Low);
611 }
612
613 #[test]
614 fn test_enqueue_with_delay_uses_explicit_delay() {
615 let mut queue = RetryQueue::new();
618 let msg = create_test_message(MessagePriority::Medium);
619
620 let before = Utc::now();
621 let retry_at = queue.enqueue_with_delay(msg, 0, 60_000).unwrap();
622
623 let delay = retry_at - before;
624 assert!(delay >= chrono::Duration::milliseconds(59_000));
625 assert!(delay <= chrono::Duration::milliseconds(61_000));
626 }
627
628 #[test]
629 fn test_enqueue_with_delay_ignores_duplicates() {
630 let mut queue = RetryQueue::new();
631 let msg = create_test_message(MessagePriority::Medium);
632
633 assert!(queue.enqueue_with_delay(msg.clone(), 0, 1000).is_some());
634 assert!(queue.enqueue_with_delay(msg.clone(), 0, 2000).is_none());
635 assert!(queue.enqueue(msg, 0).is_none());
636 assert_eq!(queue.len(), 1);
637 }
638
639 #[test]
640 fn test_remove_message() {
641 let mut queue = RetryQueue::new();
642 let msg = create_test_message(MessagePriority::Medium);
643
644 queue.enqueue(msg.clone(), 0);
645 assert!(queue.contains(&msg.id.as_str()));
646
647 assert!(queue.remove(&msg.id.as_str()));
648 assert!(!queue.contains(&msg.id.as_str()));
649 assert_eq!(queue.len(), 0);
650 }
651
652 #[test]
653 fn test_cleanup_expired() {
654 let config = RetryConfig {
655 outbox_max_lifetime_ms: 100, ..Default::default()
657 };
658 let mut queue = RetryQueue::with_config(config);
659
660 let msg = create_test_message(MessagePriority::Medium);
661 queue.enqueue(msg, 0);
662
663 thread::sleep(Duration::from_millis(150));
665
666 let removed = queue.cleanup_expired();
667 assert_eq!(removed, 1);
668 assert_eq!(queue.len(), 0);
669 }
670
671 #[test]
672 fn test_queue_stats() {
673 let config = RetryConfig {
674 initial_delay_ms: 50,
675 ..Default::default()
676 };
677 let mut queue = RetryQueue::with_config(config);
678
679 queue.enqueue(create_test_message(MessagePriority::High), 0);
680 queue.enqueue(create_test_message(MessagePriority::High), 0);
681 queue.enqueue(create_test_message(MessagePriority::Medium), 0);
682 queue.enqueue(create_test_message(MessagePriority::Low), 0);
683
684 let stats = queue.stats();
685 assert_eq!(stats.total_count, 4);
686 assert_eq!(stats.high_priority_count, 2);
687 assert_eq!(stats.medium_priority_count, 1);
688 assert_eq!(stats.low_priority_count, 1);
689 }
690}