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>,
34}
35
36#[derive(Clone)]
37struct EntryRef {
38 peer_id: String,
39 message_id: String,
40 sequence: u64,
41}
42
43pub(crate) struct DroppedPendingMessage {
49 pub(crate) message: Message,
50 pub(crate) reason: &'static str,
51}
52
53pub(crate) struct EnqueueOutcome {
69 pub(crate) admitted: bool,
70 pub(crate) dropped: Vec<DroppedPendingMessage>,
71 pub(crate) refused: Option<DroppedPendingMessage>,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76enum Admission {
77 Admitted,
79 AlreadyQueued,
81 Refused,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86enum QueueLimit {
87 PerPeer,
88 Global,
89 PerPeerBytes,
90 GlobalBytes,
91}
92
93impl QueueLimit {
94 fn as_str(self) -> &'static str {
95 match self {
96 Self::PerPeer => "per_peer",
97 Self::Global => "global",
98 Self::PerPeerBytes => "per_peer_bytes",
99 Self::GlobalBytes => "global_bytes",
100 }
101 }
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105enum DropReason {
106 OverflowDropOldest,
107 OverflowDropNewest,
108 TtlExpired,
109}
110
111impl DropReason {
112 fn as_str(self) -> &'static str {
113 match self {
114 Self::OverflowDropOldest => "overflow_drop_oldest",
115 Self::OverflowDropNewest => "overflow_drop_newest",
116 Self::TtlExpired => "ttl_expired",
117 }
118 }
119}
120
121#[derive(Debug, Clone, Default)]
123pub struct PendingQueueMetrics {
124 pub pending_messages_received_total: u64,
126 pub pending_messages_evicted_total: u64,
128 pub pending_messages_dropped_overflow_total: u64,
130 pub pending_messages_expired_total: u64,
132 pub pending_messages_eviction_failures_total: u64,
134 pub pending_queue_invariant_violations_total: u64,
136 pub pending_messages_current: usize,
138 pub pending_bytes_current: usize,
140 pub pending_messages_per_peer: HashMap<String, usize>,
142}
143
144#[derive(Clone, Default)]
148pub(crate) struct PendingDecryptionQueue {
149 queues: HashMap<String, VecDeque<PendingDecryptMessage>>,
151 global_order: VecDeque<EntryRef>,
153 live_sequences: HashSet<u64>,
155 total: usize,
157 total_bytes: usize,
159 peer_bytes: HashMap<String, usize>,
161 next_seq: u64,
163 metrics: PendingQueueMetrics,
165 peer_overflow_hits: HashMap<String, u32>,
167 drop_warning_counters: HashMap<String, u64>,
169}
170
171impl PendingDecryptionQueue {
172 pub(crate) fn metrics(&self) -> &PendingQueueMetrics {
176 &self.metrics
177 }
178
179 #[cfg(test)]
181 pub(crate) fn is_empty(&self) -> bool {
182 self.queues.is_empty()
183 }
184
185 #[cfg(test)]
187 pub(crate) fn contains_peer(&self, peer_id: &str) -> bool {
188 self.queues.contains_key(peer_id)
189 }
190
191 #[cfg(test)]
193 pub(crate) fn peer_queue_len(&self, peer_id: &str) -> usize {
194 self.queues.get(peer_id).map(VecDeque::len).unwrap_or(0)
195 }
196
197 #[cfg(test)]
199 pub(crate) fn total(&self) -> usize {
200 self.total
201 }
202
203 #[cfg(test)]
205 pub(crate) fn total_bytes(&self) -> usize {
206 self.total_bytes
207 }
208
209 #[cfg(test)]
211 pub(crate) fn peek_entry(&self, peer_id: &str, index: usize) -> Option<&PendingDecryptMessage> {
212 self.queues.get(peer_id).and_then(|q| q.get(index))
213 }
214
215 #[cfg(test)]
217 pub(crate) fn max_peer_queue_len(&self) -> usize {
218 self.queues.values().map(VecDeque::len).max().unwrap_or(0)
219 }
220
221 #[cfg(test)]
223 pub(crate) fn has_overflow_hits(&self, peer_id: &str) -> bool {
224 self.peer_overflow_hits.contains_key(peer_id)
225 }
226
227 #[cfg(test)]
229 pub(crate) fn overflow_hits_tracked(&self) -> usize {
230 self.peer_overflow_hits.len()
231 }
232
233 #[cfg(test)]
235 pub(crate) fn message_footprint(message: &Message) -> usize {
236 Self::message_bytes(message)
237 }
238
239 #[cfg(test)]
241 pub(crate) fn set_front_received_at(&mut self, peer_id: &str, at: Instant) {
242 if let Some(queue) = self.queues.get_mut(peer_id) {
243 if let Some(front) = queue.front_mut() {
244 front.received_at = at;
245 }
246 }
247 }
248
249 #[cfg(test)]
251 pub(crate) fn corrupt_clear_global_order(&mut self) {
252 self.global_order.clear();
253 }
254
255 fn ttl(config: &PendingQueueConfig) -> StdDuration {
258 StdDuration::from_millis(config.pending_ttl_ms)
259 }
260
261 fn update_peer_gauge(&mut self, peer_id: &str) {
262 let peer_len = self.queues.get(peer_id).map(VecDeque::len).unwrap_or(0);
263 if peer_len == 0 {
264 self.metrics.pending_messages_per_peer.remove(peer_id);
265 } else {
266 self.metrics
267 .pending_messages_per_peer
268 .insert(peer_id.to_string(), peer_len);
269 }
270 }
271
272 fn update_current_gauge(&mut self) {
273 self.metrics.pending_messages_current = self.total;
274 self.metrics.pending_bytes_current = self.total_bytes;
275 }
276
277 fn message_bytes(message: &Message) -> usize {
294 let metadata_bytes: usize = message
295 .metadata
296 .iter()
297 .map(|(k, v)| k.len() + v.len())
298 .sum();
299 let media_bytes = message
300 .media_metadata
301 .as_ref()
302 .map(|m| {
303 m.mime_type.len()
304 + m.file_name.len()
305 + m.thumbnail_base64.as_ref().map(String::len).unwrap_or(0)
306 })
307 .unwrap_or(0);
308 message.content.len()
309 + message
310 .binary_content
311 .as_ref()
312 .map(|binary| binary.len())
313 .unwrap_or(0)
314 + message.sender.as_str().len()
315 + message.recipient.as_str().len()
316 + message.app_id.as_str().len()
317 + metadata_bytes
318 + media_bytes
319 }
320
321 fn peer_bytes_for(&self, peer_id: &str) -> usize {
322 self.peer_bytes.get(peer_id).copied().unwrap_or(0)
323 }
324
325 fn next_sequence(&mut self) -> u64 {
326 let seq = self.next_seq;
327 self.next_seq = self.next_seq.wrapping_add(1);
328 seq
329 }
330
331 fn is_entry_expired(
332 config: &PendingQueueConfig,
333 entry: &PendingDecryptMessage,
334 now: Instant,
335 ) -> bool {
336 now.saturating_duration_since(entry.received_at) >= Self::ttl(config)
337 }
338
339 fn cleanup_global_order_front(&mut self) {
340 while let Some(front) = self.global_order.front() {
341 if self.live_sequences.contains(&front.sequence) {
342 break;
343 }
344 self.global_order.pop_front();
345 }
346 }
347
348 fn remove_entry_by_sequence(
349 &mut self,
350 peer_id: &str,
351 sequence: u64,
352 ) -> Option<PendingDecryptMessage> {
353 let (removed, queue_empty) = {
354 let queue = self.queues.get_mut(peer_id)?;
355 let position = queue.iter().position(|entry| entry.sequence == sequence)?;
356 let removed = queue.remove(position)?;
357 (removed, queue.is_empty())
358 };
359
360 self.live_sequences.remove(&sequence);
361 self.total = self.total.saturating_sub(1);
362 let removed_bytes = Self::message_bytes(&removed.message);
363 self.total_bytes = self.total_bytes.saturating_sub(removed_bytes);
364 if let Some(bytes) = self.peer_bytes.get_mut(peer_id) {
365 *bytes = bytes.saturating_sub(removed_bytes);
366 }
367 self.update_current_gauge();
368
369 if queue_empty {
370 self.queues.remove(peer_id);
371 self.peer_bytes.remove(peer_id);
372 self.peer_overflow_hits.remove(peer_id);
378 }
379 self.update_peer_gauge(peer_id);
380 Some(removed)
381 }
382
383 fn record_drop(
384 &mut self,
385 reason: DropReason,
386 limit_triggered: Option<QueueLimit>,
387 peer_id: &str,
388 message_id: &str,
389 overflow_policy: OverflowPolicy,
390 ) {
391 if matches!(
392 reason,
393 DropReason::OverflowDropOldest | DropReason::TtlExpired
394 ) {
395 self.metrics.pending_messages_evicted_total = self
396 .metrics
397 .pending_messages_evicted_total
398 .saturating_add(1);
399 }
400
401 if matches!(
402 reason,
403 DropReason::OverflowDropOldest | DropReason::OverflowDropNewest
404 ) {
405 self.metrics.pending_messages_dropped_overflow_total = self
406 .metrics
407 .pending_messages_dropped_overflow_total
408 .saturating_add(1);
409 }
410
411 if reason == DropReason::TtlExpired {
412 self.metrics.pending_messages_expired_total = self
413 .metrics
414 .pending_messages_expired_total
415 .saturating_add(1);
416 }
417
418 let limit_label = limit_triggered.map(QueueLimit::as_str).unwrap_or("ttl");
419 let counter_key = format!("{}:{}", reason.as_str(), limit_label);
420 let drop_count = {
421 let counter = self.drop_warning_counters.entry(counter_key).or_insert(0);
422 *counter = counter.saturating_add(1);
423 *counter
424 };
425
426 debug!(
427 reason = reason.as_str(),
428 peer_id = %peer_id,
429 message_id = %message_id,
430 queue_size = self.total,
431 limit_triggered = limit_label,
432 overflow_policy = ?overflow_policy,
433 "Dropped pending encrypted message"
434 );
435 if drop_count == 1 || drop_count % DROP_WARN_EVERY == 0 {
436 warn!(
437 reason = reason.as_str(),
438 limit_triggered = limit_label,
439 drops = drop_count,
440 queue_size = self.total,
441 "Pending encrypted message drops continuing"
442 );
443 }
444 }
445
446 fn record_eviction_failure(
447 &mut self,
448 limit_triggered: QueueLimit,
449 peer_id: &str,
450 message_id: &str,
451 detail: &str,
452 ) {
453 self.metrics.pending_messages_eviction_failures_total = self
454 .metrics
455 .pending_messages_eviction_failures_total
456 .saturating_add(1);
457
458 let count = self.metrics.pending_messages_eviction_failures_total;
459 if count == 1 || count.is_multiple_of(EVICTION_FAILURE_WARN_EVERY) {
460 warn!(
461 limit_triggered = limit_triggered.as_str(),
462 peer_id = %peer_id,
463 message_id = %message_id,
464 failures = count,
465 detail = detail,
466 queue_size = self.total,
467 "Pending queue eviction failure detected"
468 );
469 } else {
470 debug!(
471 limit_triggered = limit_triggered.as_str(),
472 peer_id = %peer_id,
473 message_id = %message_id,
474 detail = detail,
475 "Pending queue eviction failure detected"
476 );
477 }
478 }
479
480 fn verify_invariants(&mut self, context: &str) {
481 let per_peer_sum: usize = self.queues.values().map(VecDeque::len).sum();
482 let live_count = self.live_sequences.len();
483 let current_gauge = self.metrics.pending_messages_current;
484 let total = self.total;
485 let peer_bytes_sum: usize = self.peer_bytes.values().sum();
486 let valid = per_peer_sum == total
487 && live_count == total
488 && current_gauge == total
489 && peer_bytes_sum == self.total_bytes;
490 if valid {
491 return;
492 }
493
494 self.metrics.pending_queue_invariant_violations_total = self
495 .metrics
496 .pending_queue_invariant_violations_total
497 .saturating_add(1);
498 warn!(
499 context = context,
500 per_peer_sum,
501 live_count,
502 current_gauge,
503 total,
504 violations = self.metrics.pending_queue_invariant_violations_total,
505 "Pending queue invariant violation detected"
506 );
507 }
508
509 fn record_peer_overflow_pressure(&mut self, peer_id: &str, per_peer_limit: usize) {
510 let hits = self
511 .peer_overflow_hits
512 .entry(peer_id.to_string())
513 .or_insert(0);
514 *hits = hits.saturating_add(1);
515 if hits.is_multiple_of(PEER_PRESSURE_WARN_EVERY) {
516 warn!(
517 peer_id = %peer_id,
518 overflow_hits = *hits,
519 per_peer_limit,
520 "Peer repeatedly hitting pending queue limits"
521 );
522 }
523 }
524
525 fn evict_global_oldest(
526 &mut self,
527 reason: DropReason,
528 limit_triggered: QueueLimit,
529 overflow_policy: OverflowPolicy,
530 ) -> Option<PendingDecryptMessage> {
531 self.cleanup_global_order_front();
532 let entry_ref = self.global_order.pop_front()?;
533 if !self.live_sequences.contains(&entry_ref.sequence) {
534 return None;
535 }
536 if let Some(evicted) = self.remove_entry_by_sequence(&entry_ref.peer_id, entry_ref.sequence)
537 {
538 self.record_drop(
539 reason,
540 Some(limit_triggered),
541 &evicted.peer_id,
542 &evicted.message_id,
543 overflow_policy,
544 );
545 return Some(evicted);
546 }
547 warn!(
548 peer_id = %entry_ref.peer_id,
549 message_id = %entry_ref.message_id,
550 sequence = entry_ref.sequence,
551 "Failed to evict pending message by global order reference"
552 );
553 None
554 }
555
556 pub(crate) fn prune_expired_for_peer(
560 &mut self,
561 config: &PendingQueueConfig,
562 peer_id: &str,
563 now: Instant,
564 ) -> Vec<DroppedPendingMessage> {
565 let mut expired_entries = Vec::new();
566 let mut expired_sequences = Vec::new();
567 let mut expired_ids = Vec::new();
568 if let Some(queue) = self.queues.get(peer_id) {
569 for entry in queue {
570 if Self::is_entry_expired(config, entry, now) {
571 expired_sequences.push(entry.sequence);
572 expired_ids.push(entry.message_id.clone());
573 } else {
574 break;
575 }
576 }
577 }
578
579 let overflow_policy = config.overflow_policy;
580 for (sequence, message_id) in expired_sequences.into_iter().zip(expired_ids) {
581 if let Some(expired) = self.remove_entry_by_sequence(peer_id, sequence) {
582 self.record_drop(
583 DropReason::TtlExpired,
584 None,
585 peer_id,
586 &message_id,
587 overflow_policy,
588 );
589 expired_entries.push(DroppedPendingMessage {
590 message: expired.message,
591 reason: DropReason::TtlExpired.as_str(),
592 });
593 }
594 }
595
596 if expired_entries.len() >= TTL_SPIKE_WARN_THRESHOLD {
597 warn!(
598 peer_id = %peer_id,
599 expired = expired_entries.len(),
600 ttl_ms = config.pending_ttl_ms,
601 "Pending encrypted message TTL eviction spike"
602 );
603 }
604
605 self.cleanup_global_order_front();
606 expired_entries
607 }
608
609 pub(crate) fn prune_expired_global_front(
611 &mut self,
612 config: &PendingQueueConfig,
613 now: Instant,
614 max_evictions: usize,
615 ) -> Vec<DroppedPendingMessage> {
616 let mut evicted = Vec::new();
617 let overflow_policy = config.overflow_policy;
618 while evicted.len() < max_evictions {
619 self.cleanup_global_order_front();
620 let Some(front) = self.global_order.front().cloned() else {
621 break;
622 };
623 if !self.live_sequences.contains(&front.sequence) {
624 self.global_order.pop_front();
625 continue;
626 }
627
628 let Some(queue) = self.queues.get(&front.peer_id) else {
629 self.global_order.pop_front();
630 continue;
631 };
632 let Some(entry) = queue.iter().find(|entry| entry.sequence == front.sequence) else {
633 self.global_order.pop_front();
634 continue;
635 };
636
637 if !Self::is_entry_expired(config, entry, now) {
638 break;
639 }
640
641 if let Some(expired) = self.remove_entry_by_sequence(&front.peer_id, front.sequence) {
642 self.global_order.pop_front();
643 self.record_drop(
644 DropReason::TtlExpired,
645 None,
646 &expired.peer_id,
647 &expired.message_id,
648 overflow_policy,
649 );
650 evicted.push(DroppedPendingMessage {
651 message: expired.message,
652 reason: DropReason::TtlExpired.as_str(),
653 });
654 } else {
655 self.global_order.pop_front();
656 }
657 }
658
659 if evicted.len() >= TTL_SPIKE_WARN_THRESHOLD {
660 warn!(
661 expired = evicted.len(),
662 ttl_ms = config.pending_ttl_ms,
663 "Pending encrypted message TTL eviction spike"
664 );
665 }
666
667 evicted
668 }
669
670 #[cfg(test)]
675 pub(crate) fn enqueue(
676 &mut self,
677 config: &PendingQueueConfig,
678 sender: &str,
679 message: &Message,
680 ) -> Vec<DroppedPendingMessage> {
681 let outcome = self.enqueue_via(config, sender, message, None);
682 let mut dropped = outcome.dropped;
683 dropped.extend(outcome.refused);
684 dropped
685 }
686
687 pub(crate) fn enqueue_via(
697 &mut self,
698 config: &PendingQueueConfig,
699 sender: &str,
700 message: &Message,
701 arrival_transport: Option<TransportType>,
702 ) -> EnqueueOutcome {
703 let mut dropped = Vec::new();
704 let admission =
705 self.enqueue_via_inner(config, sender, message, arrival_transport, &mut dropped);
706 EnqueueOutcome {
707 admitted: admission == Admission::Admitted,
708 dropped,
709 refused: (admission == Admission::Refused).then(|| DroppedPendingMessage {
710 message: message.clone(),
711 reason: DropReason::OverflowDropNewest.as_str(),
712 }),
713 }
714 }
715
716 fn enqueue_via_inner(
719 &mut self,
720 config: &PendingQueueConfig,
721 sender: &str,
722 message: &Message,
723 arrival_transport: Option<TransportType>,
724 dropped: &mut Vec<DroppedPendingMessage>,
725 ) -> Admission {
726 self.metrics.pending_messages_received_total = self
727 .metrics
728 .pending_messages_received_total
729 .saturating_add(1);
730 let incoming_message_id = message.id.as_str();
731
732 let now = Instant::now();
733 dropped.extend(self.prune_expired_for_peer(config, sender, now));
734 dropped.extend(self.prune_expired_global_front(config, now, 64));
735
736 if self.queues.get(sender).is_some_and(|queue| {
744 queue
745 .iter()
746 .any(|entry| entry.message_id == incoming_message_id)
747 }) {
748 return Admission::AlreadyQueued;
749 }
750
751 let per_peer_limit = config.max_pending_per_peer;
752 let global_limit = config.max_pending_global;
753 let per_peer_bytes_limit = config.max_pending_bytes_per_peer;
754 let global_bytes_limit = config.max_pending_bytes_global;
755 let overflow_policy = config.overflow_policy;
756 let incoming_bytes = Self::message_bytes(message);
757
758 if incoming_bytes > per_peer_bytes_limit || incoming_bytes > global_bytes_limit {
762 let limit = if incoming_bytes > per_peer_bytes_limit {
763 QueueLimit::PerPeerBytes
764 } else {
765 QueueLimit::GlobalBytes
766 };
767 self.record_drop(
768 DropReason::OverflowDropNewest,
769 Some(limit),
770 sender,
771 &incoming_message_id,
772 overflow_policy,
773 );
774 return Admission::Refused;
775 }
776
777 let peer_len = self.queues.get(sender).map(VecDeque::len).unwrap_or(0);
778 if peer_len >= per_peer_limit {
779 self.record_peer_overflow_pressure(sender, per_peer_limit);
780 match overflow_policy {
781 OverflowPolicy::DropNewest => {
782 self.record_drop(
783 DropReason::OverflowDropNewest,
784 Some(QueueLimit::PerPeer),
785 sender,
786 &incoming_message_id,
787 overflow_policy,
788 );
789 return Admission::Refused;
790 }
791 OverflowPolicy::DropOldest => {
792 let evicted_sequence = self
793 .queues
794 .get(sender)
795 .and_then(|queue| queue.front().map(|entry| entry.sequence));
796 let mut evicted_any = false;
797 if let Some(sequence) = evicted_sequence {
798 if let Some(evicted) = self.remove_entry_by_sequence(sender, sequence) {
799 self.record_drop(
800 DropReason::OverflowDropOldest,
801 Some(QueueLimit::PerPeer),
802 &evicted.peer_id,
803 &evicted.message_id,
804 overflow_policy,
805 );
806 dropped.push(DroppedPendingMessage {
807 message: evicted.message,
808 reason: DropReason::OverflowDropOldest.as_str(),
809 });
810 evicted_any = true;
811 }
812 }
813 if !evicted_any {
814 self.record_eviction_failure(
815 QueueLimit::PerPeer,
816 sender,
817 &incoming_message_id,
818 "drop_oldest failed to evict per-peer oldest",
819 );
820 self.record_drop(
821 DropReason::OverflowDropNewest,
822 Some(QueueLimit::PerPeer),
823 sender,
824 &incoming_message_id,
825 overflow_policy,
826 );
827 return Admission::Refused;
828 }
829 }
830 }
831 }
832 let peer_len_after = self.queues.get(sender).map(VecDeque::len).unwrap_or(0);
833 if peer_len_after >= per_peer_limit {
834 self.record_eviction_failure(
835 QueueLimit::PerPeer,
836 sender,
837 &incoming_message_id,
838 "per-peer limit still saturated after eviction",
839 );
840 self.record_drop(
841 DropReason::OverflowDropNewest,
842 Some(QueueLimit::PerPeer),
843 sender,
844 &incoming_message_id,
845 overflow_policy,
846 );
847 return Admission::Refused;
848 }
849
850 if self.peer_bytes_for(sender) + incoming_bytes > per_peer_bytes_limit {
851 self.record_peer_overflow_pressure(sender, per_peer_limit);
852 if overflow_policy == OverflowPolicy::DropNewest {
853 self.record_drop(
854 DropReason::OverflowDropNewest,
855 Some(QueueLimit::PerPeerBytes),
856 sender,
857 &incoming_message_id,
858 overflow_policy,
859 );
860 return Admission::Refused;
861 }
862 while self.peer_bytes_for(sender) + incoming_bytes > per_peer_bytes_limit {
867 let evicted_sequence = self
868 .queues
869 .get(sender)
870 .and_then(|queue| queue.front().map(|entry| entry.sequence));
871 let evicted = evicted_sequence
872 .and_then(|sequence| self.remove_entry_by_sequence(sender, sequence));
873 match evicted {
874 Some(evicted) => {
875 self.record_drop(
876 DropReason::OverflowDropOldest,
877 Some(QueueLimit::PerPeerBytes),
878 &evicted.peer_id,
879 &evicted.message_id,
880 overflow_policy,
881 );
882 dropped.push(DroppedPendingMessage {
883 message: evicted.message,
884 reason: DropReason::OverflowDropOldest.as_str(),
885 });
886 }
887 None => {
888 self.record_eviction_failure(
889 QueueLimit::PerPeerBytes,
890 sender,
891 &incoming_message_id,
892 "drop_oldest failed to evict per-peer oldest for byte budget",
893 );
894 self.record_drop(
895 DropReason::OverflowDropNewest,
896 Some(QueueLimit::PerPeerBytes),
897 sender,
898 &incoming_message_id,
899 overflow_policy,
900 );
901 return Admission::Refused;
902 }
903 }
904 }
905 }
906
907 if self.total >= global_limit {
908 warn!(
909 queue_size = self.total,
910 global_limit, "Pending encrypted queue at global pressure limit"
911 );
912 match overflow_policy {
913 OverflowPolicy::DropNewest => {
914 self.record_drop(
915 DropReason::OverflowDropNewest,
916 Some(QueueLimit::Global),
917 sender,
918 &incoming_message_id,
919 overflow_policy,
920 );
921 return Admission::Refused;
922 }
923 OverflowPolicy::DropOldest => {
924 while self.total >= global_limit {
925 match self.evict_global_oldest(
926 DropReason::OverflowDropOldest,
927 QueueLimit::Global,
928 overflow_policy,
929 ) {
930 Some(evicted) => dropped.push(DroppedPendingMessage {
931 message: evicted.message,
932 reason: DropReason::OverflowDropOldest.as_str(),
933 }),
934 None => {
935 self.record_eviction_failure(
936 QueueLimit::Global,
937 sender,
938 &incoming_message_id,
939 "drop_oldest failed to evict global oldest",
940 );
941 break;
942 }
943 }
944 }
945 }
946 }
947 }
948 if self.total >= global_limit {
949 self.record_drop(
950 DropReason::OverflowDropNewest,
951 Some(QueueLimit::Global),
952 sender,
953 &incoming_message_id,
954 overflow_policy,
955 );
956 return Admission::Refused;
957 }
958
959 if self.total_bytes + incoming_bytes > global_bytes_limit {
960 warn!(
961 queue_bytes = self.total_bytes,
962 global_bytes_limit, "Pending encrypted queue at global byte budget"
963 );
964 if overflow_policy == OverflowPolicy::DropNewest {
965 self.record_drop(
966 DropReason::OverflowDropNewest,
967 Some(QueueLimit::GlobalBytes),
968 sender,
969 &incoming_message_id,
970 overflow_policy,
971 );
972 return Admission::Refused;
973 }
974 while self.total_bytes + incoming_bytes > global_bytes_limit {
975 match self.evict_global_oldest(
976 DropReason::OverflowDropOldest,
977 QueueLimit::GlobalBytes,
978 overflow_policy,
979 ) {
980 Some(evicted) => dropped.push(DroppedPendingMessage {
981 message: evicted.message,
982 reason: DropReason::OverflowDropOldest.as_str(),
983 }),
984 None => {
985 self.record_eviction_failure(
986 QueueLimit::GlobalBytes,
987 sender,
988 &incoming_message_id,
989 "drop_oldest failed to evict global oldest for byte budget",
990 );
991 break;
992 }
993 }
994 }
995 if self.total_bytes + incoming_bytes > global_bytes_limit {
996 self.record_drop(
997 DropReason::OverflowDropNewest,
998 Some(QueueLimit::GlobalBytes),
999 sender,
1000 &incoming_message_id,
1001 overflow_policy,
1002 );
1003 return Admission::Refused;
1004 }
1005 }
1006
1007 let sequence = self.next_sequence();
1008 let entry = PendingDecryptMessage {
1009 peer_id: sender.to_string(),
1010 message_id: incoming_message_id.clone(),
1011 received_at: now,
1012 sequence,
1013 message: message.clone(),
1014 received_via: arrival_transport,
1015 };
1016
1017 self.queues
1018 .entry(sender.to_string())
1019 .or_default()
1020 .push_back(entry.clone());
1021 self.global_order.push_back(EntryRef {
1022 peer_id: sender.to_string(),
1023 message_id: entry.message_id.clone(),
1024 sequence,
1025 });
1026 self.live_sequences.insert(sequence);
1027 self.total = self.total.saturating_add(1);
1028 self.total_bytes = self.total_bytes.saturating_add(incoming_bytes);
1029 let peer_bytes = self.peer_bytes.entry(sender.to_string()).or_insert(0);
1030 *peer_bytes = peer_bytes.saturating_add(incoming_bytes);
1031 self.update_peer_gauge(sender);
1032 self.update_current_gauge();
1033 self.cleanup_global_order_front();
1034 self.verify_invariants("enqueue");
1035
1036 let peer_len_post_insert = self.queues.get(sender).map(VecDeque::len).unwrap_or(0);
1037 let over_global = self.total > global_limit || self.total_bytes > global_bytes_limit;
1038 let over_peer = peer_len_post_insert > per_peer_limit
1039 || self.peer_bytes_for(sender) > per_peer_bytes_limit;
1040 if over_global || over_peer {
1041 let _ = self.remove_entry_by_sequence(sender, sequence);
1042 let limit = if over_global {
1043 QueueLimit::Global
1044 } else {
1045 QueueLimit::PerPeer
1046 };
1047 self.record_eviction_failure(
1048 limit,
1049 sender,
1050 &incoming_message_id,
1051 "post-insert hard-bound check failed; rolled back enqueue",
1052 );
1053 self.record_drop(
1054 DropReason::OverflowDropNewest,
1055 Some(limit),
1056 sender,
1057 &incoming_message_id,
1058 overflow_policy,
1059 );
1060 return Admission::Refused;
1061 }
1062
1063 Admission::Admitted
1064 }
1065
1066 pub(crate) fn drain_for_peer(
1069 &mut self,
1070 config: &PendingQueueConfig,
1071 sender: &str,
1072 ) -> Vec<PendingDecryptMessage> {
1073 let now = Instant::now();
1074 let _ = self.prune_expired_for_peer(config, sender, now);
1075
1076 let messages = match self.queues.remove(sender) {
1077 Some(msgs) => msgs,
1078 None => return Vec::new(),
1079 };
1080
1081 if messages.is_empty() {
1082 return Vec::new();
1083 }
1084
1085 let drained: Vec<PendingDecryptMessage> = messages.into_iter().collect();
1086 for entry in &drained {
1087 self.live_sequences.remove(&entry.sequence);
1088 self.total = self.total.saturating_sub(1);
1089 self.total_bytes = self
1090 .total_bytes
1091 .saturating_sub(Self::message_bytes(&entry.message));
1092 }
1093 self.peer_bytes.remove(sender);
1094 self.peer_overflow_hits.remove(sender);
1098 self.update_peer_gauge(sender);
1099 self.update_current_gauge();
1100 self.cleanup_global_order_front();
1101 self.verify_invariants("drain_for_peer");
1102
1103 drained
1104 }
1105}