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 get_pending_ack(&self, message_id: &MessageId) -> Option<&PendingAck> {
269 self.pending_acks.get(message_id)
270 }
271
272 pub fn pending_count(&self) -> usize {
274 self.pending_acks.len()
275 }
276
277 pub fn is_waiting_for_ack(&self, message_id: &MessageId) -> bool {
279 self.pending_acks.contains_key(message_id)
280 }
281
282 fn cleanup_timed_out(&mut self) {
284 self.drain_timed_out();
285 }
286
287 pub fn prune_old_timeouts(&mut self, older_than: Duration) {
291 let now = Utc::now();
292 let cutoff = now
293 - chrono::Duration::from_std(older_than).unwrap_or_else(|_| chrono::Duration::zero());
294
295 self.pending_acks
296 .retain(|_, ack| ack.status != AckStatus::TimedOut || ack.sent_at > cutoff);
297 }
298}
299
300impl Default for AckManager {
301 fn default() -> Self {
302 Self::new()
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309 use std::thread;
310
311 #[test]
312 fn test_register_and_receive_ack() {
313 let mut manager = AckManager::new();
314 let msg_id = MessageId::new();
315
316 manager.register_pending_ack(msg_id.clone(), None).unwrap();
318 assert!(manager.is_waiting_for_ack(&msg_id));
319 assert_eq!(manager.pending_count(), 1);
320
321 let removed = manager.remove_ack(&msg_id).unwrap();
323 assert_eq!(removed.message_id, msg_id);
324 assert!(!manager.is_waiting_for_ack(&msg_id));
325 assert_eq!(manager.pending_count(), 0);
326 }
327
328 #[test]
329 fn test_ack_timeout() {
330 let config = AckConfig {
331 default_timeout_ms: 50, max_pending_acks: 100,
333 };
334 let mut manager = AckManager::with_config(config);
335 let msg_id = MessageId::new();
336
337 manager
338 .register_pending_ack(msg_id.clone(), Some(50))
339 .unwrap();
340
341 assert!(manager.drain_timed_out().is_empty());
343
344 thread::sleep(Duration::from_millis(60));
346
347 let timed_out = manager.drain_timed_out();
349 assert_eq!(timed_out.len(), 1);
350 assert_eq!(timed_out[0].message_id, msg_id);
351 }
352
353 #[test]
354 fn test_retry_count_increment() {
355 let mut manager = AckManager::new();
356 let msg_id = MessageId::new();
357
358 manager.register_pending_ack(msg_id.clone(), None).unwrap();
359
360 let pending = manager.get_pending_ack(&msg_id).unwrap();
361 assert_eq!(pending.retry_count, 0);
362
363 manager.increment_retry_count(&msg_id);
364
365 let pending = manager.get_pending_ack(&msg_id).unwrap();
366 assert_eq!(pending.retry_count, 1);
367 }
368
369 #[test]
370 fn test_remove_ack() {
371 let mut manager = AckManager::new();
372 let msg_id = MessageId::new();
373
374 manager.register_pending_ack(msg_id.clone(), None).unwrap();
375 assert!(manager.is_waiting_for_ack(&msg_id));
376
377 manager.remove_ack(&msg_id);
378 assert!(!manager.is_waiting_for_ack(&msg_id));
379 }
380
381 #[test]
382 fn test_max_pending_limit() {
383 let config = AckConfig {
384 default_timeout_ms: 5000,
385 max_pending_acks: 3,
386 };
387 let mut manager = AckManager::with_config(config);
388
389 manager
391 .register_pending_ack(MessageId::new(), None)
392 .unwrap();
393 manager
394 .register_pending_ack(MessageId::new(), None)
395 .unwrap();
396 manager
397 .register_pending_ack(MessageId::new(), None)
398 .unwrap();
399
400 let result = manager.register_pending_ack(MessageId::new(), None);
402 assert!(result.is_err());
403 }
404
405 #[test]
406 fn test_priority_based_eviction() {
407 let config = AckConfig {
408 default_timeout_ms: 5000,
409 max_pending_acks: 2,
410 };
411 let mut manager = AckManager::with_config(config);
412
413 let low_id = MessageId::new();
414 let medium_id = MessageId::new();
415
416 manager
418 .register_pending_ack_with_priority(low_id.clone(), None, MessagePriority::Low)
419 .unwrap();
420 manager
422 .register_pending_ack_with_priority(medium_id.clone(), None, MessagePriority::Medium)
423 .unwrap();
424
425 assert_eq!(manager.pending_count(), 2);
426
427 let high_id = MessageId::new();
429 manager
430 .register_pending_ack_with_priority(high_id.clone(), None, MessagePriority::High)
431 .unwrap();
432
433 assert_eq!(manager.pending_count(), 2);
434 assert!(!manager.is_waiting_for_ack(&low_id)); assert!(manager.is_waiting_for_ack(&medium_id)); assert!(manager.is_waiting_for_ack(&high_id)); }
438
439 #[test]
440 fn test_critical_priority_evicts_all() {
441 let config = AckConfig {
442 default_timeout_ms: 5000,
443 max_pending_acks: 1,
444 };
445 let mut manager = AckManager::with_config(config);
446
447 let medium_id = MessageId::new();
448 manager
449 .register_pending_ack_with_priority(medium_id.clone(), None, MessagePriority::Medium)
450 .unwrap();
451
452 let critical_id = MessageId::new();
454 manager
455 .register_pending_ack_with_priority(
456 critical_id.clone(),
457 None,
458 MessagePriority::Critical,
459 )
460 .unwrap();
461
462 assert_eq!(manager.pending_count(), 1);
463 assert!(!manager.is_waiting_for_ack(&medium_id));
464 assert!(manager.is_waiting_for_ack(&critical_id));
465 }
466
467 #[test]
468 fn test_prune_old_timeouts() {
469 let config = AckConfig {
470 default_timeout_ms: 10,
471 max_pending_acks: 100,
472 };
473 let mut manager = AckManager::with_config(config);
474
475 let msg_id = MessageId::new();
476 manager
477 .register_pending_ack(msg_id.clone(), Some(10))
478 .unwrap();
479
480 thread::sleep(Duration::from_millis(20));
482 manager.cleanup_timed_out();
483
484 assert_eq!(manager.pending_count(), 1);
485
486 manager.prune_old_timeouts(Duration::from_millis(5));
488 assert_eq!(manager.pending_count(), 0);
489 }
490
491 #[test]
492 fn test_ack_not_found() {
493 let mut manager = AckManager::new();
494 let msg_id = MessageId::new();
495
496 assert!(manager.remove_ack(&msg_id).is_none());
498 }
499
500 #[test]
501 fn test_eviction_returned_from_register() {
502 let config = AckConfig {
503 default_timeout_ms: 5000,
504 max_pending_acks: 2,
505 };
506 let mut manager = AckManager::with_config(config);
507
508 let low_id = MessageId::new();
509 assert!(manager
511 .register_pending_ack_with_priority(low_id.clone(), None, MessagePriority::Low)
512 .unwrap()
513 .is_none());
514 assert!(manager
515 .register_pending_ack_with_priority(MessageId::new(), None, MessagePriority::Medium)
516 .unwrap()
517 .is_none());
518
519 let eviction = manager
521 .register_pending_ack_with_priority(MessageId::new(), None, MessagePriority::High)
522 .unwrap();
523 let eviction = eviction.expect("should have evicted the low-priority ACK");
524 assert_eq!(eviction.message_id, low_id);
525 assert_eq!(eviction.priority, MessagePriority::Low);
526 }
527}