1use std::collections::BTreeMap;
2use std::collections::HashMap;
3use std::collections::HashSet;
4use std::collections::VecDeque;
5use std::sync::atomic::AtomicUsize;
6use std::sync::atomic::Ordering;
7use std::sync::Arc;
8
9use bytes::Bytes;
10use uuid::Uuid;
11
12use super::Chunk;
13use super::ReassemblyLimits;
14use crate::consts::MAX_TTL_MS;
15use crate::consts::TS_OFFSET_TOLERANCE_MS;
16use crate::fair_admission::try_reserve_atomic;
17use crate::utils::get_epoch_ms;
18
19pub(super) struct Pending {
21 total: usize,
23 pub(super) slots: BTreeMap<usize, Bytes>,
26 pub(super) data_bytes: usize,
28 ts_ms: u128,
30 ttl_ms: u64,
31 failure_charged: bool,
33 local_capacity_rejected: bool,
35 peer_attributable: bool,
37}
38
39impl Pending {
40 fn new(total: usize, ts_ms: u128, ttl_ms: u64, peer_attributable: bool) -> Self {
41 Self {
42 total,
43 slots: BTreeMap::new(),
44 data_bytes: 0,
45 ts_ms,
46 ttl_ms,
47 failure_charged: false,
48 local_capacity_rejected: false,
49 peer_attributable,
50 }
51 }
52
53 fn is_complete(&self) -> bool {
56 self.slots.len() == self.total
57 }
58
59 pub(super) fn cost(&self, slot_overhead: usize) -> usize {
63 self.slots
64 .len()
65 .saturating_mul(slot_overhead)
66 .saturating_add(self.data_bytes)
67 }
68
69 fn assemble(self) -> Bytes {
70 self.slots.into_values().flatten().collect()
71 }
72}
73
74#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
78struct LogicalTransmission {
79 id: Uuid,
80 ts_ms: u128,
81 ttl_ms: u64,
82}
83
84impl LogicalTransmission {
85 const fn new(id: Uuid, ts_ms: u128, ttl_ms: u64) -> Self {
86 Self { id, ts_ms, ttl_ms }
87 }
88}
89
90pub struct MessageReassembler {
107 pub(super) pending: HashMap<Uuid, Pending>,
108 pub(super) buffered_cost: usize,
110 completed: VecDeque<(Uuid, u128)>,
115 pub(super) completed_ids: HashSet<Uuid>,
116 failed: VecDeque<(LogicalTransmission, u128)>,
122 failed_ids: HashSet<LogicalTransmission>,
123 failure_tracking_saturated_until: u128,
127 capacity_rejected: VecDeque<(Uuid, u128)>,
131 capacity_rejected_ids: HashSet<Uuid>,
132 capacity_tracking_saturated_until: u128,
136 limits: ReassemblyLimits,
138 budget: Arc<ReassemblyBudget>,
139}
140
141pub(crate) struct ReassemblyBudget {
143 pub(super) buffered_cost: AtomicUsize,
144 limit: usize,
145}
146
147impl ReassemblyBudget {
148 pub(crate) fn new(limits: ReassemblyLimits) -> Self {
149 Self {
150 buffered_cost: AtomicUsize::new(0),
151 limit: limits.normalized().max_total_buffered_cost,
152 }
153 }
154
155 fn try_reserve(&self, cost: usize) -> bool {
156 try_reserve_atomic(&self.buffered_cost, cost, self.limit)
157 }
158
159 fn release(&self, cost: usize) {
160 if self
161 .buffered_cost
162 .fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| {
163 current.checked_sub(cost)
164 })
165 .is_err()
166 {
167 tracing::error!(cost, "reassembly budget release exceeded retained cost");
168 }
169 }
170
171 #[cfg(all(test, feature = "dummy", not(target_family = "wasm")))]
172 pub(crate) fn buffered_cost_for_test(&self) -> usize {
173 self.buffered_cost.load(Ordering::Acquire)
174 }
175}
176
177pub(crate) struct RetainedReassembly {
179 bytes: Bytes,
180 budget: Arc<ReassemblyBudget>,
181 cost: usize,
182}
183
184pub(crate) enum ReassemblyOutcome {
186 Incomplete,
188 Complete(RetainedReassembly),
190 Rejected(ReassemblyRejection),
192}
193
194#[derive(Clone, Copy, Debug, Eq, PartialEq)]
196pub(crate) enum ReassemblyRejection {
197 Invalid,
199 Capacity,
201 Replay,
203}
204
205impl RetainedReassembly {
206 fn into_bytes(mut self) -> Bytes {
207 self.budget.release(self.cost);
208 self.cost = 0;
209 std::mem::take(&mut self.bytes)
210 }
211}
212
213impl AsRef<[u8]> for RetainedReassembly {
214 fn as_ref(&self) -> &[u8] {
215 &self.bytes
216 }
217}
218
219impl Drop for RetainedReassembly {
220 fn drop(&mut self) {
221 self.budget.release(self.cost);
222 }
223}
224
225impl Default for MessageReassembler {
226 fn default() -> Self {
227 Self::with_limits(ReassemblyLimits::production())
228 }
229}
230
231impl MessageReassembler {
232 pub fn new() -> Self {
234 Self::default()
235 }
236
237 pub fn with_limits(limits: ReassemblyLimits) -> Self {
240 let budget = Arc::new(ReassemblyBudget::new(limits));
241 Self::with_limits_and_budget(limits, budget)
242 }
243
244 pub(crate) fn with_limits_and_budget(
246 limits: ReassemblyLimits,
247 budget: Arc<ReassemblyBudget>,
248 ) -> Self {
249 Self {
250 pending: HashMap::new(),
251 buffered_cost: 0,
252 completed: VecDeque::new(),
253 completed_ids: HashSet::new(),
254 failed: VecDeque::new(),
255 failed_ids: HashSet::new(),
256 failure_tracking_saturated_until: 0,
257 capacity_rejected: VecDeque::new(),
258 capacity_rejected_ids: HashSet::new(),
259 capacity_tracking_saturated_until: 0,
260 limits: limits.normalized(),
262 budget,
263 }
264 }
265
266 fn mark_completed(&mut self, id: Uuid, expiry: u128) {
271 if self.completed_ids.insert(id) {
272 self.completed.push_back((id, expiry));
273 }
274 while self.completed.len() > self.limits.max_completed_ids {
275 if let Some((old, _)) = self.completed.pop_front() {
276 self.completed_ids.remove(&old);
277 }
278 }
279 }
280
281 pub fn pending_count(&self) -> usize {
283 self.pending.len()
284 }
285
286 pub fn remove_expired(&mut self) {
290 let _ = self.remove_expired_at(get_epoch_ms());
291 }
292
293 pub(crate) fn has_pending(&self) -> bool {
295 !self.pending.is_empty()
296 }
297
298 pub(crate) fn prepare_for_close(&mut self) -> bool {
305 let buffered_cost = &mut self.buffered_cost;
306 let budget = &self.budget;
307 let slot_overhead = self.limits.slot_overhead;
308 self.pending.retain(|_, pending| {
309 let retained = pending.peer_attributable
310 && !(pending.failure_charged || pending.local_capacity_rejected);
311 if !retained {
312 let cost = pending.cost(slot_overhead);
313 *buffered_cost = buffered_cost.saturating_sub(cost);
314 budget.release(cost);
315 }
316 retained
317 });
318 self.clear_terminal_history();
319 !self.pending.is_empty()
320 }
321
322 pub(crate) fn discard_after_close_timer_failure(&mut self) {
324 for pending in self.pending.values() {
325 self.budget.release(pending.cost(self.limits.slot_overhead));
326 }
327 self.pending.clear();
328 self.buffered_cost = 0;
329 self.clear_terminal_history();
330 }
331
332 fn clear_terminal_history(&mut self) {
333 self.completed.clear();
334 self.completed_ids.clear();
335 self.failed.clear();
336 self.failed_ids.clear();
337 self.failure_tracking_saturated_until = 0;
338 self.capacity_rejected.clear();
339 self.capacity_rejected_ids.clear();
340 self.capacity_tracking_saturated_until = 0;
341 }
342
343 pub(crate) fn remove_expired_at(&mut self, now: u128) -> usize {
346 self.evict_expired_terminal_history(now);
347 let mut expired_count = 0_usize;
348 let mut expired_transmissions = Vec::new();
349 let buffered_cost = &mut self.buffered_cost;
350 let budget = &self.budget;
351 let slot_overhead = self.limits.slot_overhead;
352 self.pending.retain(|id, p| {
353 let alive = p.ts_ms.saturating_add(p.ttl_ms as u128) > now;
354 if !alive {
355 let cost = p.cost(slot_overhead);
356 *buffered_cost = buffered_cost.saturating_sub(cost);
357 budget.release(cost);
358 expired_transmissions.push(LogicalTransmission::new(*id, p.ts_ms, p.ttl_ms));
359 if !(p.failure_charged || p.local_capacity_rejected) && p.peer_attributable {
360 expired_count = expired_count.saturating_add(1);
361 }
362 }
363 alive
364 });
365 for transmission in expired_transmissions {
372 if !self.mark_failed_transmission(transmission, now) {
373 self.extend_failure_tracking_saturation(now);
376 }
377 }
378 expired_count
379 }
380
381 fn evict_expired_terminal_history(&mut self, now: u128) {
382 let completed_ids = &mut self.completed_ids;
386 self.completed.retain(|&(id, expiry)| {
387 let alive = expiry > now;
388 if !alive {
389 completed_ids.remove(&id);
390 }
391 alive
392 });
393 let failed_ids = &mut self.failed_ids;
394 self.failed.retain(|&(transmission, expiry)| {
395 let alive = expiry > now;
396 if !alive {
397 failed_ids.remove(&transmission);
398 }
399 alive
400 });
401 let capacity_rejected_ids = &mut self.capacity_rejected_ids;
402 self.capacity_rejected.retain(|&(id, expiry)| {
403 let alive = expiry > now;
404 if !alive {
405 capacity_rejected_ids.remove(&id);
406 }
407 alive
408 });
409 }
410
411 pub fn remove(&mut self, id: Uuid) {
413 if let Some(p) = self.pending.remove(&id) {
414 let cost = p.cost(self.limits.slot_overhead);
415 self.buffered_cost = self.buffered_cost.saturating_sub(cost);
416 self.budget.release(cost);
417 }
418 }
419
420 pub fn handle(&mut self, chunk: Chunk) -> Option<Bytes> {
428 self.handle_at(chunk, get_epoch_ms())
429 }
430
431 #[cfg(test)]
433 pub(crate) fn handle_retained(&mut self, chunk: Chunk) -> Option<RetainedReassembly> {
434 match self.handle_retained_outcome(chunk) {
435 ReassemblyOutcome::Complete(bytes) => Some(bytes),
436 ReassemblyOutcome::Incomplete | ReassemblyOutcome::Rejected(_) => None,
437 }
438 }
439
440 #[cfg(test)]
442 pub(crate) fn handle_retained_outcome(&mut self, chunk: Chunk) -> ReassemblyOutcome {
443 self.handle_retained_at(chunk, get_epoch_ms()).0
444 }
445
446 #[cfg(test)]
447 pub(crate) fn handle_retained_outcome_at(
448 &mut self,
449 chunk: Chunk,
450 now: u128,
451 ) -> ReassemblyOutcome {
452 self.handle_retained_at(chunk, now).0
453 }
454
455 pub(crate) fn handle_retained_outcome_with_expiry(
458 &mut self,
459 chunk: Chunk,
460 peer_attributable: bool,
461 ) -> (ReassemblyOutcome, usize) {
462 self.handle_retained_at_with_attribution(chunk, get_epoch_ms(), peer_attributable)
463 }
464
465 pub(super) fn handle_at(&mut self, chunk: Chunk, now: u128) -> Option<Bytes> {
468 match self.handle_retained_at(chunk, now).0 {
469 ReassemblyOutcome::Complete(bytes) => Some(bytes.into_bytes()),
470 ReassemblyOutcome::Incomplete | ReassemblyOutcome::Rejected(_) => None,
471 }
472 }
473
474 fn handle_retained_at(&mut self, chunk: Chunk, now: u128) -> (ReassemblyOutcome, usize) {
475 self.handle_retained_at_with_attribution(chunk, now, true)
476 }
477
478 fn handle_retained_at_with_attribution(
479 &mut self,
480 chunk: Chunk,
481 now: u128,
482 peer_attributable: bool,
483 ) -> (ReassemblyOutcome, usize) {
484 let expired = self.remove_expired_at(now);
488 let outcome = match self.classify(&chunk, now) {
489 Ok(cost) => self.admit(chunk, cost, peer_attributable),
490 Err(reason) => {
491 tracing::debug!(?reason, id = ?chunk.meta.id, "reassembler dropped chunk");
492 let rejection = match reason.rejection() {
493 ReassemblyRejection::Invalid => {
494 if self.mark_logical_failure(&chunk, now) {
495 ReassemblyRejection::Invalid
496 } else {
497 ReassemblyRejection::Replay
498 }
499 }
500 ReassemblyRejection::Capacity => {
501 self.mark_pending_capacity_rejection(&chunk);
502 ReassemblyRejection::Capacity
503 }
504 ReassemblyRejection::Replay => ReassemblyRejection::Replay,
505 };
506 ReassemblyOutcome::Rejected(rejection)
507 }
508 };
509 (outcome, expired)
510 }
511
512 fn classify(&self, chunk: &Chunk, now: u128) -> std::result::Result<usize, Rejected> {
520 let meta = &chunk.meta;
521 let transmission = LogicalTransmission::new(meta.id, meta.ts_ms, meta.ttl_ms);
522 if self
523 .pending
524 .get(&meta.id)
525 .is_some_and(|pending| pending.failure_charged)
526 || self.failed_ids.contains(&transmission)
527 {
528 return Err(Rejected::AlreadyFailed);
529 }
530 if self.capacity_rejected_ids.contains(&meta.id) {
531 return Err(Rejected::CapacityRejectedId);
532 }
533 if meta.ttl_ms > MAX_TTL_MS {
534 return Err(Rejected::TtlTooLarge);
535 }
536 if meta.ts_ms.saturating_sub(TS_OFFSET_TOLERANCE_MS) > now {
539 return Err(Rejected::FutureTimestamp);
540 }
541 if meta.ts_ms.saturating_add(meta.ttl_ms as u128) <= now {
543 return Err(Rejected::Expired);
544 }
545
546 let [position, total] = chunk.chunk;
547 if total == 0 || position >= total {
549 return Err(Rejected::Malformed);
550 }
551 if total > self.limits.max_chunks_per_message {
553 return Err(Rejected::TooManyChunks);
554 }
555 if chunk.data.len() > self.limits.max_chunk_data_len {
557 return Err(Rejected::ChunkTooLarge);
558 }
559 if self.completed_ids.contains(&meta.id) {
561 return Err(Rejected::AlreadyCompleted);
562 }
563
564 let buffered_for_id = match self.pending.get(&meta.id) {
568 None if self.capacity_tracking_saturated_until > now => {
569 return Err(Rejected::CapacityTrackingFull);
570 }
571 None if self.pending.len() >= self.limits.max_pending_messages => {
573 return Err(Rejected::PendingFull);
574 }
575 None => 0,
576 Some(p) => {
577 if p.total != total {
579 return Err(Rejected::TotalMismatch);
580 }
581 if p.ts_ms != meta.ts_ms || p.ttl_ms != meta.ttl_ms {
584 return Err(Rejected::MetadataMismatch);
585 }
586 if let Some(existing) = p.slots.get(&position) {
588 return if existing == &chunk.data {
589 Err(Rejected::DuplicatePosition)
590 } else {
591 Err(Rejected::ConflictingPosition)
592 };
593 }
594 p.data_bytes
595 }
596 };
597 if buffered_for_id.saturating_add(chunk.data.len()) > self.limits.max_message_bytes {
599 return Err(Rejected::PerMessageBytes);
600 }
601
602 let cost = chunk.data.len().saturating_add(self.limits.slot_overhead);
605 if self.buffered_cost.saturating_add(cost) > self.limits.max_peer_buffered_cost() {
606 return Err(Rejected::PeerBudget);
607 }
608 Ok(cost)
609 }
610
611 fn mark_logical_failure(&mut self, chunk: &Chunk, now: u128) -> bool {
612 let meta = &chunk.meta;
613 if let Some(pending) = self.pending.get_mut(&meta.id) {
614 if pending.failure_charged {
615 return false;
616 }
617 pending.failure_charged = true;
618 return true;
619 }
620
621 self.mark_failed_transmission(
622 LogicalTransmission::new(meta.id, meta.ts_ms, meta.ttl_ms),
623 now,
624 )
625 }
626
627 fn mark_failed_transmission(&mut self, transmission: LogicalTransmission, now: u128) -> bool {
628 if self.failed_ids.contains(&transmission) || self.failure_tracking_saturated_until > now {
629 return false;
630 }
631 if self.failed_ids.len() >= self.limits.max_completed_ids {
632 self.extend_failure_tracking_saturation(now);
633 return false;
634 }
635 let expiry = now.saturating_add(MAX_TTL_MS as u128);
638 self.failed_ids.insert(transmission);
639 self.failed.push_back((transmission, expiry));
640 true
641 }
642
643 fn extend_failure_tracking_saturation(&mut self, now: u128) {
644 self.failure_tracking_saturated_until = self
645 .failure_tracking_saturated_until
646 .max(now.saturating_add(MAX_TTL_MS as u128));
647 }
648
649 fn mark_pending_capacity_rejection(&mut self, chunk: &Chunk) {
650 let meta = &chunk.meta;
651 if let Some(pending) = self.pending.get_mut(&meta.id) {
652 if pending.ts_ms == meta.ts_ms && pending.ttl_ms == meta.ttl_ms {
653 pending.local_capacity_rejected = true;
654 return;
655 }
656 }
657 self.mark_capacity_rejected_id(meta.id, meta.ts_ms, meta.ttl_ms);
658 }
659
660 fn mark_capacity_rejected_id(&mut self, id: Uuid, ts_ms: u128, ttl_ms: u64) {
661 let expiry = ts_ms.saturating_add(ttl_ms.min(MAX_TTL_MS) as u128);
662 if self.capacity_rejected_ids.contains(&id) {
663 return;
664 }
665 if self.capacity_rejected_ids.len() >= self.limits.max_completed_ids {
666 self.capacity_tracking_saturated_until =
667 self.capacity_tracking_saturated_until.max(expiry);
668 return;
669 }
670 self.capacity_rejected_ids.insert(id);
671 self.capacity_rejected.push_back((id, expiry));
672 }
673
674 fn admit(&mut self, chunk: Chunk, cost: usize, peer_attributable: bool) -> ReassemblyOutcome {
680 if !self.budget.try_reserve(cost) {
681 self.mark_pending_capacity_rejection(&chunk);
682 tracing::debug!(
683 reason = ?Rejected::GlobalBudget,
684 id = ?chunk.meta.id,
685 "reassembler dropped chunk"
686 );
687 return ReassemblyOutcome::Rejected(ReassemblyRejection::Capacity);
688 }
689 let id = chunk.meta.id;
690 let [position, total] = chunk.chunk;
691 let mut pending = self.pending.remove(&id).unwrap_or_else(|| {
692 Pending::new(
693 total,
694 chunk.meta.ts_ms,
695 chunk.meta.ttl_ms,
696 peer_attributable,
697 )
698 });
699 pending.peer_attributable &= peer_attributable;
700 pending.data_bytes = pending.data_bytes.saturating_add(chunk.data.len());
701 pending.slots.insert(position, chunk.data);
702 self.buffered_cost = self.buffered_cost.saturating_add(cost);
703
704 if !pending.is_complete() {
705 self.pending.insert(id, pending);
706 #[cfg(all(test, feature = "dummy", not(target_family = "wasm")))]
707 crate::simulation::observe_reassembly_capacity(
708 self.budget.buffered_cost.load(Ordering::Acquire),
709 self.budget.limit,
710 self.buffered_cost,
711 self.limits.max_peer_buffered_cost(),
712 self.pending.len(),
713 self.limits.max_pending_messages,
714 );
715 return ReassemblyOutcome::Incomplete;
716 }
717 let output_cost = pending.data_bytes;
718 if !self.budget.try_reserve(output_cost) {
719 self.mark_capacity_rejected_id(id, pending.ts_ms, pending.ttl_ms);
720 let dropped_cost = pending.cost(self.limits.slot_overhead);
721 self.buffered_cost = self.buffered_cost.saturating_sub(dropped_cost);
722 self.budget.release(dropped_cost);
723 tracing::debug!(
724 reason = ?Rejected::GlobalBudget,
725 ?id,
726 output_cost,
727 "reassembler dropped completed message before output allocation"
728 );
729 return ReassemblyOutcome::Rejected(ReassemblyRejection::Capacity);
730 }
731 #[cfg(all(test, feature = "dummy", not(target_family = "wasm")))]
732 crate::simulation::observe_reassembly_capacity(
733 self.budget.buffered_cost.load(Ordering::Acquire),
734 self.budget.limit,
735 self.buffered_cost,
736 self.limits.max_peer_buffered_cost(),
737 self.pending.len().saturating_add(1),
738 self.limits.max_pending_messages,
739 );
740 let done = pending;
741 let done_cost = done.cost(self.limits.slot_overhead);
742 let expiry = done.ts_ms.saturating_add(done.ttl_ms as u128);
743 self.buffered_cost = self.buffered_cost.saturating_sub(done_cost);
744 let bytes = done.assemble();
745 self.budget.release(done_cost);
746 self.mark_completed(id, expiry);
748 ReassemblyOutcome::Complete(RetainedReassembly {
749 bytes,
750 budget: self.budget.clone(),
751 cost: output_cost,
752 })
753 }
754}
755
756impl Drop for MessageReassembler {
757 fn drop(&mut self) {
758 self.budget.release(self.buffered_cost);
759 }
760}
761
762#[derive(Debug, Clone, Copy, PartialEq, Eq)]
765enum Rejected {
766 TtlTooLarge,
768 FutureTimestamp,
770 Expired,
772 Malformed,
774 TooManyChunks,
776 ChunkTooLarge,
778 AlreadyCompleted,
780 AlreadyFailed,
782 CapacityRejectedId,
784 CapacityTrackingFull,
786 PendingFull,
788 TotalMismatch,
790 MetadataMismatch,
792 DuplicatePosition,
794 ConflictingPosition,
796 PerMessageBytes,
798 PeerBudget,
800 GlobalBudget,
802}
803
804impl Rejected {
805 const fn rejection(self) -> ReassemblyRejection {
806 match self {
807 Self::AlreadyCompleted
808 | Self::AlreadyFailed
809 | Self::DuplicatePosition => ReassemblyRejection::Replay,
810 Self::CapacityRejectedId
811 | Self::CapacityTrackingFull
812 | Self::PendingFull
813 | Self::PeerBudget
814 | Self::GlobalBudget => ReassemblyRejection::Capacity,
815 Self::TtlTooLarge
816 | Self::FutureTimestamp
817 | Self::Expired
820 | Self::Malformed
821 | Self::TooManyChunks
822 | Self::ChunkTooLarge
823 | Self::TotalMismatch
824 | Self::MetadataMismatch
825 | Self::ConflictingPosition
826 | Self::PerMessageBytes => ReassemblyRejection::Invalid,
827 }
828 }
829}