Skip to main content

camel_processor/resequencer/
stream.rs

1//! Stream resequencing policy — bounded priority queue keyed by sequence
2//! number, gap detection with per-gap timeout, capacity cap, opt-in dedup.
3
4use std::collections::{BTreeMap, HashMap};
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::{Arc, Mutex, Weak};
7use std::time::Duration;
8
9use async_trait::async_trait;
10use camel_api::exchange::Exchange;
11use camel_api::resequencer::{CapacityPolicy, GapPolicy};
12use camel_language_api::Expression;
13use tokio::sync::mpsc;
14use tokio::task::JoinHandle;
15use tokio_util::sync::CancellationToken;
16
17use super::ResequencePolicy;
18
19/// Stream resequencing policy.
20///
21/// Uses a `BTreeMap<u64, Exchange>` as a priority queue keyed by sequence
22/// number. Tracks `next_expected` and emits the contiguous run whenever the
23/// expected sequence arrives. Gap timers fire when a missing sequence is not
24/// received within `gap_timeout`.
25pub struct StreamPolicy {
26    sequence_expr: Arc<dyn Expression>,
27    capacity: usize,
28    gap_timeout: Duration,
29    on_gap: GapPolicy,
30    on_capacity_exceeded: CapacityPolicy,
31    dedup: bool,
32
33    /// Weak self-reference so gap timer tasks can upgrade to `Arc<Self>`.
34    weak_self: Weak<Self>,
35
36    /// Priority queue keyed by sequence number (min-first via BTreeMap).
37    queue: Mutex<BTreeMap<u64, Exchange>>,
38
39    /// Next expected sequence number.
40    next_expected: Mutex<u64>,
41
42    /// Gap cancellation tokens, keyed by the missing sequence number.
43    gap_tokens: Mutex<HashMap<u64, CancellationToken>>,
44
45    /// Gap timer task handles, keyed by the missing sequence number.
46    gap_handles: Mutex<HashMap<u64, JoinHandle<()>>>,
47
48    /// Channel to the post-driver for gap-timer-triggered emissions.
49    driver_tx: Mutex<Option<mpsc::Sender<Exchange>>>,
50
51    /// Shutdown guard — gap tasks check this before sending.
52    shutdown_started: AtomicBool,
53}
54
55impl StreamPolicy {
56    /// Create a new `Arc<StreamPolicy>` using `Arc::new_cyclic`.
57    pub fn new_cyclic(
58        sequence_expr: Arc<dyn Expression>,
59        capacity: usize,
60        gap_timeout_ms: u64,
61        on_gap: GapPolicy,
62        on_capacity_exceeded: CapacityPolicy,
63        dedup: bool,
64    ) -> Arc<Self> {
65        Arc::new_cyclic(|weak| Self {
66            sequence_expr,
67            capacity,
68            gap_timeout: Duration::from_millis(gap_timeout_ms),
69            on_gap,
70            on_capacity_exceeded,
71            dedup,
72            weak_self: weak.clone(),
73            queue: Mutex::new(BTreeMap::new()),
74            next_expected: Mutex::new(1),
75            gap_tokens: Mutex::new(HashMap::new()),
76            gap_handles: Mutex::new(HashMap::new()),
77            driver_tx: Mutex::new(None),
78            shutdown_started: AtomicBool::new(false),
79        })
80    }
81
82    /// Set the driver channel (via `set_timeout_tx` trait method).
83    fn set_driver_tx(&self, tx: mpsc::Sender<Exchange>) {
84        let mut guard = self.driver_tx.lock().unwrap_or_else(|e| e.into_inner());
85        *guard = Some(tx);
86    }
87
88    /// Evaluate the sequence expression against an exchange.
89    async fn eval_seq(&self, exchange: &Exchange) -> Result<u64, String> {
90        let val = self
91            .sequence_expr
92            .evaluate(exchange)
93            .await
94            .map_err(|e| format!("sequence expression evaluation failed: {e}"))?;
95        match val {
96            serde_json::Value::Number(n) => n
97                .as_u64()
98                .ok_or_else(|| format!("sequence value must be a non-negative integer, got {n}")),
99            _ => Err(format!(
100                "sequence expression must evaluate to a number, got {}",
101                val
102            )),
103        }
104    }
105
106    /// Get the next expected sequence number.
107    fn next_expected(&self) -> u64 {
108        *self.next_expected.lock().unwrap_or_else(|e| e.into_inner())
109    }
110
111    /// Set the next expected sequence number.
112    fn set_next_expected(&self, v: u64) {
113        *self.next_expected.lock().unwrap_or_else(|e| e.into_inner()) = v;
114    }
115
116    /// Drain the contiguous tail from the queue, starting at `next_expected`.
117    /// Returns all consecutive exchanges in sequence order and advances `next_expected`.
118    fn drain_contiguous(&self) -> Vec<Exchange> {
119        let mut queue = self.queue.lock().unwrap_or_else(|e| e.into_inner());
120        let mut expected = *self.next_expected.lock().unwrap_or_else(|e| e.into_inner());
121        let mut emitted = Vec::new();
122
123        while let Some(ex) = queue.remove(&expected) {
124            // Cancel any gap timer for this sequence
125            self.cancel_gap_timer(expected);
126            emitted.push(ex);
127            expected += 1;
128        }
129
130        *self.next_expected.lock().unwrap_or_else(|e| e.into_inner()) = expected;
131        emitted
132    }
133
134    /// Drain ALL held exchanges from the queue in sequence order, returning
135    /// `(held_exchanges, max_seq)` where `max_seq` is the largest sequence
136    /// number drained (0 if empty).
137    fn drain_all_with_max(&self) -> (Vec<Exchange>, u64) {
138        let mut queue = self.queue.lock().unwrap_or_else(|e| e.into_inner());
139        let keys: Vec<u64> = queue.keys().copied().collect();
140        let max_seq = keys.iter().max().copied().unwrap_or(0);
141        let mut held = Vec::new();
142        for k in keys {
143            if let Some(ex) = queue.remove(&k) {
144                self.cancel_gap_timer(k);
145                held.push(ex);
146            }
147        }
148        (held, max_seq)
149    }
150
151    /// Check if a gap timer exists for a sequence number.
152    fn has_gap_timer(&self, seq: u64) -> bool {
153        let tokens = self.gap_tokens.lock().unwrap_or_else(|e| e.into_inner());
154        tokens.contains_key(&seq)
155    }
156
157    /// Cancel and remove a gap timer for a sequence number.
158    fn cancel_gap_timer(&self, seq: u64) {
159        {
160            let mut tokens = self.gap_tokens.lock().unwrap_or_else(|e| e.into_inner());
161            if let Some(token) = tokens.remove(&seq) {
162                token.cancel();
163            }
164        }
165        {
166            let mut handles = self.gap_handles.lock().unwrap_or_else(|e| e.into_inner());
167            handles.remove(&seq);
168        }
169    }
170
171    /// Cancel all gap timers.
172    fn cancel_all_gap_timers(&self) {
173        let tokens: HashMap<u64, CancellationToken> = {
174            let mut guard = self.gap_tokens.lock().unwrap_or_else(|e| e.into_inner());
175            std::mem::take(&mut *guard)
176        };
177        for (_, token) in tokens {
178            token.cancel();
179        }
180        {
181            let mut handles = self.gap_handles.lock().unwrap_or_else(|e| e.into_inner());
182            handles.clear();
183        }
184    }
185
186    /// Spawn a gap timer for the given missing sequence.
187    fn spawn_gap_timer(&self, missing_seq: u64) {
188        let cancel = CancellationToken::new();
189        let cancel_clone = cancel.clone();
190
191        {
192            let mut tokens = self.gap_tokens.lock().unwrap_or_else(|e| e.into_inner());
193            tokens.insert(missing_seq, cancel);
194        }
195
196        let weak = self.weak_self.clone();
197        let gap_timeout = self.gap_timeout;
198        let on_gap = self.on_gap;
199        let driver_tx_opt = {
200            let guard = self.driver_tx.lock().unwrap_or_else(|e| e.into_inner());
201            guard.clone()
202        };
203
204        let handle = tokio::spawn(async move {
205            tokio::select! {
206                _ = tokio::time::sleep(gap_timeout) => {
207                    if cancel_clone.is_cancelled() {
208                        return;
209                    }
210                }
211                _ = cancel_clone.cancelled() => {
212                    return;
213                }
214            }
215
216            let Some(policy) = weak.upgrade() else {
217                return;
218            };
219
220            if policy.shutdown_started.load(Ordering::SeqCst) {
221                return;
222            }
223
224            match on_gap {
225                GapPolicy::EmitPartial => {
226                    // Advance past the missing sequence, then drain only the
227                    // contiguous run from the new position. Preserves
228                    // resequencing purity: intermediate gaps stay held.
229                    policy.set_next_expected(missing_seq + 1);
230                    let emitted = policy.drain_contiguous();
231
232                    if emitted.is_empty() {
233                        return;
234                    }
235
236                    if let Some(tx) = &driver_tx_opt {
237                        for ex in emitted {
238                            if tx.send(ex).await.is_err() {
239                                break;
240                            }
241                        }
242                    }
243                }
244                // DropAndLog and any future variant clean up held exchanges
245                // without violating resequencing order (drop + log).
246                _ => {
247                    let (held, max_seq) = policy.drain_all_with_max();
248
249                    if held.is_empty() {
250                        return;
251                    }
252
253                    policy.set_next_expected(max_seq + 1);
254
255                    for ex in &held {
256                        tracing::warn!(
257                            correlation_id = %ex.correlation_id(),
258                            "stream resequencer: gap timeout — dropping held exchange (no dead-letter sink wired)"
259                        );
260                    }
261                    let _ = held;
262                }
263            }
264
265            // Clean up handle and token entries (C2 fix: remove token too)
266            {
267                let mut handles = policy.gap_handles.lock().unwrap_or_else(|e| e.into_inner());
268                handles.remove(&missing_seq);
269            }
270            {
271                let mut tokens = policy.gap_tokens.lock().unwrap_or_else(|e| e.into_inner());
272                tokens.remove(&missing_seq);
273            }
274        });
275
276        {
277            let mut handles = self.gap_handles.lock().unwrap_or_else(|e| e.into_inner());
278            handles.insert(missing_seq, handle);
279        }
280    }
281}
282
283#[async_trait]
284impl ResequencePolicy for StreamPolicy {
285    async fn accept(&self, input: Exchange) -> Vec<Exchange> {
286        let seq = match self.eval_seq(&input).await {
287            Ok(s) => s,
288            Err(e) => {
289                tracing::warn!(
290                    error = %e,
291                    correlation_id = %input.correlation_id(),
292                    "StreamPolicy: sequence expression failed, dropping exchange"
293                );
294                return vec![];
295            }
296        };
297
298        let expected = self.next_expected();
299
300        if seq == expected {
301            // C1: Cancel any gap timer that was armed for this expected sequence
302            // (e.g. next_expected=1, seq 2 arrived and armed timer for 1,
303            //  then seq 1 arrives — cancel the stale timer before draining).
304            self.cancel_gap_timer(expected);
305            // Advance next_expected past this sequence BEFORE draining
306            self.set_next_expected(seq + 1);
307            let mut emitted = vec![input];
308            emitted.append(&mut self.drain_contiguous());
309            emitted
310        } else if seq < expected {
311            // Late or duplicate
312            if self.dedup {
313                // Ignore duplicate
314                tracing::debug!(
315                    seq = seq,
316                    expected = expected,
317                    "StreamPolicy: ignoring duplicate/late sequence (dedup enabled)"
318                );
319                return vec![];
320            }
321            // dedup=false: insert anyway (Camel 4.x behavior)
322            {
323                let mut queue = self.queue.lock().unwrap_or_else(|e| e.into_inner());
324                queue.insert(seq, input);
325            }
326            vec![]
327        } else {
328            // seq > expected: insert into queue + arm gap timer
329            {
330                let mut queue = self.queue.lock().unwrap_or_else(|e| e.into_inner());
331                let queue_len = queue.len();
332
333                // I1: dedup check for held future seqs — redelivered held seq
334                // should be ignored when dedup is enabled.
335                if self.dedup && queue.contains_key(&seq) {
336                    tracing::debug!(
337                        seq = seq,
338                        "StreamPolicy: ignoring redelivered held sequence (dedup enabled)"
339                    );
340                    return vec![];
341                }
342
343                // Capacity overflow check
344                if queue_len >= self.capacity {
345                    match self.on_capacity_exceeded {
346                        CapacityPolicy::DropOldest => {
347                            // Remove the smallest seq in the queue
348                            let oldest_key = queue.keys().next().copied();
349                            if let Some(oldest) = oldest_key {
350                                let dropped = queue.remove(&oldest);
351                                self.cancel_gap_timer(oldest);
352                                tracing::debug!(
353                                    dropped_seq = oldest,
354                                    "StreamPolicy: capacity exceeded, dropped oldest exchange"
355                                );
356                                let _ = dropped;
357                            }
358                        }
359                        // LogAndDrop and any future variant drop the incoming exchange.
360                        _ => {
361                            tracing::warn!(
362                                seq = seq,
363                                capacity = self.capacity,
364                                "StreamPolicy: capacity exceeded, dropping incoming exchange"
365                            );
366                            return vec![];
367                        }
368                    }
369                }
370
371                queue.insert(seq, input);
372            }
373
374            // Arm a gap timer for the expected (missing) sequence if not already armed
375            if !self.has_gap_timer(expected) {
376                self.spawn_gap_timer(expected);
377            }
378
379            vec![]
380        }
381    }
382
383    async fn flush(&self) -> Vec<Exchange> {
384        self.shutdown_started.store(true, Ordering::SeqCst);
385        self.cancel_all_gap_timers();
386
387        let mut queue = self.queue.lock().unwrap_or_else(|e| e.into_inner());
388        let keys: Vec<u64> = queue.keys().copied().collect();
389        let mut held = Vec::new();
390        for k in keys {
391            if let Some(ex) = queue.remove(&k) {
392                held.push(ex);
393            }
394        }
395        held
396    }
397
398    fn name(&self) -> &'static str {
399        "stream-resequencer"
400    }
401
402    fn set_timeout_tx(&self, tx: mpsc::Sender<Exchange>) {
403        self.set_driver_tx(tx);
404    }
405}
406
407// ── Tests ──
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412    use camel_api::exchange::ExchangePattern;
413    use camel_api::message::Message;
414
415    /// Mock expression that reads a property by name.
416    struct PropExpr(String);
417
418    #[async_trait::async_trait]
419    impl Expression for PropExpr {
420        async fn evaluate(
421            &self,
422            exchange: &Exchange,
423        ) -> Result<serde_json::Value, camel_language_api::LanguageError> {
424            Ok(exchange
425                .property(&self.0)
426                .cloned()
427                .unwrap_or(serde_json::Value::Null))
428        }
429    }
430
431    fn mk_exchange(seq: u64) -> Exchange {
432        let mut ex = Exchange::new(Message::new(camel_api::body::Body::Text(format!(
433            "msg-{seq}"
434        ))));
435        ex.set_property("seq", serde_json::json!(seq));
436        ex.pattern = ExchangePattern::InOnly;
437        ex
438    }
439
440    fn default_policy() -> Arc<StreamPolicy> {
441        StreamPolicy::new_cyclic(
442            Arc::new(PropExpr("seq".into())),
443            100,
444            5000,
445            GapPolicy::EmitPartial,
446            CapacityPolicy::LogAndDrop,
447            false,
448        )
449    }
450
451    fn seq_of(ex: &Exchange) -> u64 {
452        ex.property("seq").and_then(|v| v.as_u64()).unwrap_or(0)
453    }
454
455    /// S1: expected=1; receive 2 (held), 1 (emit 1 then drain 2),
456    ///     4 (held), 3 (emit 3 then drain 4)
457    #[tokio::test]
458    async fn stream_emit_contiguous_run() {
459        let policy = default_policy();
460
461        // Receive 2 (held: next_expected=1, seq=2 > 1 → held)
462        let result2 = policy.accept(mk_exchange(2)).await;
463        assert!(
464            result2.is_empty(),
465            "seq 2 should be held (not yet contiguous)"
466        );
467
468        // Receive 1 (expected! → emit 1, then drain 2)
469        let result1 = policy.accept(mk_exchange(1)).await;
470        assert_eq!(result1.len(), 2, "should emit 1 then drain 2");
471        let seqs1: Vec<u64> = result1.iter().map(seq_of).collect();
472        assert_eq!(seqs1, vec![1, 2]);
473
474        // Receive 4 (held: next_expected=3, seq=4 > 3 → held)
475        let result4 = policy.accept(mk_exchange(4)).await;
476        assert!(result4.is_empty(), "seq 4 should be held");
477
478        // Receive 3 (expected! → emit 3, then drain 4)
479        let result3 = policy.accept(mk_exchange(3)).await;
480        assert_eq!(result3.len(), 2, "should emit 3 then drain 4");
481        let seqs3: Vec<u64> = result3.iter().map(seq_of).collect();
482        assert_eq!(seqs3, vec![3, 4]);
483    }
484
485    /// S2: expected=1; receive 2,3 (held); gap timer fires →
486    ///     emit held [2,3], advance expected to 4
487    #[tokio::test]
488    async fn stream_gap_timeout_emit_partial() {
489        let policy = StreamPolicy::new_cyclic(
490            Arc::new(PropExpr("seq".into())),
491            100,
492            50, // 50ms gap timeout
493            GapPolicy::EmitPartial,
494            CapacityPolicy::LogAndDrop,
495            false,
496        );
497
498        let (tx, mut rx) = mpsc::channel::<Exchange>(16);
499        policy.set_timeout_tx(tx);
500
501        // Receive 2 and 3 (held)
502        assert!(policy.accept(mk_exchange(2)).await.is_empty());
503        assert!(policy.accept(mk_exchange(3)).await.is_empty());
504
505        // Wait for gap timer to fire (seq 1 never arrives)
506        let emitted: Vec<Exchange> = tokio::time::timeout(Duration::from_millis(500), async {
507            let mut out = Vec::new();
508            out.push(rx.recv().await.unwrap());
509            out.push(rx.recv().await.unwrap());
510            out
511        })
512        .await
513        .expect("gap timer should fire within 500ms");
514
515        assert_eq!(emitted.len(), 2, "should emit all held exchanges");
516        let seqs: Vec<u64> = emitted.iter().map(seq_of).collect();
517        assert_eq!(seqs, vec![2, 3], "should emit in sequence order");
518
519        // next_expected should have advanced past the gap to max(2,3) + 1 = 4
520        assert_eq!(
521            policy.next_expected(),
522            4,
523            "next_expected should advance past gap to max(drained)+1"
524        );
525    }
526
527    /// S3: queue fills at capacity; next input triggers LogAndDrop
528    #[tokio::test]
529    async fn stream_capacity_exceeded_log_and_drop() {
530        let policy = StreamPolicy::new_cyclic(
531            Arc::new(PropExpr("seq".into())),
532            2, // tiny capacity
533            5000,
534            GapPolicy::EmitPartial,
535            CapacityPolicy::LogAndDrop,
536            false,
537        );
538
539        // Fill queue: seq 3, 4 (next_expected=1)
540        assert!(policy.accept(mk_exchange(3)).await.is_empty());
541        assert!(policy.accept(mk_exchange(4)).await.is_empty());
542
543        {
544            let queue = policy.queue.lock().unwrap();
545            assert_eq!(queue.len(), 2, "queue should be full");
546        }
547
548        // Next input (seq 5) should trigger LogAndDrop
549        let result = policy.accept(mk_exchange(5)).await;
550        assert!(
551            result.is_empty(),
552            "overflow exchange should be dead-lettered (empty result)"
553        );
554
555        {
556            let queue = policy.queue.lock().unwrap();
557            assert_eq!(queue.len(), 2, "queue should stay at capacity");
558        }
559    }
560
561    /// S4: queue fills; next input drops oldest
562    #[tokio::test]
563    async fn stream_capacity_exceeded_drop_oldest() {
564        let policy = StreamPolicy::new_cyclic(
565            Arc::new(PropExpr("seq".into())),
566            2,
567            5000,
568            GapPolicy::EmitPartial,
569            CapacityPolicy::DropOldest,
570            false,
571        );
572
573        // Fill queue: seq 3, 4
574        assert!(policy.accept(mk_exchange(3)).await.is_empty());
575        assert!(policy.accept(mk_exchange(4)).await.is_empty());
576
577        // Next input (seq 5) should drop oldest (seq 3), insert seq 5
578        let result = policy.accept(mk_exchange(5)).await;
579        assert!(
580            result.is_empty(),
581            "overflow with DropOldest should not emit"
582        );
583
584        {
585            let queue = policy.queue.lock().unwrap();
586            assert_eq!(queue.len(), 2, "queue should still be at capacity");
587            assert!(!queue.contains_key(&3), "oldest seq 3 should be dropped");
588            assert!(queue.contains_key(&4), "seq 4 should remain");
589            assert!(queue.contains_key(&5), "seq 5 should be inserted");
590        }
591    }
592
593    /// S5: with dedup=true, duplicate seq is ignored
594    #[tokio::test]
595    async fn stream_dedup_on_ignores_duplicate() {
596        let policy = StreamPolicy::new_cyclic(
597            Arc::new(PropExpr("seq".into())),
598            100,
599            5000,
600            GapPolicy::EmitPartial,
601            CapacityPolicy::LogAndDrop,
602            true, // dedup on
603        );
604
605        // Receive 1 (emit)
606        let result1 = policy.accept(mk_exchange(1)).await;
607        assert_eq!(result1.len(), 1, "seq 1 should be emitted");
608
609        // Receive 1 again — should be ignored (duplicate with dedup=true)
610        let result2 = policy.accept(mk_exchange(1)).await;
611        assert!(
612            result2.is_empty(),
613            "duplicate seq 1 should be ignored with dedup on"
614        );
615    }
616
617    /// S6: with dedup=false (default), duplicate seq is inserted
618    ///     (Camel 4.x behavior)
619    #[tokio::test]
620    async fn stream_dedup_off_inserts_duplicate() {
621        let policy = StreamPolicy::new_cyclic(
622            Arc::new(PropExpr("seq".into())),
623            100,
624            5000,
625            GapPolicy::EmitPartial,
626            CapacityPolicy::LogAndDrop,
627            false, // dedup off (default)
628        );
629
630        // Receive 1 (emit)
631        let result1 = policy.accept(mk_exchange(1)).await;
632        assert_eq!(result1.len(), 1, "seq 1 should be emitted");
633
634        // Receive 1 again — with dedup=false, duplicate is inserted
635        let result2 = policy.accept(mk_exchange(1)).await;
636        assert!(
637            result2.is_empty(),
638            "duplicate seq 1 with dedup off should be inserted, not emitted"
639        );
640
641        {
642            let queue = policy.queue.lock().unwrap();
643            assert!(
644                queue.contains_key(&1),
645                "duplicate seq 1 should be in queue (dedup off)"
646            );
647        }
648    }
649
650    /// S7: flush() emits remaining in seq order
651    #[tokio::test]
652    async fn stream_flush_emits_remaining_sorted() {
653        let policy = default_policy();
654
655        // Emit 1 first
656        assert!(!policy.accept(mk_exchange(1)).await.is_empty());
657
658        // Now next_expected=2. Hold 5, 3
659        assert!(policy.accept(mk_exchange(5)).await.is_empty());
660        assert!(policy.accept(mk_exchange(3)).await.is_empty());
661
662        // Flush
663        let flushed = policy.flush().await;
664        assert_eq!(flushed.len(), 2, "should emit all remaining held exchanges");
665        let seqs: Vec<u64> = flushed.iter().map(seq_of).collect();
666        assert_eq!(seqs, vec![3, 5], "should be in sequence order");
667    }
668
669    /// S8: seq < next_expected after advance → dedup behavior
670    #[tokio::test]
671    async fn stream_late_sequence_after_advance() {
672        // Policy with dedup=true
673        let policy_dedup = StreamPolicy::new_cyclic(
674            Arc::new(PropExpr("seq".into())),
675            100,
676            50,
677            GapPolicy::EmitPartial,
678            CapacityPolicy::LogAndDrop,
679            true,
680        );
681
682        // Advance past seq 1,2,3 by setting next_expected to 5
683        {
684            let mut ne = policy_dedup.next_expected.lock().unwrap();
685            *ne = 5;
686        }
687
688        // seq 3 < 5, dedup=true → ignored
689        let result = policy_dedup.accept(mk_exchange(3)).await;
690        assert!(
691            result.is_empty(),
692            "late seq with dedup=true should be ignored"
693        );
694
695        // Policy with dedup=false
696        let policy_no_dedup = StreamPolicy::new_cyclic(
697            Arc::new(PropExpr("seq".into())),
698            100,
699            50,
700            GapPolicy::EmitPartial,
701            CapacityPolicy::LogAndDrop,
702            false,
703        );
704
705        {
706            let mut ne = policy_no_dedup.next_expected.lock().unwrap();
707            *ne = 5;
708        }
709
710        // seq 3 < 5, dedup=false → inserted into queue
711        let result = policy_no_dedup.accept(mk_exchange(3)).await;
712        assert!(
713            result.is_empty(),
714            "late seq with dedup=false should be inserted"
715        );
716
717        {
718            let queue = policy_no_dedup.queue.lock().unwrap();
719            assert!(
720                queue.contains_key(&3),
721                "late seq should be in queue with dedup off"
722            );
723        }
724    }
725
726    /// M4/C1: next_expected=1 → seq 2 arrives (arms timer for 1) →
727    /// seq 1 arrives within timeout → timer 1 must NOT fire → no corruption
728    #[tokio::test]
729    async fn stream_stale_gap_timer_cancelled_on_normal_arrival() {
730        let policy = StreamPolicy::new_cyclic(
731            Arc::new(PropExpr("seq".into())),
732            100,
733            50, // 50ms gap timeout
734            GapPolicy::DropAndLog,
735            CapacityPolicy::LogAndDrop,
736            false,
737        );
738
739        let (tx, mut rx) = mpsc::channel::<Exchange>(16);
740        policy.set_timeout_tx(tx);
741
742        // next_expected=1, seq 2 arrives → held, gap timer armed for seq 1
743        assert!(policy.accept(mk_exchange(2)).await.is_empty());
744        assert!(
745            policy.has_gap_timer(1),
746            "gap timer should be armed for seq 1"
747        );
748
749        // seq 1 arrives within timeout → cancels timer, emits 1 and drains 2
750        let result = policy.accept(mk_exchange(1)).await;
751        assert_eq!(result.len(), 2, "should emit seq 1 and drained seq 2");
752        let seqs: Vec<u64> = result.iter().map(seq_of).collect();
753        assert_eq!(seqs, vec![1, 2]);
754
755        // Verify gap timer for seq 1 was cancelled (C1 fix)
756        assert!(
757            !policy.has_gap_timer(1),
758            "gap timer for seq 1 should be cancelled after normal arrival"
759        );
760
761        // Verify no stale timer fire: the mpsc channel should NOT receive a
762        // corrupt re-emission from the old gap timer.
763        tokio::time::sleep(Duration::from_millis(200)).await;
764        match rx.try_recv() {
765            Ok(ex) => {
766                panic!(
767                    "stale gap timer fired and sent exchange with seq={} — corruption!",
768                    seq_of(&ex)
769                );
770            }
771            Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {
772                // Expected: no stale emission
773            }
774            Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {}
775        }
776    }
777
778    /// M4/I1: dedup=true, seq 5 held, redeliver seq 5 → ignored (original preserved)
779    #[tokio::test]
780    async fn stream_dedup_held_future_seq() {
781        let policy = StreamPolicy::new_cyclic(
782            Arc::new(PropExpr("seq".into())),
783            100,
784            5000,
785            GapPolicy::EmitPartial,
786            CapacityPolicy::LogAndDrop,
787            true, // dedup on
788        );
789
790        // next_expected=1, send seq 5 → held
791        assert!(policy.accept(mk_exchange(5)).await.is_empty());
792
793        // Redeliver seq 5 → should be ignored with dedup=true
794        let result = policy.accept(mk_exchange(5)).await;
795        assert!(result.is_empty(), "redelivered held seq should be ignored");
796
797        // Queue should still contain only one seq 5
798        {
799            let queue = policy.queue.lock().unwrap();
800            assert_eq!(queue.len(), 1, "queue should still have exactly one entry");
801            assert!(queue.contains_key(&5), "seq 5 should still be in queue");
802        }
803    }
804}