use crate::constants::{DEFAULT_ACK_TIMEOUT_MS, DEFAULT_MAX_PENDING_ACKS};
use chrono::{DateTime, Utc};
use offline_protocol_core::{MessageId, MessagePriority};
use std::collections::HashMap;
use std::time::Duration;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct AckEvictionInfo {
pub message_id: MessageId,
pub priority: MessagePriority,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AckStatus {
Pending,
TimedOut,
}
#[derive(Debug, Clone)]
pub struct PendingAck {
pub message_id: MessageId,
pub sent_at: DateTime<Utc>,
pub timeout_ms: u64,
pub status: AckStatus,
pub retry_count: u32,
pub priority: MessagePriority,
}
impl PendingAck {
pub fn is_timed_out(&self) -> bool {
if self.status != AckStatus::Pending {
return false;
}
let elapsed = Utc::now().signed_duration_since(self.sent_at);
elapsed.num_milliseconds() >= self.timeout_ms as i64
}
}
#[derive(Debug, Clone)]
pub struct AckConfig {
pub default_timeout_ms: u64,
pub max_pending_acks: usize,
}
impl Default for AckConfig {
fn default() -> Self {
Self {
default_timeout_ms: DEFAULT_ACK_TIMEOUT_MS,
max_pending_acks: DEFAULT_MAX_PENDING_ACKS,
}
}
}
pub struct AckManager {
config: AckConfig,
pending_acks: HashMap<MessageId, PendingAck>,
}
impl AckManager {
pub fn new() -> Self {
Self::with_config(AckConfig::default())
}
pub fn with_config(config: AckConfig) -> Self {
Self {
config,
pending_acks: HashMap::new(),
}
}
pub fn register_pending_ack(
&mut self,
message_id: MessageId,
timeout_ms: Option<u64>,
) -> crate::Result<Option<AckEvictionInfo>> {
self.register_pending_ack_with_priority(message_id, timeout_ms, MessagePriority::Medium)
}
pub fn register_pending_ack_with_priority(
&mut self,
message_id: MessageId,
timeout_ms: Option<u64>,
priority: MessagePriority,
) -> crate::Result<Option<AckEvictionInfo>> {
let mut eviction = None;
if self.pending_acks.len() >= self.config.max_pending_acks {
self.cleanup_timed_out();
if self.pending_acks.len() >= self.config.max_pending_acks {
match self.evict_lowest_priority(priority) {
Some(info) => eviction = Some(info),
None => {
return Err(crate::Error::Other(
"Maximum pending ACKs limit reached (no lower priority ACKs to evict)"
.to_string(),
));
}
}
}
}
let pending = PendingAck {
message_id: message_id.clone(),
sent_at: Utc::now(),
timeout_ms: timeout_ms.unwrap_or(self.config.default_timeout_ms),
status: AckStatus::Pending,
retry_count: 0,
priority,
};
self.pending_acks.insert(message_id, pending);
Ok(eviction)
}
fn evict_lowest_priority(&mut self, new_priority: MessagePriority) -> Option<AckEvictionInfo> {
let evict_candidate = self
.pending_acks
.iter()
.filter(|(_, ack)| ack.status == AckStatus::Pending)
.min_by(|(_, a), (_, b)| {
let priority_cmp =
Self::priority_rank(a.priority).cmp(&Self::priority_rank(b.priority));
if priority_cmp != std::cmp::Ordering::Equal {
return priority_cmp;
}
a.sent_at.cmp(&b.sent_at)
})
.map(|(id, ack)| (id.clone(), ack.priority));
if let Some((evict_id, evict_priority)) = evict_candidate {
if Self::priority_rank(evict_priority) < Self::priority_rank(new_priority) {
self.pending_acks.remove(&evict_id);
return Some(AckEvictionInfo {
message_id: evict_id,
priority: evict_priority,
});
}
}
None
}
fn priority_rank(priority: MessagePriority) -> u8 {
match priority {
MessagePriority::Low => 0,
MessagePriority::Medium => 1,
MessagePriority::High => 2,
MessagePriority::Critical => 3,
}
}
pub fn drain_timed_out(&mut self) -> Vec<PendingAck> {
let mut timed_out = Vec::new();
for pending in self.pending_acks.values_mut() {
if pending.is_timed_out() {
pending.status = AckStatus::TimedOut;
timed_out.push(pending.clone());
}
}
timed_out
}
pub fn remove_ack(&mut self, message_id: &MessageId) -> Option<PendingAck> {
self.pending_acks.remove(message_id)
}
pub fn get_timed_out_acks(&self) -> Vec<MessageId> {
self.pending_acks
.values()
.filter(|ack| ack.is_timed_out())
.map(|ack| ack.message_id.clone())
.collect()
}
pub fn increment_retry_count(&mut self, message_id: &MessageId) {
if let Some(pending) = self.pending_acks.get_mut(message_id) {
pending.retry_count += 1;
pending.sent_at = Utc::now(); pending.status = AckStatus::Pending;
}
}
pub fn get_pending_ack(&self, message_id: &MessageId) -> Option<&PendingAck> {
self.pending_acks.get(message_id)
}
pub fn pending_count(&self) -> usize {
self.pending_acks.len()
}
pub fn is_waiting_for_ack(&self, message_id: &MessageId) -> bool {
self.pending_acks.contains_key(message_id)
}
fn cleanup_timed_out(&mut self) {
self.drain_timed_out();
}
pub fn prune_old_timeouts(&mut self, older_than: Duration) {
let now = Utc::now();
let cutoff = now
- chrono::Duration::from_std(older_than).unwrap_or_else(|_| chrono::Duration::zero());
self.pending_acks
.retain(|_, ack| ack.status != AckStatus::TimedOut || ack.sent_at > cutoff);
}
}
impl Default for AckManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
#[test]
fn test_register_and_receive_ack() {
let mut manager = AckManager::new();
let msg_id = MessageId::new();
manager.register_pending_ack(msg_id.clone(), None).unwrap();
assert!(manager.is_waiting_for_ack(&msg_id));
assert_eq!(manager.pending_count(), 1);
let removed = manager.remove_ack(&msg_id).unwrap();
assert_eq!(removed.message_id, msg_id);
assert!(!manager.is_waiting_for_ack(&msg_id));
assert_eq!(manager.pending_count(), 0);
}
#[test]
fn test_ack_timeout() {
let config = AckConfig {
default_timeout_ms: 50, max_pending_acks: 100,
};
let mut manager = AckManager::with_config(config);
let msg_id = MessageId::new();
manager
.register_pending_ack(msg_id.clone(), Some(50))
.unwrap();
assert!(manager.drain_timed_out().is_empty());
thread::sleep(Duration::from_millis(60));
let timed_out = manager.drain_timed_out();
assert_eq!(timed_out.len(), 1);
assert_eq!(timed_out[0].message_id, msg_id);
}
#[test]
fn test_retry_count_increment() {
let mut manager = AckManager::new();
let msg_id = MessageId::new();
manager.register_pending_ack(msg_id.clone(), None).unwrap();
let pending = manager.get_pending_ack(&msg_id).unwrap();
assert_eq!(pending.retry_count, 0);
manager.increment_retry_count(&msg_id);
let pending = manager.get_pending_ack(&msg_id).unwrap();
assert_eq!(pending.retry_count, 1);
}
#[test]
fn test_remove_ack() {
let mut manager = AckManager::new();
let msg_id = MessageId::new();
manager.register_pending_ack(msg_id.clone(), None).unwrap();
assert!(manager.is_waiting_for_ack(&msg_id));
manager.remove_ack(&msg_id);
assert!(!manager.is_waiting_for_ack(&msg_id));
}
#[test]
fn test_max_pending_limit() {
let config = AckConfig {
default_timeout_ms: 5000,
max_pending_acks: 3,
};
let mut manager = AckManager::with_config(config);
manager
.register_pending_ack(MessageId::new(), None)
.unwrap();
manager
.register_pending_ack(MessageId::new(), None)
.unwrap();
manager
.register_pending_ack(MessageId::new(), None)
.unwrap();
let result = manager.register_pending_ack(MessageId::new(), None);
assert!(result.is_err());
}
#[test]
fn test_priority_based_eviction() {
let config = AckConfig {
default_timeout_ms: 5000,
max_pending_acks: 2,
};
let mut manager = AckManager::with_config(config);
let low_id = MessageId::new();
let medium_id = MessageId::new();
manager
.register_pending_ack_with_priority(low_id.clone(), None, MessagePriority::Low)
.unwrap();
manager
.register_pending_ack_with_priority(medium_id.clone(), None, MessagePriority::Medium)
.unwrap();
assert_eq!(manager.pending_count(), 2);
let high_id = MessageId::new();
manager
.register_pending_ack_with_priority(high_id.clone(), None, MessagePriority::High)
.unwrap();
assert_eq!(manager.pending_count(), 2);
assert!(!manager.is_waiting_for_ack(&low_id)); assert!(manager.is_waiting_for_ack(&medium_id)); assert!(manager.is_waiting_for_ack(&high_id)); }
#[test]
fn test_critical_priority_evicts_all() {
let config = AckConfig {
default_timeout_ms: 5000,
max_pending_acks: 1,
};
let mut manager = AckManager::with_config(config);
let medium_id = MessageId::new();
manager
.register_pending_ack_with_priority(medium_id.clone(), None, MessagePriority::Medium)
.unwrap();
let critical_id = MessageId::new();
manager
.register_pending_ack_with_priority(
critical_id.clone(),
None,
MessagePriority::Critical,
)
.unwrap();
assert_eq!(manager.pending_count(), 1);
assert!(!manager.is_waiting_for_ack(&medium_id));
assert!(manager.is_waiting_for_ack(&critical_id));
}
#[test]
fn test_prune_old_timeouts() {
let config = AckConfig {
default_timeout_ms: 10,
max_pending_acks: 100,
};
let mut manager = AckManager::with_config(config);
let msg_id = MessageId::new();
manager
.register_pending_ack(msg_id.clone(), Some(10))
.unwrap();
thread::sleep(Duration::from_millis(20));
manager.cleanup_timed_out();
assert_eq!(manager.pending_count(), 1);
manager.prune_old_timeouts(Duration::from_millis(5));
assert_eq!(manager.pending_count(), 0);
}
#[test]
fn test_ack_not_found() {
let mut manager = AckManager::new();
let msg_id = MessageId::new();
assert!(manager.remove_ack(&msg_id).is_none());
}
#[test]
fn test_eviction_returned_from_register() {
let config = AckConfig {
default_timeout_ms: 5000,
max_pending_acks: 2,
};
let mut manager = AckManager::with_config(config);
let low_id = MessageId::new();
assert!(manager
.register_pending_ack_with_priority(low_id.clone(), None, MessagePriority::Low)
.unwrap()
.is_none());
assert!(manager
.register_pending_ack_with_priority(MessageId::new(), None, MessagePriority::Medium)
.unwrap()
.is_none());
let eviction = manager
.register_pending_ack_with_priority(MessageId::new(), None, MessagePriority::High)
.unwrap();
let eviction = eviction.expect("should have evicted the low-priority ACK");
assert_eq!(eviction.message_id, low_id);
assert_eq!(eviction.priority, MessagePriority::Low);
}
}