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
53impl PendingAck {
54 pub fn is_timed_out(&self) -> bool {
56 if self.status != AckStatus::Pending {
57 return false;
58 }
59
60 let elapsed = Utc::now().signed_duration_since(self.sent_at);
61 elapsed.num_milliseconds() >= self.timeout_ms as i64
62 }
63}
64
65#[derive(Debug, Clone)]
67pub struct AckConfig {
68 pub default_timeout_ms: u64,
70
71 pub max_pending_acks: usize,
73}
74
75impl Default for AckConfig {
76 fn default() -> Self {
77 Self {
78 default_timeout_ms: DEFAULT_ACK_TIMEOUT_MS,
79 max_pending_acks: DEFAULT_MAX_PENDING_ACKS,
80 }
81 }
82}
83
84pub struct AckManager {
86 config: AckConfig,
87 pending_acks: HashMap<MessageId, PendingAck>,
88}
89
90impl AckManager {
91 pub fn new() -> Self {
93 Self::with_config(AckConfig::default())
94 }
95
96 pub fn with_config(config: AckConfig) -> Self {
98 Self {
99 config,
100 pending_acks: HashMap::new(),
101 }
102 }
103
104 pub fn register_pending_ack(
117 &mut self,
118 message_id: MessageId,
119 timeout_ms: Option<u64>,
120 ) -> crate::Result<Option<AckEvictionInfo>> {
121 self.register_pending_ack_with_priority(message_id, timeout_ms, MessagePriority::Medium)
122 }
123
124 pub fn register_pending_ack_with_priority(
138 &mut self,
139 message_id: MessageId,
140 timeout_ms: Option<u64>,
141 priority: MessagePriority,
142 ) -> crate::Result<Option<AckEvictionInfo>> {
143 let mut eviction = None;
145 if self.pending_acks.len() >= self.config.max_pending_acks {
146 self.cleanup_timed_out();
148
149 if self.pending_acks.len() >= self.config.max_pending_acks {
151 match self.evict_lowest_priority(priority) {
152 Some(info) => eviction = Some(info),
153 None => {
154 return Err(crate::Error::Other(
155 "Maximum pending ACKs limit reached (no lower priority ACKs to evict)"
156 .to_string(),
157 ));
158 }
159 }
160 }
161 }
162
163 let pending = PendingAck {
164 message_id: message_id.clone(),
165 sent_at: Utc::now(),
166 timeout_ms: timeout_ms.unwrap_or(self.config.default_timeout_ms),
167 status: AckStatus::Pending,
168 retry_count: 0,
169 priority,
170 };
171
172 self.pending_acks.insert(message_id, pending);
173 Ok(eviction)
174 }
175
176 fn evict_lowest_priority(&mut self, new_priority: MessagePriority) -> Option<AckEvictionInfo> {
179 let evict_candidate = self
181 .pending_acks
182 .iter()
183 .filter(|(_, ack)| ack.status == AckStatus::Pending)
184 .min_by(|(_, a), (_, b)| {
185 let priority_cmp =
187 Self::priority_rank(a.priority).cmp(&Self::priority_rank(b.priority));
188 if priority_cmp != std::cmp::Ordering::Equal {
189 return priority_cmp;
190 }
191 a.sent_at.cmp(&b.sent_at)
193 })
194 .map(|(id, ack)| (id.clone(), ack.priority));
195
196 if let Some((evict_id, evict_priority)) = evict_candidate {
197 if Self::priority_rank(evict_priority) < Self::priority_rank(new_priority) {
199 self.pending_acks.remove(&evict_id);
200 return Some(AckEvictionInfo {
201 message_id: evict_id,
202 priority: evict_priority,
203 });
204 }
205 }
206
207 None
208 }
209
210 fn priority_rank(priority: MessagePriority) -> u8 {
212 match priority {
213 MessagePriority::Low => 0,
214 MessagePriority::Medium => 1,
215 MessagePriority::High => 2,
216 MessagePriority::Critical => 3,
217 }
218 }
219
220 pub fn drain_timed_out(&mut self) -> Vec<PendingAck> {
230 let mut timed_out = Vec::new();
231 for pending in self.pending_acks.values_mut() {
232 if pending.is_timed_out() {
233 pending.status = AckStatus::TimedOut;
234 timed_out.push(pending.clone());
235 }
236 }
237 timed_out
238 }
239
240 pub fn remove_ack(&mut self, message_id: &MessageId) -> Option<PendingAck> {
242 self.pending_acks.remove(message_id)
243 }
244
245 pub fn get_timed_out_acks(&self) -> Vec<MessageId> {
251 self.pending_acks
252 .values()
253 .filter(|ack| ack.is_timed_out())
254 .map(|ack| ack.message_id.clone())
255 .collect()
256 }
257
258 pub fn increment_retry_count(&mut self, message_id: &MessageId) {
260 if let Some(pending) = self.pending_acks.get_mut(message_id) {
261 pending.retry_count += 1;
262 pending.sent_at = Utc::now(); pending.status = AckStatus::Pending;
264 }
265 }
266
267 pub fn set_retry_count(&mut self, message_id: &MessageId, retry_count: u32) {
278 if let Some(pending) = self.pending_acks.get_mut(message_id) {
279 pending.retry_count = retry_count;
280 }
281 }
282
283 pub fn get_pending_ack(&self, message_id: &MessageId) -> Option<&PendingAck> {
285 self.pending_acks.get(message_id)
286 }
287
288 pub fn pending_count(&self) -> usize {
290 self.pending_acks.len()
291 }
292
293 pub fn is_waiting_for_ack(&self, message_id: &MessageId) -> bool {
295 self.pending_acks.contains_key(message_id)
296 }
297
298 fn cleanup_timed_out(&mut self) {
300 self.drain_timed_out();
301 }
302
303 pub fn prune_old_timeouts(&mut self, older_than: Duration) {
307 let now = Utc::now();
308 let cutoff = now
309 - chrono::Duration::from_std(older_than).unwrap_or_else(|_| chrono::Duration::zero());
310
311 self.pending_acks
312 .retain(|_, ack| ack.status != AckStatus::TimedOut || ack.sent_at > cutoff);
313 }
314}
315
316impl Default for AckManager {
317 fn default() -> Self {
318 Self::new()
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325 use std::thread;
326
327 #[test]
328 fn test_register_and_receive_ack() {
329 let mut manager = AckManager::new();
330 let msg_id = MessageId::new();
331
332 manager.register_pending_ack(msg_id.clone(), None).unwrap();
334 assert!(manager.is_waiting_for_ack(&msg_id));
335 assert_eq!(manager.pending_count(), 1);
336
337 let removed = manager.remove_ack(&msg_id).unwrap();
339 assert_eq!(removed.message_id, msg_id);
340 assert!(!manager.is_waiting_for_ack(&msg_id));
341 assert_eq!(manager.pending_count(), 0);
342 }
343
344 #[test]
345 fn test_ack_timeout() {
346 let config = AckConfig {
347 default_timeout_ms: 50, max_pending_acks: 100,
349 };
350 let mut manager = AckManager::with_config(config);
351 let msg_id = MessageId::new();
352
353 manager
354 .register_pending_ack(msg_id.clone(), Some(50))
355 .unwrap();
356
357 assert!(manager.drain_timed_out().is_empty());
359
360 thread::sleep(Duration::from_millis(60));
362
363 let timed_out = manager.drain_timed_out();
365 assert_eq!(timed_out.len(), 1);
366 assert_eq!(timed_out[0].message_id, msg_id);
367 }
368
369 #[test]
370 fn test_retry_count_increment() {
371 let mut manager = AckManager::new();
372 let msg_id = MessageId::new();
373
374 manager.register_pending_ack(msg_id.clone(), None).unwrap();
375
376 let pending = manager.get_pending_ack(&msg_id).unwrap();
377 assert_eq!(pending.retry_count, 0);
378
379 manager.increment_retry_count(&msg_id);
380
381 let pending = manager.get_pending_ack(&msg_id).unwrap();
382 assert_eq!(pending.retry_count, 1);
383 }
384
385 #[test]
386 fn test_set_retry_count_carries_backoff_forward() {
387 let mut manager = AckManager::new();
395 let msg_id = MessageId::new();
396
397 manager.register_pending_ack(msg_id.clone(), None).unwrap();
399 assert_eq!(manager.get_pending_ack(&msg_id).unwrap().retry_count, 0);
400
401 manager.set_retry_count(&msg_id, 5);
403 assert_eq!(manager.get_pending_ack(&msg_id).unwrap().retry_count, 5);
404
405 manager.set_retry_count(&msg_id, 7);
407 assert_eq!(manager.get_pending_ack(&msg_id).unwrap().retry_count, 7);
408
409 manager.set_retry_count(&MessageId::new(), 9);
411 }
412
413 #[test]
414 fn test_remove_ack() {
415 let mut manager = AckManager::new();
416 let msg_id = MessageId::new();
417
418 manager.register_pending_ack(msg_id.clone(), None).unwrap();
419 assert!(manager.is_waiting_for_ack(&msg_id));
420
421 manager.remove_ack(&msg_id);
422 assert!(!manager.is_waiting_for_ack(&msg_id));
423 }
424
425 #[test]
426 fn test_max_pending_limit() {
427 let config = AckConfig {
428 default_timeout_ms: 5000,
429 max_pending_acks: 3,
430 };
431 let mut manager = AckManager::with_config(config);
432
433 manager
435 .register_pending_ack(MessageId::new(), None)
436 .unwrap();
437 manager
438 .register_pending_ack(MessageId::new(), None)
439 .unwrap();
440 manager
441 .register_pending_ack(MessageId::new(), None)
442 .unwrap();
443
444 let result = manager.register_pending_ack(MessageId::new(), None);
446 assert!(result.is_err());
447 }
448
449 #[test]
450 fn test_priority_based_eviction() {
451 let config = AckConfig {
452 default_timeout_ms: 5000,
453 max_pending_acks: 2,
454 };
455 let mut manager = AckManager::with_config(config);
456
457 let low_id = MessageId::new();
458 let medium_id = MessageId::new();
459
460 manager
462 .register_pending_ack_with_priority(low_id.clone(), None, MessagePriority::Low)
463 .unwrap();
464 manager
466 .register_pending_ack_with_priority(medium_id.clone(), None, MessagePriority::Medium)
467 .unwrap();
468
469 assert_eq!(manager.pending_count(), 2);
470
471 let high_id = MessageId::new();
473 manager
474 .register_pending_ack_with_priority(high_id.clone(), None, MessagePriority::High)
475 .unwrap();
476
477 assert_eq!(manager.pending_count(), 2);
478 assert!(!manager.is_waiting_for_ack(&low_id)); assert!(manager.is_waiting_for_ack(&medium_id)); assert!(manager.is_waiting_for_ack(&high_id)); }
482
483 #[test]
484 fn test_critical_priority_evicts_all() {
485 let config = AckConfig {
486 default_timeout_ms: 5000,
487 max_pending_acks: 1,
488 };
489 let mut manager = AckManager::with_config(config);
490
491 let medium_id = MessageId::new();
492 manager
493 .register_pending_ack_with_priority(medium_id.clone(), None, MessagePriority::Medium)
494 .unwrap();
495
496 let critical_id = MessageId::new();
498 manager
499 .register_pending_ack_with_priority(
500 critical_id.clone(),
501 None,
502 MessagePriority::Critical,
503 )
504 .unwrap();
505
506 assert_eq!(manager.pending_count(), 1);
507 assert!(!manager.is_waiting_for_ack(&medium_id));
508 assert!(manager.is_waiting_for_ack(&critical_id));
509 }
510
511 #[test]
512 fn test_prune_old_timeouts() {
513 let config = AckConfig {
514 default_timeout_ms: 10,
515 max_pending_acks: 100,
516 };
517 let mut manager = AckManager::with_config(config);
518
519 let msg_id = MessageId::new();
520 manager
521 .register_pending_ack(msg_id.clone(), Some(10))
522 .unwrap();
523
524 thread::sleep(Duration::from_millis(20));
526 manager.cleanup_timed_out();
527
528 assert_eq!(manager.pending_count(), 1);
529
530 manager.prune_old_timeouts(Duration::from_millis(5));
532 assert_eq!(manager.pending_count(), 0);
533 }
534
535 #[test]
536 fn test_ack_not_found() {
537 let mut manager = AckManager::new();
538 let msg_id = MessageId::new();
539
540 assert!(manager.remove_ack(&msg_id).is_none());
542 }
543
544 #[test]
545 fn test_eviction_returned_from_register() {
546 let config = AckConfig {
547 default_timeout_ms: 5000,
548 max_pending_acks: 2,
549 };
550 let mut manager = AckManager::with_config(config);
551
552 let low_id = MessageId::new();
553 assert!(manager
555 .register_pending_ack_with_priority(low_id.clone(), None, MessagePriority::Low)
556 .unwrap()
557 .is_none());
558 assert!(manager
559 .register_pending_ack_with_priority(MessageId::new(), None, MessagePriority::Medium)
560 .unwrap()
561 .is_none());
562
563 let eviction = manager
565 .register_pending_ack_with_priority(MessageId::new(), None, MessagePriority::High)
566 .unwrap();
567 let eviction = eviction.expect("should have evicted the low-priority ACK");
568 assert_eq!(eviction.message_id, low_id);
569 assert_eq!(eviction.priority, MessagePriority::Low);
570 }
571}