Skip to main content

offline_protocol/protocol/
decryption_queue.rs

1//! Bounded pending decryption queue for encrypted messages received before
2//! the MLS session is ready, with per-peer and global limits, TTL expiration,
3//! and configurable overflow policies.
4
5use crate::config::{OverflowPolicy, PendingQueueConfig};
6use offline_protocol_core::Message;
7use offline_protocol_transport::TransportType;
8use std::collections::{HashMap, HashSet, VecDeque};
9use std::time::{Duration as StdDuration, Instant};
10use tracing::{debug, warn};
11
12const TTL_SPIKE_WARN_THRESHOLD: usize = 25;
13const PEER_PRESSURE_WARN_EVERY: u32 = 10;
14const DROP_WARN_EVERY: u64 = 100;
15const EVICTION_FAILURE_WARN_EVERY: u64 = 10;
16
17#[derive(Clone)]
18pub(crate) struct PendingDecryptMessage {
19    pub(crate) peer_id: String,
20    pub(crate) message_id: String,
21    pub(crate) received_at: Instant,
22    pub(crate) sequence: u64,
23    pub(crate) message: Message,
24    /// Transport the frame arrived on, when the caller knew it. Recorded so the
25    /// drain can send the deferred delivery ACK directly (see the
26    /// deferred-acknowledgement atom in
27    /// `docs/state-machines/delivery-and-acks.md`) instead of waiting for the
28    /// sender's next resend to
29    /// hit the duplicate re-ACK path. `None` when the message was enqueued from
30    /// a context with no transport (tests, or a re-queue by the handler during a
31    /// drain, which re-processes with no live arrival transport) — the drain
32    /// then falls back to the resend-driven ACK.
33    pub(crate) received_via: Option<TransportType>,
34}
35
36#[derive(Clone)]
37struct EntryRef {
38    peer_id: String,
39    message_id: String,
40    sequence: u64,
41}
42
43/// A message dropped from the pending queue (overflow or TTL expiry),
44/// together with the machine-readable drop reason. Returned to the protocol
45/// layer so it can surface user-visible consequences — in particular an
46/// encrypted media chunk that was already ACKed and dedup-marked on receipt
47/// and therefore can never be recovered.
48pub(crate) struct DroppedPendingMessage {
49    pub(crate) message: Message,
50    pub(crate) reason: &'static str,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54enum QueueLimit {
55    PerPeer,
56    Global,
57    PerPeerBytes,
58    GlobalBytes,
59}
60
61impl QueueLimit {
62    fn as_str(self) -> &'static str {
63        match self {
64            Self::PerPeer => "per_peer",
65            Self::Global => "global",
66            Self::PerPeerBytes => "per_peer_bytes",
67            Self::GlobalBytes => "global_bytes",
68        }
69    }
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73enum DropReason {
74    OverflowDropOldest,
75    OverflowDropNewest,
76    TtlExpired,
77}
78
79impl DropReason {
80    fn as_str(self) -> &'static str {
81        match self {
82            Self::OverflowDropOldest => "overflow_drop_oldest",
83            Self::OverflowDropNewest => "overflow_drop_newest",
84            Self::TtlExpired => "ttl_expired",
85        }
86    }
87}
88
89/// Counters and gauges for pending encrypted message queue pressure.
90#[derive(Debug, Clone, Default)]
91pub struct PendingQueueMetrics {
92    /// Total encrypted messages received before session readiness.
93    pub pending_messages_received_total: u64,
94    /// Total queued messages evicted from pending storage.
95    pub pending_messages_evicted_total: u64,
96    /// Total messages dropped due to overflow policy decisions.
97    pub pending_messages_dropped_overflow_total: u64,
98    /// Total pending messages expired due to TTL.
99    pub pending_messages_expired_total: u64,
100    /// Number of failed eviction attempts while enforcing hard bounds.
101    pub pending_messages_eviction_failures_total: u64,
102    /// Number of detected pending queue invariant violations.
103    pub pending_queue_invariant_violations_total: u64,
104    /// Current number of messages in pending queues across all peers.
105    pub pending_messages_current: usize,
106    /// Current payload bytes (content plus binary content) queued across all peers.
107    pub pending_bytes_current: usize,
108    /// Current per-peer pending queue sizes.
109    pub pending_messages_per_peer: HashMap<String, usize>,
110}
111
112/// Encapsulates the bounded pending decryption queue: encrypted messages
113/// received before the MLS session is ready, with per-peer and global limits,
114/// TTL expiration, and configurable overflow policies.
115#[derive(Default)]
116pub(crate) struct PendingDecryptionQueue {
117    /// Per-peer FIFO queues of encrypted messages.
118    queues: HashMap<String, VecDeque<PendingDecryptMessage>>,
119    /// Global insertion order index for deterministic global oldest eviction.
120    global_order: VecDeque<EntryRef>,
121    /// Live sequence IDs currently present in pending queues.
122    live_sequences: HashSet<u64>,
123    /// Current number of pending encrypted messages across all peers.
124    total: usize,
125    /// Current payload bytes queued across all peers.
126    total_bytes: usize,
127    /// Current payload bytes queued per peer.
128    peer_bytes: HashMap<String, usize>,
129    /// Monotonic sequence assigned on enqueue for deterministic tie-breaking.
130    next_seq: u64,
131    /// Pending queue observability counters and gauges.
132    metrics: PendingQueueMetrics,
133    /// Overflow hit count per peer for warning signal emission.
134    peer_overflow_hits: HashMap<String, u32>,
135    /// Drop warning counters used for log-rate limiting by reason/limit.
136    drop_warning_counters: HashMap<String, u64>,
137}
138
139impl PendingDecryptionQueue {
140    // ---- Public accessors ----
141
142    /// Returns a reference to the current metrics snapshot.
143    pub(crate) fn metrics(&self) -> &PendingQueueMetrics {
144        &self.metrics
145    }
146
147    /// Returns true when no peers have pending messages.
148    #[cfg(test)]
149    pub(crate) fn is_empty(&self) -> bool {
150        self.queues.is_empty()
151    }
152
153    /// Returns true when the given peer has queued messages.
154    #[cfg(test)]
155    pub(crate) fn contains_peer(&self, peer_id: &str) -> bool {
156        self.queues.contains_key(peer_id)
157    }
158
159    /// Returns the number of pending messages for a specific peer.
160    #[cfg(test)]
161    pub(crate) fn peer_queue_len(&self, peer_id: &str) -> usize {
162        self.queues.get(peer_id).map(VecDeque::len).unwrap_or(0)
163    }
164
165    /// Returns the total number of pending messages across all peers.
166    #[cfg(test)]
167    pub(crate) fn total(&self) -> usize {
168        self.total
169    }
170
171    /// Returns the total payload bytes queued across all peers.
172    #[cfg(test)]
173    pub(crate) fn total_bytes(&self) -> usize {
174        self.total_bytes
175    }
176
177    /// Returns a reference to a specific entry in a peer's queue.
178    #[cfg(test)]
179    pub(crate) fn peek_entry(&self, peer_id: &str, index: usize) -> Option<&PendingDecryptMessage> {
180        self.queues.get(peer_id).and_then(|q| q.get(index))
181    }
182
183    /// Returns the maximum queue length across all peers.
184    #[cfg(test)]
185    pub(crate) fn max_peer_queue_len(&self) -> usize {
186        self.queues.values().map(VecDeque::len).max().unwrap_or(0)
187    }
188
189    /// Returns whether an overflow-pressure counter is tracked for a peer.
190    #[cfg(test)]
191    pub(crate) fn has_overflow_hits(&self, peer_id: &str) -> bool {
192        self.peer_overflow_hits.contains_key(peer_id)
193    }
194
195    /// Returns the number of peers with a tracked overflow-pressure counter.
196    #[cfg(test)]
197    pub(crate) fn overflow_hits_tracked(&self) -> usize {
198        self.peer_overflow_hits.len()
199    }
200
201    /// Returns the retained-memory footprint the queue charges for a message.
202    #[cfg(test)]
203    pub(crate) fn message_footprint(message: &Message) -> usize {
204        Self::message_bytes(message)
205    }
206
207    /// Overrides the `received_at` timestamp of the front entry for a peer (fault injection).
208    #[cfg(test)]
209    pub(crate) fn set_front_received_at(&mut self, peer_id: &str, at: Instant) {
210        if let Some(queue) = self.queues.get_mut(peer_id) {
211            if let Some(front) = queue.front_mut() {
212                front.received_at = at;
213            }
214        }
215    }
216
217    /// Clears the global order index to simulate index corruption (fault injection).
218    #[cfg(test)]
219    pub(crate) fn corrupt_clear_global_order(&mut self) {
220        self.global_order.clear();
221    }
222
223    // ---- Internal helpers ----
224
225    fn ttl(config: &PendingQueueConfig) -> StdDuration {
226        StdDuration::from_millis(config.pending_ttl_ms)
227    }
228
229    fn update_peer_gauge(&mut self, peer_id: &str) {
230        let peer_len = self.queues.get(peer_id).map(VecDeque::len).unwrap_or(0);
231        if peer_len == 0 {
232            self.metrics.pending_messages_per_peer.remove(peer_id);
233        } else {
234            self.metrics
235                .pending_messages_per_peer
236                .insert(peer_id.to_string(), peer_len);
237        }
238    }
239
240    fn update_current_gauge(&mut self) {
241        self.metrics.pending_messages_current = self.total;
242        self.metrics.pending_bytes_current = self.total_bytes;
243    }
244
245    /// Retained-memory footprint of a queued message, for budget accounting.
246    ///
247    /// The queue stores a full [`Message`] clone, so this must reflect every
248    /// owned field a peer can inflate — not just the text/binary payload.
249    /// Counting only `content`/`binary_content` (as this once did) let a
250    /// crafted message with a tiny payload but a megabyte-scale `sender` or
251    /// `metadata` map evade the per-peer/global byte budgets entirely and drive
252    /// the queue to OOM while `pending_bytes_current` reported near-zero. The id
253    /// fields (`sender`/`recipient`/`app_id`) are additionally length-capped at
254    /// the type boundary (`offline_protocol_core::MAX_ID_LEN`); `metadata` and
255    /// the media strings are not, so this accounting is what bounds them.
256    ///
257    /// Fixed per-entry overhead (queue node, index entry, the duplicated
258    /// `peer_id`/`message_id` strings) is intentionally *not* added here — the
259    /// per-peer and global *count* caps already bound that fixed cost; the byte
260    /// budget exists to bound variable field/payload bloat.
261    fn message_bytes(message: &Message) -> usize {
262        let metadata_bytes: usize = message
263            .metadata
264            .iter()
265            .map(|(k, v)| k.len() + v.len())
266            .sum();
267        let media_bytes = message
268            .media_metadata
269            .as_ref()
270            .map(|m| {
271                m.mime_type.len()
272                    + m.file_name.len()
273                    + m.thumbnail_base64.as_ref().map(String::len).unwrap_or(0)
274            })
275            .unwrap_or(0);
276        message.content.len()
277            + message
278                .binary_content
279                .as_ref()
280                .map(|binary| binary.len())
281                .unwrap_or(0)
282            + message.sender.as_str().len()
283            + message.recipient.as_str().len()
284            + message.app_id.as_str().len()
285            + metadata_bytes
286            + media_bytes
287    }
288
289    fn peer_bytes_for(&self, peer_id: &str) -> usize {
290        self.peer_bytes.get(peer_id).copied().unwrap_or(0)
291    }
292
293    fn next_sequence(&mut self) -> u64 {
294        let seq = self.next_seq;
295        self.next_seq = self.next_seq.wrapping_add(1);
296        seq
297    }
298
299    fn is_entry_expired(
300        config: &PendingQueueConfig,
301        entry: &PendingDecryptMessage,
302        now: Instant,
303    ) -> bool {
304        now.saturating_duration_since(entry.received_at) >= Self::ttl(config)
305    }
306
307    fn cleanup_global_order_front(&mut self) {
308        while let Some(front) = self.global_order.front() {
309            if self.live_sequences.contains(&front.sequence) {
310                break;
311            }
312            self.global_order.pop_front();
313        }
314    }
315
316    fn remove_entry_by_sequence(
317        &mut self,
318        peer_id: &str,
319        sequence: u64,
320    ) -> Option<PendingDecryptMessage> {
321        let (removed, queue_empty) = {
322            let queue = self.queues.get_mut(peer_id)?;
323            let position = queue.iter().position(|entry| entry.sequence == sequence)?;
324            let removed = queue.remove(position)?;
325            (removed, queue.is_empty())
326        };
327
328        self.live_sequences.remove(&sequence);
329        self.total = self.total.saturating_sub(1);
330        let removed_bytes = Self::message_bytes(&removed.message);
331        self.total_bytes = self.total_bytes.saturating_sub(removed_bytes);
332        if let Some(bytes) = self.peer_bytes.get_mut(peer_id) {
333            *bytes = bytes.saturating_sub(removed_bytes);
334        }
335        self.update_current_gauge();
336
337        if queue_empty {
338            self.queues.remove(peer_id);
339            self.peer_bytes.remove(peer_id);
340            // Prune the overflow-pressure counter alongside the queue. It is
341            // keyed by the (attacker-controllable) wire sender and was
342            // previously never removed, so a flood of distinct forged senders
343            // leaked one permanent entry each. A peer with no queued messages
344            // has no live pressure to track.
345            self.peer_overflow_hits.remove(peer_id);
346        }
347        self.update_peer_gauge(peer_id);
348        Some(removed)
349    }
350
351    fn record_drop(
352        &mut self,
353        reason: DropReason,
354        limit_triggered: Option<QueueLimit>,
355        peer_id: &str,
356        message_id: &str,
357        overflow_policy: OverflowPolicy,
358    ) {
359        if matches!(
360            reason,
361            DropReason::OverflowDropOldest | DropReason::TtlExpired
362        ) {
363            self.metrics.pending_messages_evicted_total = self
364                .metrics
365                .pending_messages_evicted_total
366                .saturating_add(1);
367        }
368
369        if matches!(
370            reason,
371            DropReason::OverflowDropOldest | DropReason::OverflowDropNewest
372        ) {
373            self.metrics.pending_messages_dropped_overflow_total = self
374                .metrics
375                .pending_messages_dropped_overflow_total
376                .saturating_add(1);
377        }
378
379        if reason == DropReason::TtlExpired {
380            self.metrics.pending_messages_expired_total = self
381                .metrics
382                .pending_messages_expired_total
383                .saturating_add(1);
384        }
385
386        let limit_label = limit_triggered.map(QueueLimit::as_str).unwrap_or("ttl");
387        let counter_key = format!("{}:{}", reason.as_str(), limit_label);
388        let drop_count = {
389            let counter = self.drop_warning_counters.entry(counter_key).or_insert(0);
390            *counter = counter.saturating_add(1);
391            *counter
392        };
393
394        debug!(
395            reason = reason.as_str(),
396            peer_id = %peer_id,
397            message_id = %message_id,
398            queue_size = self.total,
399            limit_triggered = limit_label,
400            overflow_policy = ?overflow_policy,
401            "Dropped pending encrypted message"
402        );
403        if drop_count == 1 || drop_count % DROP_WARN_EVERY == 0 {
404            warn!(
405                reason = reason.as_str(),
406                limit_triggered = limit_label,
407                drops = drop_count,
408                queue_size = self.total,
409                "Pending encrypted message drops continuing"
410            );
411        }
412    }
413
414    fn record_eviction_failure(
415        &mut self,
416        limit_triggered: QueueLimit,
417        peer_id: &str,
418        message_id: &str,
419        detail: &str,
420    ) {
421        self.metrics.pending_messages_eviction_failures_total = self
422            .metrics
423            .pending_messages_eviction_failures_total
424            .saturating_add(1);
425
426        let count = self.metrics.pending_messages_eviction_failures_total;
427        if count == 1 || count.is_multiple_of(EVICTION_FAILURE_WARN_EVERY) {
428            warn!(
429                limit_triggered = limit_triggered.as_str(),
430                peer_id = %peer_id,
431                message_id = %message_id,
432                failures = count,
433                detail = detail,
434                queue_size = self.total,
435                "Pending queue eviction failure detected"
436            );
437        } else {
438            debug!(
439                limit_triggered = limit_triggered.as_str(),
440                peer_id = %peer_id,
441                message_id = %message_id,
442                detail = detail,
443                "Pending queue eviction failure detected"
444            );
445        }
446    }
447
448    fn verify_invariants(&mut self, context: &str) {
449        let per_peer_sum: usize = self.queues.values().map(VecDeque::len).sum();
450        let live_count = self.live_sequences.len();
451        let current_gauge = self.metrics.pending_messages_current;
452        let total = self.total;
453        let peer_bytes_sum: usize = self.peer_bytes.values().sum();
454        let valid = per_peer_sum == total
455            && live_count == total
456            && current_gauge == total
457            && peer_bytes_sum == self.total_bytes;
458        if valid {
459            return;
460        }
461
462        self.metrics.pending_queue_invariant_violations_total = self
463            .metrics
464            .pending_queue_invariant_violations_total
465            .saturating_add(1);
466        warn!(
467            context = context,
468            per_peer_sum,
469            live_count,
470            current_gauge,
471            total,
472            violations = self.metrics.pending_queue_invariant_violations_total,
473            "Pending queue invariant violation detected"
474        );
475    }
476
477    fn record_peer_overflow_pressure(&mut self, peer_id: &str, per_peer_limit: usize) {
478        let hits = self
479            .peer_overflow_hits
480            .entry(peer_id.to_string())
481            .or_insert(0);
482        *hits = hits.saturating_add(1);
483        if hits.is_multiple_of(PEER_PRESSURE_WARN_EVERY) {
484            warn!(
485                peer_id = %peer_id,
486                overflow_hits = *hits,
487                per_peer_limit,
488                "Peer repeatedly hitting pending queue limits"
489            );
490        }
491    }
492
493    fn evict_global_oldest(
494        &mut self,
495        reason: DropReason,
496        limit_triggered: QueueLimit,
497        overflow_policy: OverflowPolicy,
498    ) -> Option<PendingDecryptMessage> {
499        self.cleanup_global_order_front();
500        let entry_ref = self.global_order.pop_front()?;
501        if !self.live_sequences.contains(&entry_ref.sequence) {
502            return None;
503        }
504        if let Some(evicted) = self.remove_entry_by_sequence(&entry_ref.peer_id, entry_ref.sequence)
505        {
506            self.record_drop(
507                reason,
508                Some(limit_triggered),
509                &evicted.peer_id,
510                &evicted.message_id,
511                overflow_policy,
512            );
513            return Some(evicted);
514        }
515        warn!(
516            peer_id = %entry_ref.peer_id,
517            message_id = %entry_ref.message_id,
518            sequence = entry_ref.sequence,
519            "Failed to evict pending message by global order reference"
520        );
521        None
522    }
523
524    // ---- Public queue operations ----
525
526    /// Prunes expired entries for a specific peer. Returns the dropped entries.
527    pub(crate) fn prune_expired_for_peer(
528        &mut self,
529        config: &PendingQueueConfig,
530        peer_id: &str,
531        now: Instant,
532    ) -> Vec<DroppedPendingMessage> {
533        let mut expired_entries = Vec::new();
534        let mut expired_sequences = Vec::new();
535        let mut expired_ids = Vec::new();
536        if let Some(queue) = self.queues.get(peer_id) {
537            for entry in queue {
538                if Self::is_entry_expired(config, entry, now) {
539                    expired_sequences.push(entry.sequence);
540                    expired_ids.push(entry.message_id.clone());
541                } else {
542                    break;
543                }
544            }
545        }
546
547        let overflow_policy = config.overflow_policy;
548        for (sequence, message_id) in expired_sequences.into_iter().zip(expired_ids) {
549            if let Some(expired) = self.remove_entry_by_sequence(peer_id, sequence) {
550                self.record_drop(
551                    DropReason::TtlExpired,
552                    None,
553                    peer_id,
554                    &message_id,
555                    overflow_policy,
556                );
557                expired_entries.push(DroppedPendingMessage {
558                    message: expired.message,
559                    reason: DropReason::TtlExpired.as_str(),
560                });
561            }
562        }
563
564        if expired_entries.len() >= TTL_SPIKE_WARN_THRESHOLD {
565            warn!(
566                peer_id = %peer_id,
567                expired = expired_entries.len(),
568                ttl_ms = config.pending_ttl_ms,
569                "Pending encrypted message TTL eviction spike"
570            );
571        }
572
573        self.cleanup_global_order_front();
574        expired_entries
575    }
576
577    /// Prunes expired entries from the front of the global order. Returns the dropped entries.
578    pub(crate) fn prune_expired_global_front(
579        &mut self,
580        config: &PendingQueueConfig,
581        now: Instant,
582        max_evictions: usize,
583    ) -> Vec<DroppedPendingMessage> {
584        let mut evicted = Vec::new();
585        let overflow_policy = config.overflow_policy;
586        while evicted.len() < max_evictions {
587            self.cleanup_global_order_front();
588            let Some(front) = self.global_order.front().cloned() else {
589                break;
590            };
591            if !self.live_sequences.contains(&front.sequence) {
592                self.global_order.pop_front();
593                continue;
594            }
595
596            let Some(queue) = self.queues.get(&front.peer_id) else {
597                self.global_order.pop_front();
598                continue;
599            };
600            let Some(entry) = queue.iter().find(|entry| entry.sequence == front.sequence) else {
601                self.global_order.pop_front();
602                continue;
603            };
604
605            if !Self::is_entry_expired(config, entry, now) {
606                break;
607            }
608
609            if let Some(expired) = self.remove_entry_by_sequence(&front.peer_id, front.sequence) {
610                self.global_order.pop_front();
611                self.record_drop(
612                    DropReason::TtlExpired,
613                    None,
614                    &expired.peer_id,
615                    &expired.message_id,
616                    overflow_policy,
617                );
618                evicted.push(DroppedPendingMessage {
619                    message: expired.message,
620                    reason: DropReason::TtlExpired.as_str(),
621                });
622            } else {
623                self.global_order.pop_front();
624            }
625        }
626
627        if evicted.len() >= TTL_SPIKE_WARN_THRESHOLD {
628            warn!(
629                expired = evicted.len(),
630                ttl_ms = config.pending_ttl_ms,
631                "Pending encrypted message TTL eviction spike"
632            );
633        }
634
635        evicted
636    }
637
638    /// Enqueues an encrypted message that arrived before the MLS session was
639    /// ready, without recording an arrival transport (the drain will fall back
640    /// to the resend-driven ACK). See [`Self::enqueue_via`]. Used only by tests;
641    /// production always routes through `enqueue_via`.
642    #[cfg(test)]
643    pub(crate) fn enqueue(
644        &mut self,
645        config: &PendingQueueConfig,
646        sender: &str,
647        message: &Message,
648    ) -> Vec<DroppedPendingMessage> {
649        self.enqueue_via(config, sender, message, None)
650    }
651
652    /// Enqueues an encrypted message that arrived before the MLS session was
653    /// ready, recording the transport it arrived on so the drain can ACK it
654    /// directly.
655    ///
656    /// Returns every message dropped in the process — TTL-expired entries,
657    /// entries evicted to make room, or the incoming message itself when it
658    /// could not be admitted — so the protocol layer can surface the loss.
659    pub(crate) fn enqueue_via(
660        &mut self,
661        config: &PendingQueueConfig,
662        sender: &str,
663        message: &Message,
664        arrival_transport: Option<TransportType>,
665    ) -> Vec<DroppedPendingMessage> {
666        self.metrics.pending_messages_received_total = self
667            .metrics
668            .pending_messages_received_total
669            .saturating_add(1);
670        let incoming_message_id = message.id.as_str();
671
672        let now = Instant::now();
673        let mut dropped = self.prune_expired_for_peer(config, sender, now);
674        dropped.extend(self.prune_expired_global_front(config, now, 64));
675
676        // Idempotent by message id: under the deferred-ACK model the sender
677        // resends an undecryptable message on its ACK-timeout, and each resend
678        // re-enters `enqueue`. Stacking duplicate copies would burn the per-peer
679        // budget (evicting *other* peers' recoverable messages) and surface the
680        // same message twice on drain. If this id is already queued for the
681        // peer, treat the resend as a no-op (the original copy is authoritative
682        // and its TTL is measured from first receipt).
683        if self.queues.get(sender).is_some_and(|queue| {
684            queue
685                .iter()
686                .any(|entry| entry.message_id == incoming_message_id)
687        }) {
688            return dropped;
689        }
690
691        let per_peer_limit = config.max_pending_per_peer;
692        let global_limit = config.max_pending_global;
693        let per_peer_bytes_limit = config.max_pending_bytes_per_peer;
694        let global_bytes_limit = config.max_pending_bytes_global;
695        let overflow_policy = config.overflow_policy;
696        let incoming_bytes = Self::message_bytes(message);
697
698        // A message that can never fit within the byte budgets is dropped
699        // outright — evicting the entire queue would not make room for it.
700        // This also guarantees the byte-eviction loops below terminate.
701        if incoming_bytes > per_peer_bytes_limit || incoming_bytes > global_bytes_limit {
702            let limit = if incoming_bytes > per_peer_bytes_limit {
703                QueueLimit::PerPeerBytes
704            } else {
705                QueueLimit::GlobalBytes
706            };
707            self.record_drop(
708                DropReason::OverflowDropNewest,
709                Some(limit),
710                sender,
711                &incoming_message_id,
712                overflow_policy,
713            );
714            dropped.push(DroppedPendingMessage {
715                message: message.clone(),
716                reason: DropReason::OverflowDropNewest.as_str(),
717            });
718            return dropped;
719        }
720
721        let peer_len = self.queues.get(sender).map(VecDeque::len).unwrap_or(0);
722        if peer_len >= per_peer_limit {
723            self.record_peer_overflow_pressure(sender, per_peer_limit);
724            match overflow_policy {
725                OverflowPolicy::DropNewest => {
726                    self.record_drop(
727                        DropReason::OverflowDropNewest,
728                        Some(QueueLimit::PerPeer),
729                        sender,
730                        &incoming_message_id,
731                        overflow_policy,
732                    );
733                    dropped.push(DroppedPendingMessage {
734                        message: message.clone(),
735                        reason: DropReason::OverflowDropNewest.as_str(),
736                    });
737                    return dropped;
738                }
739                OverflowPolicy::DropOldest => {
740                    let evicted_sequence = self
741                        .queues
742                        .get(sender)
743                        .and_then(|queue| queue.front().map(|entry| entry.sequence));
744                    let mut evicted_any = false;
745                    if let Some(sequence) = evicted_sequence {
746                        if let Some(evicted) = self.remove_entry_by_sequence(sender, sequence) {
747                            self.record_drop(
748                                DropReason::OverflowDropOldest,
749                                Some(QueueLimit::PerPeer),
750                                &evicted.peer_id,
751                                &evicted.message_id,
752                                overflow_policy,
753                            );
754                            dropped.push(DroppedPendingMessage {
755                                message: evicted.message,
756                                reason: DropReason::OverflowDropOldest.as_str(),
757                            });
758                            evicted_any = true;
759                        }
760                    }
761                    if !evicted_any {
762                        self.record_eviction_failure(
763                            QueueLimit::PerPeer,
764                            sender,
765                            &incoming_message_id,
766                            "drop_oldest failed to evict per-peer oldest",
767                        );
768                        self.record_drop(
769                            DropReason::OverflowDropNewest,
770                            Some(QueueLimit::PerPeer),
771                            sender,
772                            &incoming_message_id,
773                            overflow_policy,
774                        );
775                        dropped.push(DroppedPendingMessage {
776                            message: message.clone(),
777                            reason: DropReason::OverflowDropNewest.as_str(),
778                        });
779                        return dropped;
780                    }
781                }
782            }
783        }
784        let peer_len_after = self.queues.get(sender).map(VecDeque::len).unwrap_or(0);
785        if peer_len_after >= per_peer_limit {
786            self.record_eviction_failure(
787                QueueLimit::PerPeer,
788                sender,
789                &incoming_message_id,
790                "per-peer limit still saturated after eviction",
791            );
792            self.record_drop(
793                DropReason::OverflowDropNewest,
794                Some(QueueLimit::PerPeer),
795                sender,
796                &incoming_message_id,
797                overflow_policy,
798            );
799            dropped.push(DroppedPendingMessage {
800                message: message.clone(),
801                reason: DropReason::OverflowDropNewest.as_str(),
802            });
803            return dropped;
804        }
805
806        if self.peer_bytes_for(sender) + incoming_bytes > per_peer_bytes_limit {
807            self.record_peer_overflow_pressure(sender, per_peer_limit);
808            if overflow_policy == OverflowPolicy::DropNewest {
809                self.record_drop(
810                    DropReason::OverflowDropNewest,
811                    Some(QueueLimit::PerPeerBytes),
812                    sender,
813                    &incoming_message_id,
814                    overflow_policy,
815                );
816                dropped.push(DroppedPendingMessage {
817                    message: message.clone(),
818                    reason: DropReason::OverflowDropNewest.as_str(),
819                });
820                return dropped;
821            }
822            // DropOldest: evict from the peer's front until the incoming
823            // message fits. Terminates: each eviction shrinks the peer's byte
824            // total, and an empty queue leaves it at 0 (the incoming message
825            // fits by the oversized pre-check above).
826            while self.peer_bytes_for(sender) + incoming_bytes > per_peer_bytes_limit {
827                let evicted_sequence = self
828                    .queues
829                    .get(sender)
830                    .and_then(|queue| queue.front().map(|entry| entry.sequence));
831                let evicted = evicted_sequence
832                    .and_then(|sequence| self.remove_entry_by_sequence(sender, sequence));
833                match evicted {
834                    Some(evicted) => {
835                        self.record_drop(
836                            DropReason::OverflowDropOldest,
837                            Some(QueueLimit::PerPeerBytes),
838                            &evicted.peer_id,
839                            &evicted.message_id,
840                            overflow_policy,
841                        );
842                        dropped.push(DroppedPendingMessage {
843                            message: evicted.message,
844                            reason: DropReason::OverflowDropOldest.as_str(),
845                        });
846                    }
847                    None => {
848                        self.record_eviction_failure(
849                            QueueLimit::PerPeerBytes,
850                            sender,
851                            &incoming_message_id,
852                            "drop_oldest failed to evict per-peer oldest for byte budget",
853                        );
854                        self.record_drop(
855                            DropReason::OverflowDropNewest,
856                            Some(QueueLimit::PerPeerBytes),
857                            sender,
858                            &incoming_message_id,
859                            overflow_policy,
860                        );
861                        dropped.push(DroppedPendingMessage {
862                            message: message.clone(),
863                            reason: DropReason::OverflowDropNewest.as_str(),
864                        });
865                        return dropped;
866                    }
867                }
868            }
869        }
870
871        if self.total >= global_limit {
872            warn!(
873                queue_size = self.total,
874                global_limit, "Pending encrypted queue at global pressure limit"
875            );
876            match overflow_policy {
877                OverflowPolicy::DropNewest => {
878                    self.record_drop(
879                        DropReason::OverflowDropNewest,
880                        Some(QueueLimit::Global),
881                        sender,
882                        &incoming_message_id,
883                        overflow_policy,
884                    );
885                    dropped.push(DroppedPendingMessage {
886                        message: message.clone(),
887                        reason: DropReason::OverflowDropNewest.as_str(),
888                    });
889                    return dropped;
890                }
891                OverflowPolicy::DropOldest => {
892                    while self.total >= global_limit {
893                        match self.evict_global_oldest(
894                            DropReason::OverflowDropOldest,
895                            QueueLimit::Global,
896                            overflow_policy,
897                        ) {
898                            Some(evicted) => dropped.push(DroppedPendingMessage {
899                                message: evicted.message,
900                                reason: DropReason::OverflowDropOldest.as_str(),
901                            }),
902                            None => {
903                                self.record_eviction_failure(
904                                    QueueLimit::Global,
905                                    sender,
906                                    &incoming_message_id,
907                                    "drop_oldest failed to evict global oldest",
908                                );
909                                break;
910                            }
911                        }
912                    }
913                }
914            }
915        }
916        if self.total >= global_limit {
917            self.record_drop(
918                DropReason::OverflowDropNewest,
919                Some(QueueLimit::Global),
920                sender,
921                &incoming_message_id,
922                overflow_policy,
923            );
924            dropped.push(DroppedPendingMessage {
925                message: message.clone(),
926                reason: DropReason::OverflowDropNewest.as_str(),
927            });
928            return dropped;
929        }
930
931        if self.total_bytes + incoming_bytes > global_bytes_limit {
932            warn!(
933                queue_bytes = self.total_bytes,
934                global_bytes_limit, "Pending encrypted queue at global byte budget"
935            );
936            if overflow_policy == OverflowPolicy::DropNewest {
937                self.record_drop(
938                    DropReason::OverflowDropNewest,
939                    Some(QueueLimit::GlobalBytes),
940                    sender,
941                    &incoming_message_id,
942                    overflow_policy,
943                );
944                dropped.push(DroppedPendingMessage {
945                    message: message.clone(),
946                    reason: DropReason::OverflowDropNewest.as_str(),
947                });
948                return dropped;
949            }
950            while self.total_bytes + incoming_bytes > global_bytes_limit {
951                match self.evict_global_oldest(
952                    DropReason::OverflowDropOldest,
953                    QueueLimit::GlobalBytes,
954                    overflow_policy,
955                ) {
956                    Some(evicted) => dropped.push(DroppedPendingMessage {
957                        message: evicted.message,
958                        reason: DropReason::OverflowDropOldest.as_str(),
959                    }),
960                    None => {
961                        self.record_eviction_failure(
962                            QueueLimit::GlobalBytes,
963                            sender,
964                            &incoming_message_id,
965                            "drop_oldest failed to evict global oldest for byte budget",
966                        );
967                        break;
968                    }
969                }
970            }
971            if self.total_bytes + incoming_bytes > global_bytes_limit {
972                self.record_drop(
973                    DropReason::OverflowDropNewest,
974                    Some(QueueLimit::GlobalBytes),
975                    sender,
976                    &incoming_message_id,
977                    overflow_policy,
978                );
979                dropped.push(DroppedPendingMessage {
980                    message: message.clone(),
981                    reason: DropReason::OverflowDropNewest.as_str(),
982                });
983                return dropped;
984            }
985        }
986
987        let sequence = self.next_sequence();
988        let entry = PendingDecryptMessage {
989            peer_id: sender.to_string(),
990            message_id: incoming_message_id.clone(),
991            received_at: now,
992            sequence,
993            message: message.clone(),
994            received_via: arrival_transport,
995        };
996
997        self.queues
998            .entry(sender.to_string())
999            .or_default()
1000            .push_back(entry.clone());
1001        self.global_order.push_back(EntryRef {
1002            peer_id: sender.to_string(),
1003            message_id: entry.message_id.clone(),
1004            sequence,
1005        });
1006        self.live_sequences.insert(sequence);
1007        self.total = self.total.saturating_add(1);
1008        self.total_bytes = self.total_bytes.saturating_add(incoming_bytes);
1009        let peer_bytes = self.peer_bytes.entry(sender.to_string()).or_insert(0);
1010        *peer_bytes = peer_bytes.saturating_add(incoming_bytes);
1011        self.update_peer_gauge(sender);
1012        self.update_current_gauge();
1013        self.cleanup_global_order_front();
1014        self.verify_invariants("enqueue");
1015
1016        let peer_len_post_insert = self.queues.get(sender).map(VecDeque::len).unwrap_or(0);
1017        let over_global = self.total > global_limit || self.total_bytes > global_bytes_limit;
1018        let over_peer = peer_len_post_insert > per_peer_limit
1019            || self.peer_bytes_for(sender) > per_peer_bytes_limit;
1020        if over_global || over_peer {
1021            let _ = self.remove_entry_by_sequence(sender, sequence);
1022            let limit = if over_global {
1023                QueueLimit::Global
1024            } else {
1025                QueueLimit::PerPeer
1026            };
1027            self.record_eviction_failure(
1028                limit,
1029                sender,
1030                &incoming_message_id,
1031                "post-insert hard-bound check failed; rolled back enqueue",
1032            );
1033            self.record_drop(
1034                DropReason::OverflowDropNewest,
1035                Some(limit),
1036                sender,
1037                &incoming_message_id,
1038                overflow_policy,
1039            );
1040            dropped.push(DroppedPendingMessage {
1041                message: message.clone(),
1042                reason: DropReason::OverflowDropNewest.as_str(),
1043            });
1044        }
1045
1046        dropped
1047    }
1048
1049    /// Drains all pending messages for a peer, updating bookkeeping.
1050    /// Returns the drained messages for the caller to process.
1051    pub(crate) fn drain_for_peer(
1052        &mut self,
1053        config: &PendingQueueConfig,
1054        sender: &str,
1055    ) -> Vec<PendingDecryptMessage> {
1056        let now = Instant::now();
1057        let _ = self.prune_expired_for_peer(config, sender, now);
1058
1059        let messages = match self.queues.remove(sender) {
1060            Some(msgs) => msgs,
1061            None => return Vec::new(),
1062        };
1063
1064        if messages.is_empty() {
1065            return Vec::new();
1066        }
1067
1068        let drained: Vec<PendingDecryptMessage> = messages.into_iter().collect();
1069        for entry in &drained {
1070            self.live_sequences.remove(&entry.sequence);
1071            self.total = self.total.saturating_sub(1);
1072            self.total_bytes = self
1073                .total_bytes
1074                .saturating_sub(Self::message_bytes(&entry.message));
1075        }
1076        self.peer_bytes.remove(sender);
1077        // Prune the overflow-pressure counter with the drained queue (see the
1078        // matching cleanup in `remove_entry_by_sequence`) so it cannot outlive
1079        // the peer's queued messages and leak per-sender.
1080        self.peer_overflow_hits.remove(sender);
1081        self.update_peer_gauge(sender);
1082        self.update_current_gauge();
1083        self.cleanup_global_order_front();
1084        self.verify_invariants("drain_for_peer");
1085
1086        drained
1087    }
1088}