1use std::sync::Arc;
9use std::time::{Duration, Instant};
10
11use dashmap::DashMap;
12use serde::{Deserialize, Serialize};
13
14use crate::dead_letter::{DeadLetterQueue, DeadLetterReason};
15use crate::ipc::KernelMessage;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ReliableConfig {
20 pub max_retries: u32,
22 pub initial_timeout: Duration,
24 pub max_timeout: Duration,
26 pub backoff_multiplier: f64,
28}
29
30impl Default for ReliableConfig {
31 fn default() -> Self {
32 Self {
33 max_retries: 3,
34 initial_timeout: Duration::from_secs(5),
35 max_timeout: Duration::from_secs(30),
36 backoff_multiplier: 2.0,
37 }
38 }
39}
40
41#[derive(Debug, Clone)]
43pub struct PendingDelivery {
44 pub message: KernelMessage,
46 pub attempts: u32,
48 pub first_sent: Instant,
50 pub last_attempt: Instant,
52 pub ack_deadline: Instant,
54}
55
56#[non_exhaustive]
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub enum DeliveryResult {
60 Acknowledged {
62 msg_id: String,
64 ack_time_ms: u64,
66 },
67 MaxRetriesExceeded {
69 msg_id: String,
71 attempts: u32,
73 },
74 DeadLettered {
76 msg_id: String,
78 reason: String,
80 },
81}
82
83pub struct ReliableQueue {
89 pending: DashMap<String, PendingDelivery>,
90 config: ReliableConfig,
91 dead_letter: Arc<DeadLetterQueue>,
92 dead_letter_model: Option<Arc<std::sync::Mutex<crate::eml_kernel::DeadLetterModel>>>,
98}
99
100impl ReliableQueue {
101 pub fn new(config: ReliableConfig, dead_letter: Arc<DeadLetterQueue>) -> Self {
103 Self {
104 pending: DashMap::new(),
105 config,
106 dead_letter,
107 dead_letter_model: None,
108 }
109 }
110
111 pub fn with_defaults(dead_letter: Arc<DeadLetterQueue>) -> Self {
113 Self::new(ReliableConfig::default(), dead_letter)
114 }
115
116 pub fn with_model(
125 mut self,
126 model: Arc<std::sync::Mutex<crate::eml_kernel::DeadLetterModel>>,
127 ) -> Self {
128 self.dead_letter_model = Some(model);
129 self
130 }
131
132 pub fn dead_letter_model(
134 &self,
135 ) -> Option<&Arc<std::sync::Mutex<crate::eml_kernel::DeadLetterModel>>> {
136 self.dead_letter_model.as_ref()
137 }
138
139 pub fn config(&self) -> &ReliableConfig {
141 &self.config
142 }
143
144 pub fn register(&self, mut msg: KernelMessage) -> (String, KernelMessage) {
149 let correlation_id = msg
150 .correlation_id
151 .clone()
152 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
153 msg.correlation_id = Some(correlation_id.clone());
154
155 let now = Instant::now();
156 let pending = PendingDelivery {
157 message: msg.clone(),
158 attempts: 1,
159 first_sent: now,
160 last_attempt: now,
161 ack_deadline: now + self.config.initial_timeout,
162 };
163
164 self.pending.insert(correlation_id.clone(), pending);
165 (correlation_id, msg)
166 }
167
168 pub fn acknowledge(&self, correlation_id: &str) -> Option<DeliveryResult> {
173 let (_, pending) = self.pending.remove(correlation_id)?;
174 let ack_time = pending.first_sent.elapsed();
175 Some(DeliveryResult::Acknowledged {
176 msg_id: pending.message.id.clone(),
177 ack_time_ms: ack_time.as_millis() as u64,
178 })
179 }
180
181 pub fn check_timeouts(&self) -> Vec<(String, KernelMessage)> {
192 let now = Instant::now();
193 let mut retries = Vec::new();
194 let queue_depth = self.pending.len();
195
196 for mut entry in self.pending.iter_mut() {
197 if now >= entry.ack_deadline {
198 let corr_id = entry.key().clone();
199 let pending = entry.value_mut();
200
201 pending.attempts += 1;
202 pending.last_attempt = now;
203
204 let message_age_ms = pending.first_sent.elapsed().as_millis() as u64;
205 let next_timeout = self.compute_next_timeout(
206 pending.attempts.saturating_sub(1),
207 message_age_ms,
208 queue_depth,
209 );
210 pending.ack_deadline = now + next_timeout;
211
212 retries.push((corr_id, pending.message.clone()));
213 }
214 }
215
216 retries
217 }
218
219 fn compute_next_timeout(
228 &self,
229 attempt_idx: u32,
230 message_age_ms: u64,
231 queue_depth: usize,
232 ) -> Duration {
233 let backoff = self.config.initial_timeout.as_secs_f64()
235 * self.config.backoff_multiplier.powi(attempt_idx as i32);
236 let fallback = Duration::from_secs_f64(backoff.min(self.config.max_timeout.as_secs_f64()));
237
238 let Some(model_handle) = self.dead_letter_model.as_ref() else {
239 return fallback;
240 };
241 let Ok(model) = model_handle.lock() else {
242 return fallback;
243 };
244 if !model.is_trained() {
245 return fallback;
246 }
247 let (delay_ms, _should_discard) = model.predict(attempt_idx, message_age_ms, queue_depth);
248 Duration::from_millis(delay_ms.min(self.config.max_timeout.as_millis() as u64))
249 }
250
251 pub fn expire_max_retries(&self) -> Vec<DeliveryResult> {
261 let mut expired = Vec::new();
262 let mut to_remove = Vec::new();
263 let queue_depth = self.pending.len();
264
265 for entry in self.pending.iter() {
266 if self.should_discard(
267 entry.attempts,
268 entry.first_sent.elapsed().as_millis() as u64,
269 queue_depth,
270 ) {
271 to_remove.push(entry.key().clone());
272 }
273 }
274
275 for corr_id in to_remove {
276 if let Some((_, pending)) = self.pending.remove(&corr_id) {
277 let total_ms = pending.first_sent.elapsed().as_millis() as u64;
278 self.dead_letter.intake(
279 pending.message.clone(),
280 DeadLetterReason::Timeout {
281 duration_ms: total_ms,
282 },
283 );
284 expired.push(DeliveryResult::MaxRetriesExceeded {
285 msg_id: pending.message.id,
286 attempts: pending.attempts,
287 });
288 }
289 }
290
291 expired
292 }
293
294 fn should_discard(&self, attempts: u32, message_age_ms: u64, queue_depth: usize) -> bool {
298 let fallback = attempts > self.config.max_retries;
300
301 let Some(model_handle) = self.dead_letter_model.as_ref() else {
302 return fallback;
303 };
304 let Ok(model) = model_handle.lock() else {
305 return fallback;
306 };
307 if !model.is_trained() {
308 return fallback;
309 }
310 let (_delay_ms, model_discard) = model.predict(attempts, message_age_ms, queue_depth);
311 fallback || model_discard
314 }
315
316 pub fn pending_count(&self) -> usize {
318 self.pending.len()
319 }
320
321 pub fn is_pending(&self, correlation_id: &str) -> bool {
323 self.pending.contains_key(correlation_id)
324 }
325
326 pub fn pending_snapshot(&self) -> Vec<(String, PendingDelivery)> {
328 self.pending
329 .iter()
330 .map(|entry| (entry.key().clone(), entry.value().clone()))
331 .collect()
332 }
333
334 pub fn cancel(&self, correlation_id: &str) -> Option<KernelMessage> {
336 self.pending.remove(correlation_id).map(|(_, p)| p.message)
337 }
338
339 pub fn timeout_for_attempt(&self, attempt: u32) -> Duration {
345 self.compute_next_timeout(attempt, 0, self.pending.len())
346 }
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352 use crate::ipc::{MessagePayload, MessageTarget};
353
354 fn make_msg(from: u64, target: u64) -> KernelMessage {
355 KernelMessage::new(
356 from,
357 MessageTarget::Process(target),
358 MessagePayload::Text("reliable-test".into()),
359 )
360 }
361
362 fn make_queue() -> (ReliableQueue, Arc<DeadLetterQueue>) {
363 let dlq = Arc::new(DeadLetterQueue::new(100));
364 let config = ReliableConfig {
365 max_retries: 3,
366 initial_timeout: Duration::from_millis(100),
367 max_timeout: Duration::from_secs(2),
368 backoff_multiplier: 2.0,
369 };
370 (ReliableQueue::new(config, dlq.clone()), dlq)
371 }
372
373 #[test]
374 fn register_and_acknowledge() {
375 let (queue, _dlq) = make_queue();
376 let msg = make_msg(1, 2);
377 let (corr_id, _msg) = queue.register(msg);
378
379 assert!(queue.is_pending(&corr_id));
380 assert_eq!(queue.pending_count(), 1);
381
382 let result = queue.acknowledge(&corr_id).unwrap();
383 assert!(matches!(result, DeliveryResult::Acknowledged { .. }));
384 assert!(!queue.is_pending(&corr_id));
385 assert_eq!(queue.pending_count(), 0);
386 }
387
388 #[test]
389 fn acknowledge_unknown_returns_none() {
390 let (queue, _dlq) = make_queue();
391 assert!(queue.acknowledge("nonexistent").is_none());
392 }
393
394 #[test]
395 fn register_generates_correlation_id() {
396 let (queue, _dlq) = make_queue();
397 let msg = make_msg(1, 2);
398 assert!(msg.correlation_id.is_none());
399
400 let (corr_id, msg) = queue.register(msg);
401 assert!(!corr_id.is_empty());
402 assert_eq!(msg.correlation_id, Some(corr_id));
403 }
404
405 #[test]
406 fn register_preserves_existing_correlation_id() {
407 let (queue, _dlq) = make_queue();
408 let msg = KernelMessage::with_correlation(
409 1,
410 MessageTarget::Process(2),
411 MessagePayload::Text("test".into()),
412 "my-corr-id".into(),
413 );
414
415 let (corr_id, _) = queue.register(msg);
416 assert_eq!(corr_id, "my-corr-id");
417 }
418
419 #[test]
420 fn check_timeouts_empty() {
421 let (queue, _dlq) = make_queue();
422 let retries = queue.check_timeouts();
423 assert!(retries.is_empty());
424 }
425
426 #[test]
427 fn backoff_increases_per_attempt() {
428 let (queue, _dlq) = make_queue();
429 let t0 = queue.timeout_for_attempt(0);
431 let t1 = queue.timeout_for_attempt(1);
432 let t2 = queue.timeout_for_attempt(2);
433
434 assert_eq!(t0.as_millis(), 100);
435 assert_eq!(t1.as_millis(), 200);
436 assert_eq!(t2.as_millis(), 400);
437 }
438
439 #[test]
440 fn backoff_caps_at_max_timeout() {
441 let (queue, _dlq) = make_queue();
442 let t10 = queue.timeout_for_attempt(10);
444 assert!(t10 <= Duration::from_secs(2));
445 }
446
447 #[test]
448 fn expire_max_retries_sends_to_dlq() {
449 let (queue, dlq) = make_queue();
450 let msg = make_msg(1, 2);
451 let (corr_id, _msg) = queue.register(msg);
452
453 if let Some(mut entry) = queue.pending.get_mut(&corr_id) {
455 entry.attempts = 5; }
457
458 let expired = queue.expire_max_retries();
459 assert_eq!(expired.len(), 1);
460 assert!(matches!(
461 &expired[0],
462 DeliveryResult::MaxRetriesExceeded { attempts: 5, .. }
463 ));
464 assert_eq!(queue.pending_count(), 0);
465 assert_eq!(dlq.len(), 1);
466 }
467
468 #[test]
469 fn concurrent_sends_tracked_independently() {
470 let (queue, _dlq) = make_queue();
471 let (id1, _) = queue.register(make_msg(1, 2));
472 let (id2, _) = queue.register(make_msg(3, 4));
473 let (id3, _) = queue.register(make_msg(5, 6));
474
475 assert_eq!(queue.pending_count(), 3);
476
477 queue.acknowledge(&id2);
479 assert_eq!(queue.pending_count(), 2);
480 assert!(queue.is_pending(&id1));
481 assert!(!queue.is_pending(&id2));
482 assert!(queue.is_pending(&id3));
483 }
484
485 #[test]
486 fn cancel_removes_tracking() {
487 let (queue, _dlq) = make_queue();
488 let (corr_id, _) = queue.register(make_msg(1, 2));
489 assert!(queue.is_pending(&corr_id));
490
491 let msg = queue.cancel(&corr_id).unwrap();
492 assert!(!queue.is_pending(&corr_id));
493 assert_eq!(msg.from, 1);
494 }
495
496 #[test]
497 fn cancel_nonexistent_returns_none() {
498 let (queue, _dlq) = make_queue();
499 assert!(queue.cancel("nope").is_none());
500 }
501
502 #[test]
503 fn pending_snapshot() {
504 let (queue, _dlq) = make_queue();
505 queue.register(make_msg(1, 2));
506 queue.register(make_msg(3, 4));
507
508 let snap = queue.pending_snapshot();
509 assert_eq!(snap.len(), 2);
510 }
511
512 #[test]
513 fn default_config() {
514 let config = ReliableConfig::default();
515 assert_eq!(config.max_retries, 3);
516 assert_eq!(config.initial_timeout, Duration::from_secs(5));
517 assert_eq!(config.max_timeout, Duration::from_secs(30));
518 assert!((config.backoff_multiplier - 2.0).abs() < f64::EPSILON);
519 }
520
521 #[test]
522 fn delivery_result_serde_roundtrip() {
523 let result = DeliveryResult::Acknowledged {
524 msg_id: "msg-1".into(),
525 ack_time_ms: 42,
526 };
527 let json = serde_json::to_string(&result).unwrap();
528 let restored: DeliveryResult = serde_json::from_str(&json).unwrap();
529 assert!(matches!(
530 restored,
531 DeliveryResult::Acknowledged {
532 ack_time_ms: 42,
533 ..
534 }
535 ));
536 }
537
538 #[test]
541 fn untrained_model_preserves_backoff_schedule() {
542 let dlq = Arc::new(DeadLetterQueue::new(100));
546 let cfg = ReliableConfig {
547 max_retries: 3,
548 initial_timeout: Duration::from_millis(100),
549 max_timeout: Duration::from_secs(2),
550 backoff_multiplier: 2.0,
551 };
552 let plain = ReliableQueue::new(cfg.clone(), dlq.clone());
553 let model = Arc::new(std::sync::Mutex::new(
554 crate::eml_kernel::DeadLetterModel::new(),
555 ));
556 let wired = ReliableQueue::new(cfg, dlq).with_model(model);
557
558 for attempt in 0..6 {
559 assert_eq!(
560 plain.timeout_for_attempt(attempt),
561 wired.timeout_for_attempt(attempt),
562 "untrained DeadLetterModel must reproduce backoff at attempt {attempt}"
563 );
564 }
565 }
566
567 #[test]
568 fn untrained_model_preserves_discard_rule() {
569 let dlq = Arc::new(DeadLetterQueue::new(100));
572 let cfg = ReliableConfig {
573 max_retries: 3,
574 initial_timeout: Duration::from_millis(100),
575 max_timeout: Duration::from_secs(2),
576 backoff_multiplier: 2.0,
577 };
578 let model = Arc::new(std::sync::Mutex::new(
579 crate::eml_kernel::DeadLetterModel::new(),
580 ));
581 let queue = ReliableQueue::new(cfg, dlq.clone()).with_model(model);
582
583 let (corr_id, _) = queue.register(make_msg(1, 2));
584 assert!(queue.expire_max_retries().is_empty());
586
587 if let Some(mut entry) = queue.pending.get_mut(&corr_id) {
588 entry.attempts = 5; }
590 let expired = queue.expire_max_retries();
591 assert_eq!(expired.len(), 1);
592 assert_eq!(dlq.len(), 1);
593 }
594
595 #[test]
596 fn trained_model_can_shorten_backoff() {
597 let dlq = Arc::new(DeadLetterQueue::new(100));
601 let cfg = ReliableConfig {
602 max_retries: 3,
603 initial_timeout: Duration::from_millis(100),
604 max_timeout: Duration::from_secs(2),
605 backoff_multiplier: 2.0,
606 };
607 let plain = ReliableQueue::new(cfg.clone(), dlq.clone());
608
609 let untrained = crate::eml_kernel::DeadLetterModel::new();
610 let mut json = serde_json::to_value(&untrained).unwrap();
611 if let Some(inner) = json.get_mut("inner").and_then(|v| v.as_object_mut()) {
612 inner.insert("trained".into(), serde_json::Value::Bool(true));
613 }
614 let forced: crate::eml_kernel::DeadLetterModel = serde_json::from_value(json).unwrap();
615 assert!(forced.is_trained());
616
617 let model = Arc::new(std::sync::Mutex::new(forced));
618 let wired = ReliableQueue::new(cfg, dlq).with_model(model);
619
620 let attempt = 2;
621 let hardcoded = plain.timeout_for_attempt(attempt);
622 let model_driven = wired.timeout_for_attempt(attempt);
623 assert_ne!(hardcoded, model_driven);
625 }
626
627 #[test]
628 fn check_timeouts_with_untrained_model_matches_plain() {
629 let dlq = Arc::new(DeadLetterQueue::new(100));
632 let cfg = ReliableConfig {
633 max_retries: 3,
634 initial_timeout: Duration::from_millis(50),
635 max_timeout: Duration::from_secs(2),
636 backoff_multiplier: 2.0,
637 };
638 let model = Arc::new(std::sync::Mutex::new(
639 crate::eml_kernel::DeadLetterModel::new(),
640 ));
641 let queue = ReliableQueue::new(cfg, dlq).with_model(model);
642
643 let (corr_id, _) = queue.register(make_msg(1, 2));
644 if let Some(mut entry) = queue.pending.get_mut(&corr_id) {
646 entry.ack_deadline = Instant::now() - Duration::from_millis(10);
647 }
648 let retries = queue.check_timeouts();
649 assert_eq!(retries.len(), 1);
650 let entry = queue.pending.get(&corr_id).unwrap();
652 assert_eq!(entry.attempts, 2);
653 }
654}