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