offline_protocol_reliability/
ack_manager.rs1use crate::constants::{DEFAULT_ACK_TIMEOUT_MS, DEFAULT_MAX_PENDING_ACKS};
4use chrono::{DateTime, Utc};
5use offline_protocol_core::{MessageId, MessagePriority};
6use std::collections::HashMap;
7use std::time::Duration;
8
9#[derive(Debug, Clone)]
11#[non_exhaustive]
12pub struct AckEvictionInfo {
13 pub message_id: MessageId,
15 pub priority: MessagePriority,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum AckStatus {
25 Pending,
27 TimedOut,
29}
30
31#[derive(Debug, Clone)]
33pub struct PendingAck {
34 pub message_id: MessageId,
36
37 pub sent_at: DateTime<Utc>,
39
40 pub timeout_ms: u64,
42
43 pub status: AckStatus,
45
46 pub retry_count: u32,
48
49 pub priority: MessagePriority,
51
52 pub extended_ms: u64,
57}
58
59impl PendingAck {
60 pub fn is_timed_out(&self) -> bool {
62 if self.status != AckStatus::Pending {
63 return false;
64 }
65
66 let elapsed = Utc::now().signed_duration_since(self.sent_at);
67 elapsed.num_milliseconds() >= self.timeout_ms.saturating_add(self.extended_ms) as i64
68 }
69}
70
71#[derive(Debug, Clone)]
73pub struct AckConfig {
74 pub default_timeout_ms: u64,
76
77 pub max_pending_acks: usize,
79}
80
81impl Default for AckConfig {
82 fn default() -> Self {
83 Self {
84 default_timeout_ms: DEFAULT_ACK_TIMEOUT_MS,
85 max_pending_acks: DEFAULT_MAX_PENDING_ACKS,
86 }
87 }
88}
89
90pub struct AckManager {
92 config: AckConfig,
93 pending_acks: HashMap<MessageId, PendingAck>,
94}
95
96impl AckManager {
97 pub fn new() -> Self {
99 Self::with_config(AckConfig::default())
100 }
101
102 pub fn with_config(config: AckConfig) -> Self {
104 Self {
105 config,
106 pending_acks: HashMap::new(),
107 }
108 }
109
110 pub fn register_pending_ack(
123 &mut self,
124 message_id: MessageId,
125 timeout_ms: Option<u64>,
126 ) -> crate::Result<Option<AckEvictionInfo>> {
127 self.register_pending_ack_with_priority(message_id, timeout_ms, MessagePriority::Medium)
128 }
129
130 pub fn register_pending_ack_with_priority(
144 &mut self,
145 message_id: MessageId,
146 timeout_ms: Option<u64>,
147 priority: MessagePriority,
148 ) -> crate::Result<Option<AckEvictionInfo>> {
149 let mut eviction = None;
159 if !self.has_capacity_for(&message_id) {
160 match self.evict_lowest_priority(priority) {
161 Some(info) => eviction = Some(info),
162 None => {
163 return Err(crate::Error::AckCapacity {
164 max: self.config.max_pending_acks,
165 });
166 }
167 }
168 }
169
170 let pending = PendingAck {
171 message_id: message_id.clone(),
172 sent_at: Utc::now(),
173 timeout_ms: timeout_ms.unwrap_or(self.config.default_timeout_ms),
174 status: AckStatus::Pending,
175 retry_count: 0,
176 priority,
177 extended_ms: 0,
178 };
179
180 self.pending_acks.insert(message_id, pending);
181 Ok(eviction)
182 }
183
184 fn evict_lowest_priority(&mut self, new_priority: MessagePriority) -> Option<AckEvictionInfo> {
187 let evict_candidate = self
189 .pending_acks
190 .iter()
191 .filter(|(_, ack)| ack.status == AckStatus::Pending)
192 .min_by(|(_, a), (_, b)| {
193 let priority_cmp =
195 Self::priority_rank(a.priority).cmp(&Self::priority_rank(b.priority));
196 if priority_cmp != std::cmp::Ordering::Equal {
197 return priority_cmp;
198 }
199 a.sent_at.cmp(&b.sent_at)
201 })
202 .map(|(id, ack)| (id.clone(), ack.priority));
203
204 if let Some((evict_id, evict_priority)) = evict_candidate {
205 if Self::priority_rank(evict_priority) < Self::priority_rank(new_priority) {
207 self.pending_acks.remove(&evict_id);
208 return Some(AckEvictionInfo {
209 message_id: evict_id,
210 priority: evict_priority,
211 });
212 }
213 }
214
215 None
216 }
217
218 fn priority_rank(priority: MessagePriority) -> u8 {
220 match priority {
221 MessagePriority::Low => 0,
222 MessagePriority::Medium => 1,
223 MessagePriority::High => 2,
224 MessagePriority::Critical => 3,
225 }
226 }
227
228 pub fn drain_timed_out(&mut self) -> Vec<PendingAck> {
238 let mut timed_out = Vec::new();
239 for pending in self.pending_acks.values_mut() {
240 if pending.is_timed_out() {
241 pending.status = AckStatus::TimedOut;
242 timed_out.push(pending.clone());
243 }
244 }
245 timed_out
246 }
247
248 pub fn remove_ack(&mut self, message_id: &MessageId) -> Option<PendingAck> {
250 self.pending_acks.remove(message_id)
251 }
252
253 pub fn get_timed_out_acks(&self) -> Vec<MessageId> {
259 self.pending_acks
260 .values()
261 .filter(|ack| ack.is_timed_out())
262 .map(|ack| ack.message_id.clone())
263 .collect()
264 }
265
266 pub fn increment_retry_count(&mut self, message_id: &MessageId) {
268 if let Some(pending) = self.pending_acks.get_mut(message_id) {
269 pending.retry_count += 1;
270 pending.sent_at = Utc::now(); pending.extended_ms = 0;
272 pending.status = AckStatus::Pending;
273 }
274 }
275
276 pub fn extend_while_queued(&mut self, message_id: &MessageId, max_wait: Duration) -> bool {
289 let Some(pending) = self.pending_acks.get_mut(message_id) else {
290 return false;
291 };
292 let elapsed_ms = Utc::now()
293 .signed_duration_since(pending.sent_at)
294 .num_milliseconds()
295 .max(0) as u64;
296 if elapsed_ms >= max_wait.as_millis().min(u128::from(u64::MAX)) as u64 {
297 return false;
298 }
299 pending.extended_ms = elapsed_ms;
300 pending.status = AckStatus::Pending;
301 true
302 }
303
304 pub fn set_retry_count(&mut self, message_id: &MessageId, retry_count: u32) {
315 if let Some(pending) = self.pending_acks.get_mut(message_id) {
316 pending.retry_count = retry_count;
317 }
318 }
319
320 pub fn get_pending_ack(&self, message_id: &MessageId) -> Option<&PendingAck> {
322 self.pending_acks.get(message_id)
323 }
324
325 pub fn pending_count(&self) -> usize {
327 self.pending_acks.len()
328 }
329
330 pub fn has_capacity_for(&self, message_id: &MessageId) -> bool {
337 self.pending_acks.contains_key(message_id)
338 || self.pending_acks.len() < self.config.max_pending_acks
339 }
340
341 pub fn is_waiting_for_ack(&self, message_id: &MessageId) -> bool {
343 self.pending_acks.contains_key(message_id)
344 }
345
346 pub fn prune_old_timeouts(&mut self, older_than: Duration) {
350 let now = Utc::now();
351 let cutoff = now
352 - chrono::Duration::from_std(older_than).unwrap_or_else(|_| chrono::Duration::zero());
353
354 self.pending_acks
355 .retain(|_, ack| ack.status != AckStatus::TimedOut || ack.sent_at > cutoff);
356 }
357}
358
359impl Default for AckManager {
360 fn default() -> Self {
361 Self::new()
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368 use std::thread;
369
370 #[test]
371 fn test_register_and_receive_ack() {
372 let mut manager = AckManager::new();
373 let msg_id = MessageId::new();
374
375 manager.register_pending_ack(msg_id.clone(), None).unwrap();
377 assert!(manager.is_waiting_for_ack(&msg_id));
378 assert_eq!(manager.pending_count(), 1);
379
380 let removed = manager.remove_ack(&msg_id).unwrap();
382 assert_eq!(removed.message_id, msg_id);
383 assert!(!manager.is_waiting_for_ack(&msg_id));
384 assert_eq!(manager.pending_count(), 0);
385 }
386
387 #[test]
388 fn test_ack_timeout() {
389 let config = AckConfig {
390 default_timeout_ms: 50, max_pending_acks: 100,
392 };
393 let mut manager = AckManager::with_config(config);
394 let msg_id = MessageId::new();
395
396 manager
397 .register_pending_ack(msg_id.clone(), Some(50))
398 .unwrap();
399
400 assert!(manager.drain_timed_out().is_empty());
402
403 thread::sleep(Duration::from_millis(60));
405
406 let timed_out = manager.drain_timed_out();
408 assert_eq!(timed_out.len(), 1);
409 assert_eq!(timed_out[0].message_id, msg_id);
410 }
411
412 #[test]
413 fn test_retry_count_increment() {
414 let mut manager = AckManager::new();
415 let msg_id = MessageId::new();
416
417 manager.register_pending_ack(msg_id.clone(), None).unwrap();
418
419 let pending = manager.get_pending_ack(&msg_id).unwrap();
420 assert_eq!(pending.retry_count, 0);
421
422 manager.increment_retry_count(&msg_id);
423
424 let pending = manager.get_pending_ack(&msg_id).unwrap();
425 assert_eq!(pending.retry_count, 1);
426 }
427
428 #[test]
429 fn test_set_retry_count_carries_backoff_forward() {
430 let mut manager = AckManager::new();
438 let msg_id = MessageId::new();
439
440 manager.register_pending_ack(msg_id.clone(), None).unwrap();
442 assert_eq!(manager.get_pending_ack(&msg_id).unwrap().retry_count, 0);
443
444 manager.set_retry_count(&msg_id, 5);
446 assert_eq!(manager.get_pending_ack(&msg_id).unwrap().retry_count, 5);
447
448 manager.set_retry_count(&msg_id, 7);
450 assert_eq!(manager.get_pending_ack(&msg_id).unwrap().retry_count, 7);
451
452 manager.set_retry_count(&MessageId::new(), 9);
454 }
455
456 #[test]
457 fn test_remove_ack() {
458 let mut manager = AckManager::new();
459 let msg_id = MessageId::new();
460
461 manager.register_pending_ack(msg_id.clone(), None).unwrap();
462 assert!(manager.is_waiting_for_ack(&msg_id));
463
464 manager.remove_ack(&msg_id);
465 assert!(!manager.is_waiting_for_ack(&msg_id));
466 }
467
468 #[test]
469 fn test_max_pending_limit() {
470 let config = AckConfig {
471 default_timeout_ms: 5000,
472 max_pending_acks: 3,
473 };
474 let mut manager = AckManager::with_config(config);
475
476 manager
478 .register_pending_ack(MessageId::new(), None)
479 .unwrap();
480 manager
481 .register_pending_ack(MessageId::new(), None)
482 .unwrap();
483 manager
484 .register_pending_ack(MessageId::new(), None)
485 .unwrap();
486
487 let result = manager.register_pending_ack(MessageId::new(), None);
490 assert!(matches!(result, Err(crate::Error::AckCapacity { max: 3 })));
491 }
492
493 #[test]
494 fn has_capacity_for_admits_a_tracked_id_at_the_ceiling() {
495 let mut manager = AckManager::with_config(AckConfig {
496 default_timeout_ms: 5000,
497 max_pending_acks: 1,
498 });
499 let tracked = MessageId::new();
500 assert!(manager.has_capacity_for(&tracked));
501 manager.register_pending_ack(tracked.clone(), None).unwrap();
502 assert!(manager.has_capacity_for(&tracked));
504 assert!(!manager.has_capacity_for(&MessageId::new()));
506 }
507
508 #[test]
509 fn extend_while_queued_grants_one_more_timeout_within_the_bound() {
510 let mut manager = AckManager::with_config(AckConfig {
511 default_timeout_ms: 10,
512 max_pending_acks: 10,
513 });
514 let id = MessageId::new();
515 manager.register_pending_ack(id.clone(), Some(10)).unwrap();
516 thread::sleep(Duration::from_millis(15));
517 assert_eq!(manager.drain_timed_out().len(), 1);
518
519 assert!(manager.extend_while_queued(&id, Duration::from_secs(60)));
520 assert!(
521 manager.drain_timed_out().is_empty(),
522 "extended for one more timeout"
523 );
524 thread::sleep(Duration::from_millis(15));
525 assert_eq!(manager.drain_timed_out().len(), 1, "and only one");
526
527 assert!(!manager.extend_while_queued(&id, Duration::from_millis(1)));
529 manager.increment_retry_count(&id);
531 assert_eq!(manager.get_pending_ack(&id).unwrap().extended_ms, 0);
532 }
533
534 #[test]
539 fn a_refused_registration_leaves_timed_out_entries_for_the_sweep() {
540 let mut manager = AckManager::with_config(AckConfig {
541 default_timeout_ms: 10,
542 max_pending_acks: 2,
543 });
544 let first = MessageId::new();
545 let second = MessageId::new();
546 manager
547 .register_pending_ack(first.clone(), Some(10))
548 .unwrap();
549 manager
550 .register_pending_ack(second.clone(), Some(10))
551 .unwrap();
552 thread::sleep(Duration::from_millis(20));
553
554 assert!(manager
555 .register_pending_ack(MessageId::new(), None)
556 .is_err());
557
558 let mut drained: Vec<_> = manager
559 .drain_timed_out()
560 .into_iter()
561 .map(|ack| ack.message_id)
562 .collect();
563 drained.sort_by_key(|id| id.as_str());
564 let mut expected = vec![first, second];
565 expected.sort_by_key(|id| id.as_str());
566 assert_eq!(drained, expected);
567 }
568
569 #[test]
570 fn test_priority_based_eviction() {
571 let config = AckConfig {
572 default_timeout_ms: 5000,
573 max_pending_acks: 2,
574 };
575 let mut manager = AckManager::with_config(config);
576
577 let low_id = MessageId::new();
578 let medium_id = MessageId::new();
579
580 manager
582 .register_pending_ack_with_priority(low_id.clone(), None, MessagePriority::Low)
583 .unwrap();
584 manager
586 .register_pending_ack_with_priority(medium_id.clone(), None, MessagePriority::Medium)
587 .unwrap();
588
589 assert_eq!(manager.pending_count(), 2);
590
591 let high_id = MessageId::new();
593 manager
594 .register_pending_ack_with_priority(high_id.clone(), None, MessagePriority::High)
595 .unwrap();
596
597 assert_eq!(manager.pending_count(), 2);
598 assert!(!manager.is_waiting_for_ack(&low_id)); assert!(manager.is_waiting_for_ack(&medium_id)); assert!(manager.is_waiting_for_ack(&high_id)); }
602
603 #[test]
604 fn test_critical_priority_evicts_all() {
605 let config = AckConfig {
606 default_timeout_ms: 5000,
607 max_pending_acks: 1,
608 };
609 let mut manager = AckManager::with_config(config);
610
611 let medium_id = MessageId::new();
612 manager
613 .register_pending_ack_with_priority(medium_id.clone(), None, MessagePriority::Medium)
614 .unwrap();
615
616 let critical_id = MessageId::new();
618 manager
619 .register_pending_ack_with_priority(
620 critical_id.clone(),
621 None,
622 MessagePriority::Critical,
623 )
624 .unwrap();
625
626 assert_eq!(manager.pending_count(), 1);
627 assert!(!manager.is_waiting_for_ack(&medium_id));
628 assert!(manager.is_waiting_for_ack(&critical_id));
629 }
630
631 #[test]
632 fn test_prune_old_timeouts() {
633 let config = AckConfig {
634 default_timeout_ms: 10,
635 max_pending_acks: 100,
636 };
637 let mut manager = AckManager::with_config(config);
638
639 let msg_id = MessageId::new();
640 manager
641 .register_pending_ack(msg_id.clone(), Some(10))
642 .unwrap();
643
644 thread::sleep(Duration::from_millis(20));
646 assert_eq!(manager.drain_timed_out().len(), 1);
647
648 assert_eq!(manager.pending_count(), 1);
649
650 manager.prune_old_timeouts(Duration::from_millis(5));
652 assert_eq!(manager.pending_count(), 0);
653 }
654
655 #[test]
656 fn test_ack_not_found() {
657 let mut manager = AckManager::new();
658 let msg_id = MessageId::new();
659
660 assert!(manager.remove_ack(&msg_id).is_none());
662 }
663
664 #[test]
665 fn test_eviction_returned_from_register() {
666 let config = AckConfig {
667 default_timeout_ms: 5000,
668 max_pending_acks: 2,
669 };
670 let mut manager = AckManager::with_config(config);
671
672 let low_id = MessageId::new();
673 assert!(manager
675 .register_pending_ack_with_priority(low_id.clone(), None, MessagePriority::Low)
676 .unwrap()
677 .is_none());
678 assert!(manager
679 .register_pending_ack_with_priority(MessageId::new(), None, MessagePriority::Medium)
680 .unwrap()
681 .is_none());
682
683 let eviction = manager
685 .register_pending_ack_with_priority(MessageId::new(), None, MessagePriority::High)
686 .unwrap();
687 let eviction = eviction.expect("should have evicted the low-priority ACK");
688 assert_eq!(eviction.message_id, low_id);
689 assert_eq!(eviction.priority, MessagePriority::Low);
690 }
691}