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