1use 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 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
41pub(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#[derive(Debug, Clone, Default)]
89pub struct PendingQueueMetrics {
90 pub pending_messages_received_total: u64,
92 pub pending_messages_evicted_total: u64,
94 pub pending_messages_dropped_overflow_total: u64,
96 pub pending_messages_expired_total: u64,
98 pub pending_messages_eviction_failures_total: u64,
100 pub pending_queue_invariant_violations_total: u64,
102 pub pending_messages_current: usize,
104 pub pending_bytes_current: usize,
106 pub pending_messages_per_peer: HashMap<String, usize>,
108}
109
110#[derive(Default)]
114pub(crate) struct PendingDecryptionQueue {
115 queues: HashMap<String, VecDeque<PendingDecryptMessage>>,
117 global_order: VecDeque<EntryRef>,
119 live_sequences: HashSet<u64>,
121 total: usize,
123 total_bytes: usize,
125 peer_bytes: HashMap<String, usize>,
127 next_seq: u64,
129 metrics: PendingQueueMetrics,
131 peer_overflow_hits: HashMap<String, u32>,
133 drop_warning_counters: HashMap<String, u64>,
135}
136
137impl PendingDecryptionQueue {
138 pub(crate) fn metrics(&self) -> &PendingQueueMetrics {
142 &self.metrics
143 }
144
145 #[cfg(test)]
147 pub(crate) fn is_empty(&self) -> bool {
148 self.queues.is_empty()
149 }
150
151 #[cfg(test)]
153 pub(crate) fn contains_peer(&self, peer_id: &str) -> bool {
154 self.queues.contains_key(peer_id)
155 }
156
157 #[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 #[cfg(test)]
165 pub(crate) fn total(&self) -> usize {
166 self.total
167 }
168
169 #[cfg(test)]
171 pub(crate) fn total_bytes(&self) -> usize {
172 self.total_bytes
173 }
174
175 #[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 #[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 #[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 #[cfg(test)]
195 pub(crate) fn overflow_hits_tracked(&self) -> usize {
196 self.peer_overflow_hits.len()
197 }
198
199 #[cfg(test)]
201 pub(crate) fn message_footprint(message: &Message) -> usize {
202 Self::message_bytes(message)
203 }
204
205 #[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 #[cfg(test)]
217 pub(crate) fn corrupt_clear_global_order(&mut self) {
218 self.global_order.clear();
219 }
220
221 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 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 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 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 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 #[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 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 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 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 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 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 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}