Skip to main content

sedsnet/
relay.rs

1#[cfg(all(test, feature = "std", feature = "discovery"))]
2#[path = "tests/shared_chunk_budget_relay.rs"]
3mod shared_chunk_budget_tests;
4// Apply the same cache contract independently to router and relay state.
5#[cfg(all(test, feature = "discovery"))]
6#[path = "tests/relay_ack_recovery.rs"]
7mod ack_recovery_tests;
8#[cfg(all(test, feature = "std"))]
9#[path = "tests/relay_control_priority.rs"]
10mod control_priority_tests;
11#[cfg(test)]
12#[allow(clippy::duplicate_mod)]
13#[path = "tests/side_template_index.rs"]
14mod side_template_index_tests;
15use crate::side_transport::{
16    SIDE_TRANSPORT_FLAG_PACKET_NONCE, SIDE_TRANSPORT_KIND_CHUNK, SIDE_TRANSPORT_KIND_COMPACT,
17    SIDE_TRANSPORT_KIND_COMPACT_DELTA, SIDE_TRANSPORT_KIND_COMPACT_SAME_TIMESTAMP,
18    SIDE_TRANSPORT_KIND_FULL, SideChunkAssembly, SideCompactTimestampMode, SideHeaderTemplate,
19    SideTransportFrames, extract_side_header_template, extract_side_header_template_view,
20    parse_side_transport_wrapper, read_uleb128_local, reconstruct_side_compact_frame,
21    wrap_side_transport_frame, write_uleb128_local,
22};
23#[cfg(all(test, feature = "discovery"))]
24#[path = "tests/relay_restart_transport.rs"]
25mod restart_transport_tests;
26
27#[cfg(all(test, feature = "discovery"))]
28mod compact_summary_tests {
29    use super::*;
30
31    #[test]
32    fn compact_loss_and_reordering_preserve_packet_identity() {
33        crate::tests::ensure_common_test_schema();
34        let tx = Relay::new(Box::new(|| 0));
35        let rx = Relay::new(Box::new(|| 0));
36        let opts = RelaySideOptions::default().with_small_packet_transport(0);
37        let a = tx.add_side_packed_with_options("wire", |_| Ok(()), opts.clone());
38        let b = rx.add_side_packed_with_options("wire", |_| Ok(()), opts.clone());
39        let mut encoded = Vec::new();
40        for timestamp in [10000, 10010, 10020, 10030] {
41            let pkt = Packet::new(
42                crate::DataType::named("GPS_DATA"),
43                &[crate::DataEndpoint::named("RADIO")],
44                "VB",
45                timestamp,
46                Arc::<[u8]>::from([0u8; 12]),
47            )
48            .unwrap();
49            let raw = wire_format::pack_packet(&pkt);
50            let frames = tx
51                .encode_side_transport_frames(
52                    a,
53                    opts.clone(),
54                    crate::shared_bytes::convert(raw.clone()),
55                )
56                .unwrap();
57            assert_eq!(frames.len(), 1);
58            encoded.push((raw, frames.into_iter().next().unwrap()));
59        }
60        // Establish template, lose frame 1, reorder and duplicate later frames.
61        for index in [0, 3, 2, 3] {
62            let (raw, frame) = &encoded[index];
63            let received = rx.decode_side_transport_frame(b, frame).unwrap().unwrap();
64            assert_eq!(&received, raw);
65        }
66    }
67
68    #[test]
69    fn relay_preserves_compact_routes_without_inventing_endpoint_ownership() {
70        crate::tests::ensure_common_test_schema();
71        let endpoint = crate::DataEndpoint::named("RADIO");
72        let relay = Relay::new(Box::new(|| 0));
73        let mut sender = DiscoverySenderState {
74            advertised_reachable: vec![endpoint],
75            advertised_reachable_timesync_sources: vec!["clock-behind-bridge".into()],
76            topology_boards: Arc::new(vec![TopologyBoardNode {
77                sender_id: "bridge".into(),
78                reachable_endpoints: vec![],
79                reachable_timesync_sources: vec![],
80                connections: vec![],
81            }]),
82            ..Default::default()
83        };
84        Relay::refresh_sender_topology_state(&mut sender);
85        let mut route = DiscoverySideState::default();
86        route.announcers.insert("bridge".into(), sender);
87        Relay::recompute_discovery_side_state(&mut route);
88        let mut state = relay.state.lock();
89        state.discovery_routes.insert(0, route);
90        assert!(
91            relay
92                .advertised_discovery_endpoints_for_link_locked(&state, 0, true, Some(1))
93                .contains(&endpoint)
94        );
95        assert!(
96            !relay
97                .advertised_discovery_endpoints_for_link_locked(&state, 0, true, Some(0))
98                .contains(&endpoint)
99        );
100        assert!(
101            !relay
102                .advertised_discovery_endpoints_for_link_locked(
103                    &state,
104                    DISCOVERY_ROUTE_TTL_MS + 1,
105                    true,
106                    Some(1)
107                )
108                .contains(&endpoint)
109        );
110        assert_eq!(
111            relay.advertised_discovery_timesync_sources_for_link_locked(&state, 0, Some(1)),
112            vec!["clock-behind-bridge".to_string()]
113        );
114        assert!(
115            relay
116                .advertised_discovery_timesync_sources_for_link_locked(&state, 0, Some(0))
117                .is_empty()
118        );
119        assert!(
120            relay
121                .advertised_discovery_timesync_sources_for_link_locked(
122                    &state,
123                    DISCOVERY_ROUTE_TTL_MS + 1,
124                    Some(1)
125                )
126                .is_empty()
127        );
128        let topology =
129            relay.advertised_discovery_topology_for_link_locked(&state, 0, true, Some(1));
130        assert!(
131            topology
132                .iter()
133                .find(|node| node.sender_id == "bridge")
134                .unwrap()
135                .reachable_endpoints
136                .is_empty()
137        );
138    }
139}
140
141use crate::config::{
142    RuntimeMemoryConfig, runtime_reliable_max_end_to_end_ack_cache,
143    runtime_reliable_max_end_to_end_pending, runtime_reliable_max_pending,
144    runtime_reliable_max_retries, runtime_reliable_max_return_routes,
145    runtime_reliable_retransmit_ms,
146};
147use crate::diagnostics::{
148    AdaptiveLinkStats, DiscoveryRuntimeStats, QueueRuntimeStats, ReliableRuntimeStats,
149    RouteModeStats, RouteOverrideStats, RoutePriorityStats, RouteWeightStats, RuntimeSideStats,
150    RuntimeStatsSnapshot, RuntimeTypeStats, TypedRouteOverrideStats,
151};
152#[cfg(feature = "discovery")]
153use crate::discovery::{
154    self, ClientStatsSnapshot, DISCOVERY_INCREMENTAL_RETRY_COUNT, DISCOVERY_ROUTE_TTL_MS,
155    DISCOVERY_SLOW_LINK_PING_INTERVAL_MS, DiscoveryCadenceState,
156    TIMESYNC_SLOW_LINK_MIN_INTERVAL_MS, TopologyAnnouncerRoute, TopologyBoardNode,
157    TopologySideRoute, TopologySnapshot,
158};
159#[cfg(feature = "discovery")]
160use crate::packet::sender_address_u32;
161use crate::packet::{Packet, hash_bytes_u64};
162use crate::queue::{BoundedDeque, ByteCost};
163use crate::wire_format;
164use crate::{is_reliable_type, message_meta, message_priority, reliable_mode};
165use crate::{
166    router::{Clock, CompactTimestampOmissionPolicy, SideTransportProfile},
167    {
168        RouteSelectionMode, TelemetryError, TelemetryResult,
169        lock::{ReentryGate, ReentryGuard, RouterMutex},
170    },
171};
172use alloc::borrow::ToOwned;
173use alloc::boxed::Box;
174use alloc::collections::{BTreeMap, BTreeSet, VecDeque};
175use alloc::string::{String, ToString};
176use alloc::{sync::Arc, vec, vec::Vec};
177use core::mem::size_of;
178
179const MAX_PRIORITY_BURST: u8 = 8;
180
181/// Logical side index (CAN, UART, RADIO, etc.)
182pub type RelaySideId = usize;
183const CONTROL_SLOW_LINK_CAPACITY_BPS: u64 = 512;
184pub const IPV4_LIKE_COMPACT_HEADER_TARGET_BYTES: usize =
185    crate::router::IPV4_LIKE_COMPACT_HEADER_TARGET_BYTES;
186pub const IPV6_LIKE_COMPACT_HEADER_TARGET_BYTES: usize =
187    crate::router::IPV6_LIKE_COMPACT_HEADER_TARGET_BYTES;
188pub const DEFAULT_SIDE_TRANSPORT_TEMPLATE_LIMIT: usize =
189    crate::router::DEFAULT_SIDE_TRANSPORT_TEMPLATE_LIMIT;
190/// Packet Handler function type
191type PacketHandlerFn = dyn Fn(&Packet) -> TelemetryResult<()> + Send + Sync + 'static;
192
193/// Packed Handler function type
194type PackedHandlerFn = dyn Fn(&[u8]) -> TelemetryResult<()> + Send + Sync + 'static;
195
196/// TX handler for a relay side: either packed or packet-based.
197#[derive(Clone)]
198pub enum RelayTxHandlerFn {
199    Packed(Arc<PackedHandlerFn>),
200    Packet(Arc<PacketHandlerFn>),
201}
202
203#[derive(Clone, Debug)]
204pub struct RelaySideOptions {
205    /// Enables the relay's per-link reliable transport layer on this side.
206    ///
207    /// When `true` and the side uses a packed TX handler, reliable schema traffic on this hop
208    /// gains relay-managed sequence numbers, ACKs, packet requests, and retransmits.
209    /// Packet-output sides still receive decoded packets rather than packed reliable framing.
210    pub reliable_enabled: bool,
211    /// Marks the side as eligible for link-local-only endpoints and discovery routes.
212    pub link_local_enabled: bool,
213    /// Allows packets received from this side to enter relay processing.
214    pub ingress_enabled: bool,
215    /// Allows the relay to transmit packets toward this side.
216    pub egress_enabled: bool,
217    /// Enables side-local header-template reuse for packed transport.
218    pub header_template_enabled: bool,
219    /// Maximum number of bytes to emit per packed TX callback.
220    ///
221    /// When non-zero and a packed frame would exceed this size, the relay
222    /// splits it into ordered side-transport chunks and reassembles them on RX
223    /// before normal relay processing. This is intended for fixed-size links
224    /// such as CAN or I2C while keeping the user API packet-oriented.
225    pub max_frame_bytes: usize,
226    /// Target total side-transport overhead for compact follow-up frames.
227    pub compact_header_target_bytes: usize,
228    /// Maximum side-local header templates retained for TX and RX dictionaries.
229    pub max_side_transport_templates: usize,
230    /// Omits the timestamp field from compact follow-up frames when it is unchanged.
231    pub omit_unchanged_compact_timestamps: bool,
232    /// Optional per-data-type timestamp omission policy for compact follow-up frames.
233    pub compact_timestamp_omission_types: CompactTimestampOmissionPolicy,
234    /// Declared compact-link profile for stats and future negotiation.
235    pub side_transport_profile: SideTransportProfile,
236}
237
238impl Default for RelaySideOptions {
239    fn default() -> Self {
240        Self {
241            reliable_enabled: false,
242            link_local_enabled: false,
243            ingress_enabled: true,
244            egress_enabled: true,
245            header_template_enabled: false,
246            max_frame_bytes: 0,
247            compact_header_target_bytes: 0,
248            max_side_transport_templates: DEFAULT_SIDE_TRANSPORT_TEMPLATE_LIMIT,
249            omit_unchanged_compact_timestamps: false,
250            compact_timestamp_omission_types: CompactTimestampOmissionPolicy::none(),
251            side_transport_profile: SideTransportProfile::Canonical,
252        }
253    }
254}
255
256impl RelaySideOptions {
257    /// Convenience preset for bounded packed-side transport.
258    ///
259    /// `max_frame_bytes == 0` leaves packed frames unbounded. Values greater
260    /// than zero enable relay-managed chunking/reassembly on this side.
261    #[inline]
262    pub fn with_small_packet_transport(mut self, max_frame_bytes: usize) -> Self {
263        self.header_template_enabled = true;
264        self.max_frame_bytes = max_frame_bytes;
265        self.compact_header_target_bytes = IPV6_LIKE_COMPACT_HEADER_TARGET_BYTES;
266        self.side_transport_profile = SideTransportProfile::Ipv6Like;
267        self
268    }
269
270    #[inline]
271    pub fn with_ipv4_like_compact_header_target(mut self) -> Self {
272        self.header_template_enabled = true;
273        self.compact_header_target_bytes = IPV4_LIKE_COMPACT_HEADER_TARGET_BYTES;
274        self.omit_unchanged_compact_timestamps = true;
275        self.side_transport_profile = SideTransportProfile::Ipv4Like;
276        self
277    }
278
279    #[inline]
280    pub fn with_ipv6_like_compact_header_target(mut self) -> Self {
281        self.header_template_enabled = true;
282        self.compact_header_target_bytes = IPV6_LIKE_COMPACT_HEADER_TARGET_BYTES;
283        self.side_transport_profile = SideTransportProfile::Ipv6Like;
284        self
285    }
286
287    #[inline]
288    pub fn with_template_transport(mut self) -> Self {
289        self.header_template_enabled = true;
290        self.side_transport_profile = SideTransportProfile::Template;
291        self
292    }
293
294    #[inline]
295    pub fn with_omitted_unchanged_compact_timestamps(mut self) -> Self {
296        self.header_template_enabled = true;
297        self.omit_unchanged_compact_timestamps = true;
298        self
299    }
300
301    #[inline]
302    pub fn with_omitted_unchanged_compact_timestamps_for_type(
303        mut self,
304        ty: crate::DataType,
305    ) -> Self {
306        self.header_template_enabled = true;
307        self.compact_timestamp_omission_types.insert(ty);
308        self
309    }
310
311    #[inline]
312    pub fn effective_transport_profile(&self) -> SideTransportProfile {
313        if !self.header_template_enabled && self.max_frame_bytes == 0 {
314            SideTransportProfile::Canonical
315        } else if self.side_transport_profile == SideTransportProfile::Canonical {
316            SideTransportProfile::Template
317        } else {
318            self.side_transport_profile
319        }
320    }
321
322    #[cfg(feature = "discovery")]
323    #[inline]
324    pub fn link_capabilities(&self) -> discovery::LinkCapabilities {
325        let mut flags = discovery::LINK_CAPABILITY_END_TO_END_RELIABILITY;
326        if self.header_template_enabled {
327            flags |= discovery::LINK_CAPABILITY_HEADER_TEMPLATES;
328        }
329        if self.max_frame_bytes != 0 {
330            flags |= discovery::LINK_CAPABILITY_CHUNKING;
331        }
332        if self.reliable_enabled {
333            flags |= discovery::LINK_CAPABILITY_RELIABILITY;
334        }
335        if self.omit_unchanged_compact_timestamps
336            || !self.compact_timestamp_omission_types.is_empty()
337        {
338            flags |= discovery::LINK_CAPABILITY_OMIT_UNCHANGED_TIMESTAMPS;
339        }
340        #[cfg(feature = "cryptography")]
341        {
342            flags |= discovery::LINK_CAPABILITY_CRYPTO;
343        }
344        discovery::LinkCapabilities {
345            version: 1,
346            flags,
347            profile: self.effective_transport_profile().discovery_code(),
348            max_frame_bytes: self.max_frame_bytes.min(u32::MAX as usize) as u32,
349            compact_header_target_bytes: self.compact_header_target_bytes.min(u32::MAX as usize)
350                as u32,
351            max_side_transport_templates: self.max_side_transport_templates.min(u32::MAX as usize)
352                as u32,
353        }
354    }
355}
356
357/// One side of the relay – a name + TX handler.
358#[derive(Clone)]
359pub struct RelaySide {
360    pub name: Arc<str>,
361    pub tx_handler: RelayTxHandlerFn,
362    pub opts: RelaySideOptions,
363}
364
365#[derive(Clone, Debug, PartialEq, Eq)]
366pub enum RelayItem {
367    Packed(crate::SharedBytes),
368    Packet(Arc<Packet>),
369}
370
371/// Item that was received by the relay from some side.
372#[derive(Clone, Debug, PartialEq, Eq)]
373struct RelayRxItem {
374    src: RelaySideId,
375    data: RelayItem,
376    priority: u8,
377}
378
379impl ByteCost for RelayRxItem {
380    #[cfg(feature = "compact-packet-store")]
381    fn park_for_queue(&mut self) -> TelemetryResult<()> {
382        match &mut self.data {
383            RelayItem::Packet(packet) => Arc::make_mut(packet).park_for_queue()?,
384            RelayItem::Packed(bytes) => bytes.park_for_queue()?,
385        }
386        Ok(())
387    }
388    fn byte_cost(&self) -> usize {
389        match &self.data {
390            RelayItem::Packed(bytes) => bytes.len(),
391            RelayItem::Packet(pkt) => pkt.byte_cost(),
392        }
393    }
394}
395
396/// Item that is ready to be transmitted out a destination side.
397#[derive(Clone, Debug, PartialEq, Eq)]
398struct RelayTxItem {
399    src: Option<RelaySideId>,
400    dst: RelaySideId,
401    data: RelayItem,
402    priority: u8,
403    retry_after_ms: u64,
404}
405
406#[derive(Clone, Debug, PartialEq, Eq)]
407struct RelayReplayItem {
408    dst: RelaySideId,
409    bytes: crate::SharedBytes,
410    priority: u8,
411    retry_after_ms: u64,
412}
413
414impl ByteCost for RelayTxItem {
415    #[cfg(feature = "compact-packet-store")]
416    fn park_for_queue(&mut self) -> TelemetryResult<()> {
417        match &mut self.data {
418            RelayItem::Packet(packet) => Arc::make_mut(packet).park_for_queue()?,
419            RelayItem::Packed(bytes) => bytes.park_for_queue()?,
420        }
421        Ok(())
422    }
423    fn byte_cost(&self) -> usize {
424        (match &self.data {
425            RelayItem::Packed(bytes) => bytes.len(),
426            RelayItem::Packet(pkt) => pkt.byte_cost(),
427        })
428        .saturating_add(size_of::<u64>())
429    }
430}
431
432impl ByteCost for RelayReplayItem {
433    #[cfg(feature = "compact-packet-store")]
434    fn park_for_queue(&mut self) -> TelemetryResult<()> {
435        self.bytes.park_for_queue()
436    }
437    fn byte_cost(&self) -> usize {
438        (self.bytes.len()).saturating_add(size_of::<u64>())
439    }
440}
441
442// -------------------- Reliable delivery state (relay) --------------------
443
444#[derive(Debug, Clone)]
445struct ReliableTxState {
446    next_seq: u32,
447    sent_order: VecDeque<u32>,
448    sent: BTreeMap<u32, ReliableSent>,
449}
450
451#[derive(Debug, Clone)]
452struct ReliableSent {
453    bytes: crate::SharedBytes,
454    last_send_ms: u64,
455    retries: u32,
456    queued: bool,
457    partial_acked: bool,
458}
459
460#[derive(Debug, Clone)]
461struct ReliableRxState {
462    expected_seq: u32,
463    buffered: BTreeMap<u32, crate::SharedBytes>,
464}
465
466#[derive(Debug, Clone)]
467struct ReliableReturnRouteState {
468    side: RelaySideId,
469    last_replay_ms: u64,
470}
471
472#[cfg(feature = "discovery")]
473#[derive(Debug, Clone, Default, PartialEq, Eq)]
474struct DiscoverySenderState {
475    /// Numeric identity advertised for this hostname. Compact frames may carry
476    /// this address instead of the hostname, so routing must accept either.
477    advertised_address: Option<u32>,
478    reachable: Vec<crate::DataEndpoint>,
479    advertised_reachable: Vec<crate::DataEndpoint>,
480    reachable_timesync_sources: Vec<String>,
481    advertised_reachable_timesync_sources: Vec<String>,
482    topology_boards: Arc<Vec<TopologyBoardNode>>,
483    has_full_topology: bool,
484    has_schema: bool,
485    last_seen_ms: u64,
486}
487
488#[inline]
489fn is_internal_control_type(ty: crate::DataType) -> bool {
490    if matches!(
491        ty,
492        crate::DataType::ReliableAck
493            | crate::DataType::ReliablePartialAck
494            | crate::DataType::ReliablePacketRequest
495            | crate::DataType::P2pMessage
496    ) {
497        return true;
498    }
499
500    #[cfg(feature = "timesync")]
501    if matches!(
502        ty,
503        crate::DataType::TimeSyncAnnounce
504            | crate::DataType::TimeSyncRequest
505            | crate::DataType::TimeSyncResponse
506    ) {
507        return true;
508    }
509
510    #[cfg(feature = "discovery")]
511    if discovery::is_discovery_type(ty) {
512        return true;
513    }
514
515    let _ = ty;
516    false
517}
518
519#[cfg(feature = "discovery")]
520#[derive(Debug, Clone, Default, PartialEq, Eq)]
521struct DiscoverySideState {
522    reachable: Vec<crate::DataEndpoint>,
523    reachable_timesync_sources: Vec<String>,
524    last_seen_ms: u64,
525    announcers: BTreeMap<String, DiscoverySenderState>,
526}
527
528#[derive(Clone, Debug, Default)]
529struct SideTransportState {
530    tx_chunk_progress: Option<Box<crate::side_transport::SideTxProgress>>,
531    tx_template_ids: BTreeMap<u64, u32>,
532    tx_templates: BTreeMap<u64, SideHeaderTemplate>,
533    tx_last_timestamps: BTreeMap<u32, u64>,
534    tx_compact_uses: BTreeMap<u32, u8>,
535    rx_templates: BTreeMap<u64, u32>,
536    rx_templates_by_id: BTreeMap<u32, SideHeaderTemplate>,
537    rx_last_timestamps: BTreeMap<u32, u64>,
538    rx_chunks: BTreeMap<u32, SideChunkAssembly>,
539    rx_chunk_reclaims: u32,
540    rx_chunk_bytes: usize,
541    next_template_id: u32,
542}
543
544impl SideTransportState {
545    #[inline(never)]
546    fn take_chunk(&mut self, id: u32, reclaimed: bool) -> Option<SideChunkAssembly> {
547        let value = SideChunkAssembly::remove(&mut self.rx_chunks, &mut self.rx_chunk_bytes, id)?;
548        if reclaimed {
549            self.rx_chunk_reclaims = self.rx_chunk_reclaims.saturating_add(1);
550        }
551        Some(value)
552    }
553    /// Seed fixtures that construct raw maps directly.
554    #[cfg(test)]
555    #[inline(never)]
556    fn refresh_rx_chunk_bytes(&mut self) {
557        self.rx_chunk_bytes = self
558            .rx_chunks
559            .values()
560            .map(SideChunkAssembly::byte_cost)
561            .sum();
562    }
563    fn refresh_transmit_header_templates(&mut self) {
564        // Refresh on the wire, not by forgetting the eviction history. RX
565        // retains its dictionary; clearing only TX makes bounded caches evict
566        // different entries after the next insertion/capacity negotiation.
567        for uses in self.tx_compact_uses.values_mut() {
568            *uses = u8::MAX;
569        }
570        // Local topology changes do not invalidate dictionaries received from
571        // peers. Dropping RX state here loses already-in-flight compact ACKs.
572    }
573
574    fn tx_template_count(&self) -> usize {
575        self.tx_template_ids.len()
576    }
577
578    fn rx_template_count(&self) -> usize {
579        self.rx_templates_by_id.len()
580    }
581
582    fn trim_tx_templates_to(&mut self, limit: usize) -> usize {
583        let mut removed = 0usize;
584        while self.tx_template_ids.len() > limit {
585            let Some(old_hash) = self.tx_template_ids.keys().next().copied() else {
586                break;
587            };
588            if let Some(old_id) = self.tx_template_ids.remove(&old_hash) {
589                self.tx_last_timestamps.remove(&old_id);
590                self.tx_compact_uses.remove(&old_id);
591            }
592            self.tx_templates.remove(&old_hash);
593            removed += 1;
594        }
595        removed
596    }
597
598    fn insert_tx_template(
599        &mut self,
600        template: SideHeaderTemplate,
601        template_id: u32,
602        max_templates: usize,
603    ) -> bool {
604        if max_templates == 0 {
605            return false;
606        }
607        let mut evicted = false;
608        if self.tx_template_ids.len() >= max_templates
609            && !self.tx_template_ids.contains_key(&template.hash)
610            && let Some(old_hash) = self.tx_template_ids.keys().next().copied()
611        {
612            if let Some(old_id) = self.tx_template_ids.remove(&old_hash) {
613                self.tx_last_timestamps.remove(&old_id);
614                self.tx_compact_uses.remove(&old_id);
615            }
616            self.tx_templates.remove(&old_hash);
617            evicted = true;
618        }
619        self.tx_template_ids.insert(template.hash, template_id);
620        self.tx_templates.insert(template.hash, template);
621        self.tx_compact_uses.insert(template_id, 0);
622        evicted
623    }
624
625    fn insert_rx_template(
626        &mut self,
627        template_id: u32,
628        template: SideHeaderTemplate,
629        max_templates: usize,
630    ) -> bool {
631        if max_templates == 0 {
632            return false;
633        }
634        // Keep one owned header. The hash index needs only its wire ID, not
635        // another copy of the header and its shared-buffer owners.
636        if let Some(old_id) = self.rx_templates.get(&template.hash).copied()
637            && old_id != template_id
638        {
639            self.rx_templates_by_id.remove(&old_id);
640            self.rx_last_timestamps.remove(&old_id);
641            self.rx_templates.remove(&template.hash);
642        }
643        if let Some(previous) = self.rx_templates_by_id.get(&template_id)
644            && previous.hash != template.hash
645        {
646            self.rx_templates.remove(&previous.hash);
647            self.rx_last_timestamps.remove(&template_id);
648        }
649        let mut evicted = false;
650        if self.rx_templates_by_id.len() >= max_templates
651            && !self.rx_templates_by_id.contains_key(&template_id)
652            // Match TX's deterministic lowest-hash eviction on both routers
653            // and relays; lowest wire ID can select a different live header.
654            && let Some((&old_hash, &old_id)) = self.rx_templates.first_key_value()
655        {
656            self.rx_templates.remove(&old_hash);
657            self.rx_templates_by_id.remove(&old_id);
658            self.rx_last_timestamps.remove(&old_id);
659            evicted = true;
660        }
661        self.rx_templates.insert(template.hash, template_id);
662        self.rx_templates_by_id.insert(template_id, template);
663        evicted
664    }
665}
666
667#[derive(Debug, Clone, Default)]
668struct AdaptiveRouteStats {
669    estimated_bandwidth_bps: u64,
670    peak_bandwidth_bps: u64,
671    last_observed_ms: u64,
672    last_slow_observed_ms: u64,
673    sample_count: u64,
674    window_started_ms: u64,
675    window_bytes: u64,
676    peak_usage_bps: u64,
677}
678
679#[cfg(feature = "discovery")]
680#[derive(Debug, Clone, Default)]
681struct DiscoverySideThrottleState {
682    next_topology_request_ms: u64,
683    next_schema_request_ms: u64,
684    next_ping_ms: u64,
685    // Scheduled and accepted sends differ when a constrained link is backed up.
686    last_liveness_tx_ms: u64,
687    pending_incremental: bool,
688    incremental_retries_remaining: u8,
689    has_sent_full: bool,
690    last_topology: Vec<TopologyBoardNode>,
691}
692
693#[cfg(feature = "discovery")]
694impl DiscoverySideThrottleState {
695    fn liveness_due(&self, now_ms: u64) -> bool {
696        now_ms >= self.next_ping_ms
697            && now_ms.saturating_sub(self.last_liveness_tx_ms)
698                >= DISCOVERY_SLOW_LINK_PING_INTERVAL_MS
699    }
700}
701
702#[cfg(all(feature = "discovery", feature = "timesync"))]
703#[derive(Debug, Clone, Default)]
704struct TimeSyncSideThrottleState {
705    next_allowed_ms: u64,
706}
707
708#[cfg(feature = "discovery")]
709#[derive(Debug, Clone, Copy, PartialEq, Eq)]
710enum DiscoveryAdvertiseLevel {
711    MinimalPing,
712    Incremental,
713    Full,
714}
715
716impl AdaptiveRouteStats {
717    #[inline]
718    fn observe(&mut self, bytes: usize, sample_bps: u64, now_ms: u64) {
719        self.estimated_bandwidth_bps = if self.estimated_bandwidth_bps == 0 {
720            sample_bps
721        } else if sample_bps >= self.estimated_bandwidth_bps {
722            self.estimated_bandwidth_bps
723                .saturating_mul(3)
724                .saturating_add(sample_bps.saturating_mul(5))
725                / 8
726        } else {
727            self.estimated_bandwidth_bps
728                .saturating_mul(7)
729                .saturating_add(sample_bps)
730                / 8
731        };
732        self.peak_bandwidth_bps = self.peak_bandwidth_bps.max(sample_bps);
733        self.last_observed_ms = now_ms;
734        if sample_bps > 0 && sample_bps <= CONTROL_SLOW_LINK_CAPACITY_BPS {
735            self.last_slow_observed_ms = now_ms;
736        }
737        self.sample_count = self.sample_count.saturating_add(1);
738        if self.window_started_ms == 0 || now_ms.saturating_sub(self.window_started_ms) > 1_000 {
739            self.window_started_ms = now_ms;
740            self.window_bytes = 0;
741        }
742        self.window_bytes = self.window_bytes.saturating_add(bytes as u64);
743        self.peak_usage_bps = self.peak_usage_bps.max(self.current_usage_bps(now_ms));
744    }
745
746    #[inline]
747    fn current_usage_bps(&self, now_ms: u64) -> u64 {
748        if self.window_started_ms == 0 {
749            return 0;
750        }
751        let elapsed_ms = now_ms.saturating_sub(self.window_started_ms).max(1);
752        (u128::from(self.window_bytes).saturating_mul(1000) / u128::from(elapsed_ms))
753            .min(u128::from(u64::MAX)) as u64
754    }
755
756    #[inline]
757    fn available_headroom_bps(&self, now_ms: u64) -> u64 {
758        let capacity = self
759            .estimated_bandwidth_bps
760            .max(self.peak_bandwidth_bps)
761            .max(1);
762        capacity.saturating_sub(self.current_usage_bps(now_ms))
763    }
764
765    #[inline]
766    fn weight(&self, now_ms: u64) -> u64 {
767        self.available_headroom_bps(now_ms).max(1)
768    }
769
770    #[inline]
771    fn snapshot(&self, now_ms: u64, auto_balancing_enabled: bool) -> AdaptiveLinkStats {
772        let current_usage_bps = self.current_usage_bps(now_ms);
773        let estimated_capacity_bps = self.estimated_bandwidth_bps.max(1);
774        let peak_capacity_bps = self.peak_bandwidth_bps.max(estimated_capacity_bps);
775        let available_headroom_bps = peak_capacity_bps.saturating_sub(current_usage_bps);
776        AdaptiveLinkStats {
777            auto_balancing_enabled,
778            estimated_capacity_bps,
779            peak_capacity_bps,
780            current_usage_bps,
781            peak_usage_bps: self.peak_usage_bps.max(current_usage_bps),
782            available_headroom_bps,
783            effective_weight: available_headroom_bps.max(1),
784            last_observed_ms: self.last_observed_ms,
785            sample_count: self.sample_count,
786        }
787    }
788}
789
790use crate::type_stats::TypeRuntimeStatsInner;
791
792#[derive(Debug, Clone, Default)]
793struct SideRuntimeStatsInner {
794    tx_packets: u64,
795    tx_bytes: u64,
796    rx_packets: u64,
797    rx_bytes: u64,
798    relayed_tx_packets: u64,
799    relayed_tx_bytes: u64,
800    relayed_rx_packets: u64,
801    relayed_rx_bytes: u64,
802    tx_retries: u64,
803    tx_handler_failures: u64,
804    total_handler_retries: u64,
805    side_transport_full_frames: u64,
806    side_transport_compact_frames: u64,
807    side_transport_compact_delta_frames: u64,
808    side_transport_compact_omitted_timestamp_frames: u64,
809    side_transport_chunk_frames: u64,
810    side_transport_raw_bytes: u64,
811    side_transport_wire_bytes: u64,
812    side_transport_bytes_saved: u64,
813    side_transport_min_compact_overhead_bytes: Option<usize>,
814    side_transport_max_compact_overhead_bytes: Option<usize>,
815    side_transport_compact_target_misses: u64,
816    side_transport_template_evictions: u64,
817    data_types: BTreeMap<u32, TypeRuntimeStatsInner>,
818}
819
820impl SideRuntimeStatsInner {
821    fn type_stats_mut(&mut self, ty: crate::DataType) -> &mut TypeRuntimeStatsInner {
822        self.data_types.entry(ty.as_u32()).or_default()
823    }
824
825    fn note_tx(&mut self, ty: crate::DataType, bytes: usize, retries: usize) {
826        self.tx_packets = self.tx_packets.saturating_add(1);
827        self.tx_bytes = self.tx_bytes.saturating_add(bytes as u64);
828        self.relayed_tx_packets = self.relayed_tx_packets.saturating_add(1);
829        self.relayed_tx_bytes = self.relayed_tx_bytes.saturating_add(bytes as u64);
830        self.tx_retries = self.tx_retries.saturating_add(retries as u64);
831        self.total_handler_retries = self.total_handler_retries.saturating_add(retries as u64);
832        self.type_stats_mut(ty)
833            .tx(bytes as u64, true, retries as u64);
834    }
835
836    fn note_rx(&mut self, ty: crate::DataType, bytes: usize) {
837        self.rx_packets = self.rx_packets.saturating_add(1);
838        self.rx_bytes = self.rx_bytes.saturating_add(bytes as u64);
839        self.relayed_rx_packets = self.relayed_rx_packets.saturating_add(1);
840        self.relayed_rx_bytes = self.relayed_rx_bytes.saturating_add(bytes as u64);
841        self.type_stats_mut(ty).rx(bytes as u64, true);
842    }
843
844    fn note_tx_failure(&mut self, ty: crate::DataType, retries: usize) {
845        self.tx_handler_failures = self.tx_handler_failures.saturating_add(1);
846        self.tx_retries = self.tx_retries.saturating_add(retries as u64);
847        self.total_handler_retries = self.total_handler_retries.saturating_add(retries as u64);
848        self.type_stats_mut(ty).failure(retries as u64);
849    }
850
851    fn note_side_transport_full(&mut self, raw_bytes: usize, wire_bytes: usize) {
852        self.side_transport_full_frames = self.side_transport_full_frames.saturating_add(1);
853        self.note_side_transport_bytes(raw_bytes, wire_bytes);
854    }
855
856    fn note_side_transport_compact(
857        &mut self,
858        raw_bytes: usize,
859        wire_bytes: usize,
860        compact_overhead_bytes: usize,
861        used_timestamp_delta: bool,
862        omitted_timestamp: bool,
863    ) {
864        self.side_transport_compact_frames = self.side_transport_compact_frames.saturating_add(1);
865        if used_timestamp_delta {
866            self.side_transport_compact_delta_frames =
867                self.side_transport_compact_delta_frames.saturating_add(1);
868        }
869        if omitted_timestamp {
870            self.side_transport_compact_omitted_timestamp_frames = self
871                .side_transport_compact_omitted_timestamp_frames
872                .saturating_add(1);
873        }
874        self.note_side_transport_bytes(raw_bytes, wire_bytes);
875        self.side_transport_min_compact_overhead_bytes = Some(
876            self.side_transport_min_compact_overhead_bytes
877                .map_or(compact_overhead_bytes, |v| v.min(compact_overhead_bytes)),
878        );
879        self.side_transport_max_compact_overhead_bytes = Some(
880            self.side_transport_max_compact_overhead_bytes
881                .map_or(compact_overhead_bytes, |v| v.max(compact_overhead_bytes)),
882        );
883    }
884
885    fn note_side_transport_chunks(&mut self, chunks: usize) {
886        self.side_transport_chunk_frames = self
887            .side_transport_chunk_frames
888            .saturating_add(chunks as u64);
889    }
890
891    fn note_side_transport_bytes(&mut self, raw_bytes: usize, wire_bytes: usize) {
892        self.side_transport_raw_bytes = self
893            .side_transport_raw_bytes
894            .saturating_add(raw_bytes as u64);
895        self.side_transport_wire_bytes = self
896            .side_transport_wire_bytes
897            .saturating_add(wire_bytes as u64);
898        if raw_bytes > wire_bytes {
899            self.side_transport_bytes_saved = self
900                .side_transport_bytes_saved
901                .saturating_add((raw_bytes - wire_bytes) as u64);
902        }
903    }
904
905    fn note_side_transport_compact_target_miss(&mut self) {
906        self.side_transport_compact_target_misses =
907            self.side_transport_compact_target_misses.saturating_add(1);
908    }
909
910    fn note_side_transport_template_eviction(&mut self) {
911        self.side_transport_template_evictions =
912            self.side_transport_template_evictions.saturating_add(1);
913    }
914}
915
916#[derive(Clone, Debug)]
917pub struct RelayConfig {
918    sender: Arc<str>,
919    memory: RuntimeMemoryConfig,
920}
921
922impl RelayConfig {
923    pub fn new() -> Self {
924        Self::default()
925    }
926
927    pub fn with_sender<S: AsRef<str>>(mut self, sender: S) -> Self {
928        self.sender = Arc::from(sender.as_ref());
929        self
930    }
931
932    pub fn with_memory_config(mut self, memory: RuntimeMemoryConfig) -> TelemetryResult<Self> {
933        memory.validate()?;
934        self.memory = memory;
935        Ok(self)
936    }
937
938    fn sender(&self) -> Arc<str> {
939        self.sender.clone()
940    }
941
942    fn memory_config(&self) -> RuntimeMemoryConfig {
943        self.memory
944    }
945}
946
947impl Default for RelayConfig {
948    fn default() -> Self {
949        Self {
950            sender: Arc::from("RELAY"),
951            memory: RuntimeMemoryConfig::default(),
952        }
953    }
954}
955
956#[derive(Clone, Copy, Debug, PartialEq, Eq)]
957enum RouteSelectionOrigin {
958    Flood,
959    Discovered,
960}
961
962/// Internal state, protected by RouterMutex so all public methods can take &self.
963struct RelayInner {
964    memory: RuntimeMemoryConfig,
965    sides: Vec<Option<RelaySide>>,
966    route_overrides: BTreeMap<(Option<RelaySideId>, RelaySideId), bool>,
967    typed_route_overrides: BTreeMap<(Option<RelaySideId>, u32, RelaySideId), bool>,
968    route_weights: BTreeMap<(Option<RelaySideId>, RelaySideId), u32>,
969    route_priorities: BTreeMap<(Option<RelaySideId>, RelaySideId), u32>,
970    source_route_modes: BTreeMap<Option<RelaySideId>, RouteSelectionMode>,
971    route_selection_cursors: BTreeMap<Option<RelaySideId>, u64>,
972    adaptive_route_stats: BTreeMap<RelaySideId, AdaptiveRouteStats>,
973    // Sparse side tables must not reserve eleven full metadata values in
974    // every B-tree leaf. Own each side once; keep the index pointer-sized.
975    side_runtime_stats: BTreeMap<RelaySideId, Box<SideRuntimeStatsInner>>,
976    side_transport: BTreeMap<RelaySideId, Box<SideTransportState>>,
977    rx_queue: BoundedDeque<RelayRxItem>,
978    tx_queue: BoundedDeque<RelayTxItem>,
979    tx_priority_burst: u8,
980    replay_queue: BoundedDeque<RelayReplayItem>,
981    recent_rx: BoundedDeque<u64>,
982    reliable_tx: BTreeMap<(RelaySideId, u32), ReliableTxState>,
983    reliable_rx: BTreeMap<(RelaySideId, u32), ReliableRxState>,
984    reliable_return_routes: BTreeMap<u64, ReliableReturnRouteState>,
985    reliable_return_route_order: VecDeque<u64>,
986    end_to_end_acked_destinations: BTreeMap<u64, BTreeSet<u64>>,
987    end_to_end_acked_destination_order: VecDeque<u64>,
988    total_handler_failures: u64,
989    total_handler_retries: u64,
990    #[cfg(feature = "discovery")]
991    discovery_routes: BTreeMap<RelaySideId, DiscoverySideState>,
992    #[cfg(feature = "discovery")]
993    discovery_cadence: DiscoveryCadenceState,
994    #[cfg(feature = "discovery")]
995    discovery_side_throttle: BTreeMap<RelaySideId, DiscoverySideThrottleState>,
996    #[cfg(all(feature = "discovery", feature = "timesync"))]
997    timesync_side_throttle: BTreeMap<RelaySideId, TimeSyncSideThrottleState>,
998}
999
1000#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1001enum RelayQueueKind {
1002    Rx,
1003    Tx,
1004    Replay,
1005}
1006
1007impl RelayInner {
1008    #[cfg(any(feature = "std", feature = "fair-admission"))]
1009    fn fair_bulk_owner(data: &RelayItem, priority: u8) -> Option<u32> {
1010        match data {
1011            RelayItem::Packet(packet) => crate::admission_fairness::packet_owner(packet, priority),
1012            RelayItem::Packed(bytes) => crate::shared_bytes::with_slice(bytes, |view| {
1013                crate::admission_fairness::packed_owner(view, priority)
1014            })
1015            .ok()
1016            .flatten(),
1017        }
1018    }
1019    #[cfg(any(feature = "std", feature = "fair-admission"))]
1020    fn bulk_stream(data: &RelayItem) -> Option<u32> {
1021        match data {
1022            RelayItem::Packet(packet) => Some(packet.data_type().as_u32()),
1023            RelayItem::Packed(bytes) => crate::shared_bytes::with_slice(bytes, |view| {
1024                wire_format::scheduling_type(view).map(|ty| ty.as_u32())
1025            })
1026            .ok()
1027            .flatten(),
1028        }
1029    }
1030    #[cfg(any(feature = "std", feature = "fair-admission"))]
1031    fn bulk_loads(&self, priority: u8) -> crate::admission_fairness::Loads {
1032        let mut loads = crate::admission_fairness::Loads::new();
1033        for item in self.rx_queue.iter() {
1034            if item.priority <= priority
1035                && let Some(owner) = Self::fair_bulk_owner(&item.data, item.priority)
1036            {
1037                loads.record(owner, item.byte_cost());
1038            }
1039        }
1040        for item in self.tx_queue.iter() {
1041            if item.priority <= priority
1042                && let Some(owner) = Self::fair_bulk_owner(&item.data, item.priority)
1043            {
1044                loads.record(owner, item.byte_cost());
1045            }
1046        }
1047        loads
1048    }
1049    #[cfg(any(feature = "std", feature = "fair-admission"))]
1050    fn competing_bulk_owner(&self, incoming: u32, priority: u8) -> Option<u32> {
1051        self.bulk_loads(priority).dominant_other(incoming)
1052    }
1053    #[cfg(any(feature = "std", feature = "fair-admission"))]
1054    fn reclaim_bulk_from(
1055        &mut self,
1056        owner: u32,
1057        priority: u8,
1058        incoming_stream: Option<u32>,
1059    ) -> bool {
1060        let mut streams = crate::admission_fairness::Loads::new();
1061        for item in self.rx_queue.iter() {
1062            if item.priority <= priority
1063                && Self::fair_bulk_owner(&item.data, item.priority) == Some(owner)
1064                && let Some(ty) = Self::bulk_stream(&item.data)
1065            {
1066                streams.record(ty, item.byte_cost());
1067            }
1068        }
1069        for item in self.tx_queue.iter() {
1070            if item.priority <= priority
1071                && Self::fair_bulk_owner(&item.data, item.priority) == Some(owner)
1072                && let Some(ty) = Self::bulk_stream(&item.data)
1073            {
1074                streams.record(ty, item.byte_cost());
1075            }
1076        }
1077        let stream = streams.queue_victim(incoming_stream);
1078        let rx = self
1079            .rx_queue
1080            .iter()
1081            .enumerate()
1082            .filter(|(_, item)| {
1083                item.priority <= priority
1084                    && Self::fair_bulk_owner(&item.data, item.priority) == Some(owner)
1085                    && stream.is_none_or(|ty| Self::bulk_stream(&item.data) == Some(ty))
1086            })
1087            .min_by_key(|(index, item)| (item.priority, *index))
1088            .map(|(index, item)| (index, item.priority));
1089        let tx = self
1090            .tx_queue
1091            .iter()
1092            .enumerate()
1093            .filter(|(_, item)| {
1094                item.priority <= priority
1095                    && Self::fair_bulk_owner(&item.data, item.priority) == Some(owner)
1096                    && stream.is_none_or(|ty| Self::bulk_stream(&item.data) == Some(ty))
1097            })
1098            .min_by_key(|(index, item)| (item.priority, *index))
1099            .map(|(index, item)| (index, item.priority));
1100        match (rx, tx) {
1101            (Some((index, rp)), Some((_, tp))) if rp <= tp => {
1102                self.rx_queue.remove_pos(index).is_some()
1103            }
1104            (Some((index, _)), None) => self.rx_queue.remove_pos(index).is_some(),
1105            (_, Some((index, _))) => self.tx_queue.remove_pos(index).is_some(),
1106            _ => false,
1107        }
1108    }
1109
1110    #[cfg(feature = "discovery")]
1111    fn topology_board_byte_cost(board: &TopologyBoardNode) -> usize {
1112        board
1113            .sender_id
1114            .len()
1115            .saturating_add(board.reachable_endpoints.len() * size_of::<crate::DataEndpoint>())
1116            .saturating_add(
1117                board
1118                    .reachable_timesync_sources
1119                    .iter()
1120                    .map(|s| s.len())
1121                    .sum::<usize>(),
1122            )
1123            .saturating_add(board.connections.iter().map(|s| s.len()).sum::<usize>())
1124    }
1125
1126    #[cfg(feature = "discovery")]
1127    fn discovery_sender_byte_cost(sender: &str, state: &DiscoverySenderState) -> usize {
1128        sender
1129            .len()
1130            .saturating_add(state.reachable.len() * size_of::<crate::DataEndpoint>())
1131            .saturating_add(state.advertised_reachable.len() * size_of::<crate::DataEndpoint>())
1132            .saturating_add(
1133                state
1134                    .reachable_timesync_sources
1135                    .iter()
1136                    .map(|s| s.len())
1137                    .sum::<usize>(),
1138            )
1139            .saturating_add(
1140                state
1141                    .advertised_reachable_timesync_sources
1142                    .iter()
1143                    .map(|s| s.len())
1144                    .sum::<usize>(),
1145            )
1146            .saturating_add(
1147                state
1148                    .topology_boards
1149                    .iter()
1150                    .map(Self::topology_board_byte_cost)
1151                    .sum::<usize>(),
1152            )
1153            .saturating_add(size_of::<DiscoverySenderState>())
1154    }
1155
1156    #[cfg(feature = "discovery")]
1157    fn discovery_route_byte_cost(route: &DiscoverySideState) -> usize {
1158        size_of::<DiscoverySideState>()
1159            .saturating_add(route.reachable.len() * size_of::<crate::DataEndpoint>())
1160            .saturating_add(
1161                route
1162                    .reachable_timesync_sources
1163                    .iter()
1164                    .map(|s| s.len())
1165                    .sum::<usize>(),
1166            )
1167            .saturating_add(
1168                route
1169                    .announcers
1170                    .iter()
1171                    .map(|(sender, state)| Self::discovery_sender_byte_cost(sender, state))
1172                    .sum::<usize>(),
1173            )
1174    }
1175
1176    #[cfg(feature = "discovery")]
1177    fn discovery_bytes_used(&self) -> usize {
1178        self.discovery_routes
1179            .values()
1180            .map(Self::discovery_route_byte_cost)
1181            .sum()
1182    }
1183
1184    #[inline]
1185    fn reliable_rx_buffered_bytes(&self) -> usize {
1186        self.reliable_rx
1187            .values()
1188            .flat_map(|state| state.buffered.values())
1189            .map(|bytes| size_of::<crate::SharedBytes>() + bytes.len())
1190            .sum()
1191    }
1192
1193    #[inline(never)]
1194    fn side_chunk_bytes_used(&self) -> usize {
1195        self.side_transport
1196            .values()
1197            .map(|side| side.rx_chunk_bytes)
1198            .sum()
1199    }
1200    fn discard_side_chunk(&mut self, side: RelaySideId, id: u32) -> bool {
1201        self.side_transport
1202            .get_mut(&side)
1203            .is_some_and(|state| state.take_chunk(id, true).is_some())
1204    }
1205    fn reclaim_oldest_side_chunk(&mut self, keep: Option<(RelaySideId, u32)>) -> bool {
1206        let mut victim = None;
1207        let mut time = u64::MAX;
1208        for (&side, state) in &self.side_transport {
1209            let preserved = match keep {
1210                Some((owner, id)) if owner == side => Some(id),
1211                _ => None,
1212            };
1213            if let Some((id, seen)) = SideChunkAssembly::oldest(&state.rx_chunks, preserved)
1214                && (victim.is_none() || seen < time)
1215            {
1216                time = seen;
1217                victim = Some((side, id));
1218            }
1219        }
1220        match victim {
1221            Some((side, id)) => self.discard_side_chunk(side, id),
1222            None => false,
1223        }
1224    }
1225    fn make_side_chunk_room(
1226        &mut self,
1227        side: RelaySideId,
1228        id: u32,
1229        index: u16,
1230        total: u16,
1231        payload_len: usize,
1232    ) -> TelemetryResult<()> {
1233        let budget = self
1234            .memory
1235            .max_queue_budget
1236            .saturating_sub(self.protected_metadata_bytes_used());
1237        let state = self
1238            .side_transport
1239            .get_mut(&side)
1240            .ok_or(TelemetryError::BadArg)?;
1241        let existing = state.rx_chunks.get(&id).map(|a| a.total);
1242        if existing.is_some_and(|old| old != 0 && old != total) {
1243            state.take_chunk(id, true);
1244            return Err(TelemetryError::Unpack("side chunk total mismatch"));
1245        }
1246        let before = state.rx_chunks.len();
1247        let room = SideChunkAssembly::make_room(
1248            &mut state.rx_chunks,
1249            &mut state.rx_chunk_bytes,
1250            id,
1251            index,
1252            total,
1253            payload_len,
1254            budget,
1255        );
1256        if before != state.rx_chunks.len() {
1257            state.rx_chunk_reclaims = state
1258                .rx_chunk_reclaims
1259                .saturating_add((before - state.rx_chunks.len()) as u32);
1260        }
1261        let cost = room?;
1262        if cost == 0 {
1263            return Ok(());
1264        }
1265        if existing.is_none()
1266            && state.rx_chunks.len() >= 4
1267            && let Some((oldest, _)) = SideChunkAssembly::oldest(&state.rx_chunks, None)
1268        {
1269            state.take_chunk(oldest, true);
1270        }
1271        while self.shared_queue_bytes_used().saturating_add(cost) > self.memory.max_queue_budget {
1272            #[cfg(any(feature = "std", feature = "fair-admission"))]
1273            if let Some(owner) = self.bulk_loads(199).queue_victim(None)
1274                && self.reclaim_bulk_from(owner, 199, None)
1275            {
1276                continue;
1277            }
1278            if self.reclaim_oldest_side_chunk(Some((side, id))) {
1279                continue;
1280            }
1281            // The sender retries this fragment, not the earlier accepted ones.
1282            // Keep the bounded partial assembly while protected queues drain.
1283            return Err(TelemetryError::Io("memory pressure"));
1284        }
1285        Ok(())
1286    }
1287
1288    #[inline(never)]
1289    fn protected_metadata_bytes_used(&self) -> usize {
1290        self.recent_rx
1291            .max_bytes()
1292            .saturating_add(self.reliable_rx_buffered_bytes())
1293            .saturating_add(crate::config::schema_bytes_used())
1294            .saturating_add({
1295                #[cfg(feature = "discovery")]
1296                {
1297                    self.discovery_bytes_used()
1298                }
1299                #[cfg(not(feature = "discovery"))]
1300                {
1301                    0
1302                }
1303            })
1304    }
1305    #[inline]
1306    fn shared_queue_bytes_used(&self) -> usize {
1307        self.rx_queue
1308            .bytes_used()
1309            .saturating_add(self.tx_queue.bytes_used())
1310            .saturating_add(self.replay_queue.bytes_used())
1311            .saturating_add(self.side_chunk_bytes_used())
1312            .saturating_add(self.protected_metadata_bytes_used())
1313    }
1314
1315    fn reliable_rx_buffer_len(&self) -> usize {
1316        self.reliable_rx
1317            .values()
1318            .map(|state| state.buffered.len())
1319            .sum()
1320    }
1321
1322    fn pop_reliable_rx_buffered(&mut self) -> Option<crate::SharedBytes> {
1323        let key = self
1324            .reliable_rx
1325            .iter()
1326            .find_map(|(key, state)| (!state.buffered.is_empty()).then_some(*key))?;
1327        self.reliable_rx
1328            .get_mut(&key)?
1329            .buffered
1330            .pop_first()
1331            .map(|(_, v)| v)
1332    }
1333
1334    fn pop_lowest_packet_queue_item(&mut self, below: usize) -> bool {
1335        let victim = [
1336            (
1337                RelayQueueKind::Rx,
1338                self.rx_queue.lowest_priority(|item| item.priority),
1339            ),
1340            (
1341                RelayQueueKind::Tx,
1342                self.tx_queue.lowest_priority(|item| item.priority),
1343            ),
1344            (
1345                RelayQueueKind::Replay,
1346                self.replay_queue.lowest_priority(|item| item.priority),
1347            ),
1348        ]
1349        .into_iter()
1350        .filter_map(|(kind, priority)| priority.map(|priority| (kind, priority)))
1351        .filter(|(_, priority)| usize::from(*priority) < below)
1352        .min_by_key(|(_, priority)| *priority)
1353        .map(|(kind, _)| kind);
1354        match victim {
1355            Some(RelayQueueKind::Rx) => self.rx_queue.pop_lowest_priority().is_some(),
1356            Some(RelayQueueKind::Tx) => self.tx_queue.pop_lowest_priority().is_some(),
1357            Some(RelayQueueKind::Replay) => self.replay_queue.pop_lowest_priority().is_some(),
1358            _ => false,
1359        }
1360    }
1361
1362    fn make_shared_queue_room(
1363        &mut self,
1364        incoming_cost: usize,
1365        _incoming_priority: u8,
1366        _incoming: Option<&RelayItem>,
1367    ) -> TelemetryResult<()> {
1368        if incoming_cost > self.memory.max_queue_budget {
1369            return Err(TelemetryError::PacketTooLarge(
1370                "Item exceeds maximum shared queue budget",
1371            ));
1372        }
1373
1374        #[cfg(any(feature = "std", feature = "fair-admission"))]
1375        let mut fair_victim = None;
1376        while self.shared_queue_bytes_used().saturating_add(incoming_cost)
1377            > self.memory.max_queue_budget
1378        {
1379            #[cfg(any(feature = "std", feature = "fair-admission"))]
1380            {
1381                let incoming =
1382                    _incoming.and_then(|data| Self::fair_bulk_owner(data, _incoming_priority));
1383                let victim = *fair_victim.get_or_insert_with(|| {
1384                    self.bulk_loads(_incoming_priority.min(199))
1385                        .queue_victim(incoming)
1386                });
1387                if let Some(victim) = victim
1388                    && self.reclaim_bulk_from(
1389                        victim,
1390                        _incoming_priority.min(199),
1391                        (incoming == Some(victim))
1392                            .then(|| _incoming.and_then(Self::bulk_stream))
1393                            .flatten(),
1394                    )
1395                {
1396                    continue;
1397                }
1398                if incoming.is_some() {
1399                    // Best effort cannot replace another peer's sole report,
1400                    // reliable replay, schema, or protected control.
1401                    return Err(TelemetryError::Io("shared fair queue saturated"));
1402                }
1403            }
1404            // Incoming work cannot evict the routes required to deliver it.
1405            // Prefer low-priority packets; refuse admission once only metadata
1406            // remains rather than treating topology as an ordinary queue.
1407            if !self.pop_lowest_packet_queue_item(256) && self.pop_reliable_rx_buffered().is_none()
1408            {
1409                if _incoming_priority >= 200 && self.reclaim_oldest_side_chunk(None) {
1410                    continue;
1411                }
1412                return Err(TelemetryError::PacketTooLarge(
1413                    "Item exceeds maximum shared queue budget",
1414                ));
1415            }
1416        }
1417
1418        Ok(())
1419    }
1420
1421    #[cfg(feature = "discovery")]
1422    fn schema_growth_budget(&self) -> usize {
1423        // Queued packets are reclaimable. Existing reachability and active
1424        // reliable reassembly remain protected when admitting a new schema.
1425        let protected = self
1426            .recent_rx
1427            .max_bytes()
1428            .saturating_add(self.discovery_bytes_used())
1429            .saturating_add(self.reliable_rx_buffered_bytes());
1430        self.memory.max_queue_budget.saturating_sub(protected)
1431    }
1432
1433    #[cfg(feature = "discovery")]
1434    fn commit_discovery_route(
1435        &mut self,
1436        side: RelaySideId,
1437        route: DiscoverySideState,
1438    ) -> TelemetryResult<()> {
1439        let previous = self
1440            .discovery_routes
1441            .get(&side)
1442            .map_or(0, Self::discovery_route_byte_cost);
1443        let incoming = Self::discovery_route_byte_cost(&route);
1444        let growth = incoming.saturating_sub(previous);
1445        while self.shared_queue_bytes_used().saturating_add(growth) > self.memory.max_queue_budget {
1446            if !self.pop_lowest_packet_queue_item(256) {
1447                return Err(TelemetryError::Io("shared priority queue saturated"));
1448            }
1449        }
1450        self.discovery_routes.insert(side, route);
1451        Ok(())
1452    }
1453
1454    #[cfg(feature = "discovery")]
1455    fn fit_discovery_budget(&mut self) {
1456        while self.shared_queue_bytes_used() > self.memory.max_queue_budget {
1457            if self.pop_lowest_packet_queue_item(256) {
1458                continue;
1459            }
1460            // Live routes are protected. Growth is admitted transactionally;
1461            // external registry/config changes cannot justify erasing routes.
1462            break;
1463        }
1464    }
1465
1466    fn push_rx(&mut self, item: RelayRxItem) -> TelemetryResult<()> {
1467        self.make_shared_queue_room(item.byte_cost(), item.priority, Some(&item.data))?;
1468        #[cfg(any(feature = "std", feature = "fair-admission"))]
1469        {
1470            #[cfg(any(feature = "std", feature = "fair-admission"))]
1471            let mut fair_victim = None;
1472            self.rx_queue.push_back_prioritized_select(
1473                item,
1474                |queued| queued.priority,
1475                |queue, _incoming| {
1476                    #[cfg(any(feature = "std", feature = "fair-admission"))]
1477                    {
1478                        crate::admission_fairness::queue_victim(
1479                            queue,
1480                            _incoming,
1481                            |item| Self::fair_bulk_owner(&item.data, item.priority),
1482                            |item| item.priority,
1483                            |item| Self::bulk_stream(&item.data),
1484                            &mut fair_victim,
1485                        )
1486                    }
1487                },
1488            )
1489        }
1490        #[cfg(not(any(feature = "std", feature = "fair-admission")))]
1491        {
1492            self.rx_queue
1493                .push_back_prioritized(item, |queued| queued.priority)
1494        }
1495    }
1496
1497    fn push_tx(&mut self, item: RelayTxItem) -> TelemetryResult<()> {
1498        #[cfg(feature = "discovery")]
1499        {
1500            // Preserve the transfer identity of an unchanged queued catalog.
1501            if let RelayItem::Packet(incoming) = &item.data
1502                && self.tx_queue.iter().any(|queued| {
1503                    queued.dst == item.dst && matches!(&queued.data,
1504                        RelayItem::Packet(existing) if discovery::same_schema_snapshot(existing, incoming))
1505                })
1506            {
1507                return Ok(());
1508            }
1509            let incoming = match &item.data {
1510                RelayItem::Packet(pkt) if discovery::is_discovery_type(pkt.data_type()) => {
1511                    Some((pkt.data_type(), pkt.sender().to_owned()))
1512                }
1513                RelayItem::Packet(_) | RelayItem::Packed(_) => None,
1514            };
1515            if let Some((incoming_ty, incoming_sender)) = incoming {
1516                self.tx_queue.retain(|queued| {
1517                    let same_snapshot = match &queued.data {
1518                        RelayItem::Packet(pkt) => {
1519                            pkt.data_type() == incoming_ty && pkt.sender() == incoming_sender
1520                        }
1521                        RelayItem::Packed(_) => false,
1522                    };
1523                    queued.dst != item.dst || !same_snapshot
1524                });
1525            }
1526        }
1527        self.make_shared_queue_room(item.byte_cost(), item.priority, Some(&item.data))?;
1528        #[cfg(any(feature = "std", feature = "fair-admission"))]
1529        {
1530            #[cfg(any(feature = "std", feature = "fair-admission"))]
1531            let mut fair_victim = None;
1532            self.tx_queue.push_back_prioritized_select(
1533                item,
1534                |queued| queued.priority,
1535                |queue, _incoming| {
1536                    #[cfg(any(feature = "std", feature = "fair-admission"))]
1537                    {
1538                        crate::admission_fairness::queue_victim(
1539                            queue,
1540                            _incoming,
1541                            |item| Self::fair_bulk_owner(&item.data, item.priority),
1542                            |item| item.priority,
1543                            |item| Self::bulk_stream(&item.data),
1544                            &mut fair_victim,
1545                        )
1546                    }
1547                },
1548            )
1549        }
1550        #[cfg(not(any(feature = "std", feature = "fair-admission")))]
1551        {
1552            self.tx_queue
1553                .push_back_prioritized(item, |queued| queued.priority)
1554        }
1555    }
1556
1557    fn push_replay(&mut self, item: RelayReplayItem) -> TelemetryResult<()> {
1558        self.make_shared_queue_room(item.byte_cost(), item.priority, None)?;
1559        self.replay_queue
1560            .push_back_prioritized(item, |queued| queued.priority)
1561    }
1562
1563    fn push_recent_rx(&mut self, id: u64) -> TelemetryResult<()> {
1564        while self.recent_rx.len() >= self.memory.max_recent_rx_ids {
1565            let _ = self.recent_rx.pop_front();
1566        }
1567        self.make_shared_queue_room(0, 0, None)?;
1568        self.recent_rx.push_back(id)
1569    }
1570
1571    #[allow(unused_mut)]
1572    fn buffer_reliable_rx(
1573        &mut self,
1574        side: RelaySideId,
1575        ty: crate::DataType,
1576        seq: u32,
1577        mut bytes: crate::SharedBytes,
1578    ) -> TelemetryResult<()> {
1579        let key = Relay::reliable_key(side, ty);
1580        if self
1581            .reliable_rx
1582            .get(&key)
1583            .is_some_and(|state| state.buffered.contains_key(&seq))
1584        {
1585            return Ok(());
1586        }
1587        let cost = size_of::<crate::SharedBytes>() + bytes.len();
1588        self.make_shared_queue_room(cost, crate::transport_priority(ty), None)?;
1589        #[cfg(feature = "compact-packet-store")]
1590        bytes.park_for_queue()?;
1591        let rx_state = self
1592            .reliable_rx
1593            .entry(key)
1594            .or_insert_with(|| ReliableRxState {
1595                expected_seq: 1,
1596                buffered: BTreeMap::new(),
1597            });
1598        if rx_state.buffered.len() >= runtime_reliable_max_pending() {
1599            let _ = rx_state.buffered.pop_first();
1600        }
1601        rx_state.buffered.insert(seq, bytes);
1602        Ok(())
1603    }
1604}
1605
1606/// Relay that fans out packets from one side to all others.
1607/// - Supports both packed bytes and full Packet.
1608/// - Has RX & TX queues, like Router.
1609/// - Uses a Clock for the *_with_timeout APIs, same style as Router.
1610pub struct Relay {
1611    sender: RouterMutex<Arc<str>>,
1612    state: RouterMutex<RelayInner>,
1613    side_tx_gate: ReentryGate,
1614    clock: Box<dyn Clock + Send + Sync>,
1615}
1616
1617enum RemoteSidePlan {
1618    Target(Vec<RelaySideId>),
1619}
1620
1621impl Relay {
1622    const END_TO_END_ACK_SENDER: &'static str = "E2EACK";
1623    const END_TO_END_ACK_PREFIX: &'static str = "E2EACK:";
1624
1625    fn item_type_priority(data: &RelayItem) -> TelemetryResult<(crate::DataType, u8)> {
1626        let (ty, control) = match data {
1627            RelayItem::Packet(pkt) => (
1628                pkt.data_type(),
1629                pkt.payload().len() <= 16
1630                    && pkt.endpoints().len() <= 3
1631                    && crate::small_frozen_control(
1632                        pkt.data_type(),
1633                        pkt.wire_target_senders().len(),
1634                        pkt.wire_shape(),
1635                    ),
1636            ),
1637            RelayItem::Packed(bytes) => {
1638                let env = wire_format::peek_routing_envelope(bytes.as_ref())?;
1639                let control = bytes.len() <= 128
1640                    && bytes.first().is_some_and(|flags| flags & 0x11 == 0)
1641                    && env.endpoints().take(4).count() <= 3
1642                    && crate::small_frozen_control(
1643                        env.ty,
1644                        env.target_senders().len(),
1645                        env.wire_shape,
1646                    );
1647                (env.ty, control)
1648            }
1649        };
1650        let priority = crate::scheduler_priority(ty);
1651        Ok((ty, if control { priority.max(254) } else { priority }))
1652    }
1653
1654    fn relay_item_priority(data: &RelayItem) -> TelemetryResult<u8> {
1655        Self::item_type_priority(data).map(|(_, priority)| priority)
1656    }
1657
1658    #[inline]
1659    fn is_transient_tx_error(err: &TelemetryError) -> bool {
1660        matches!(err, TelemetryError::Io("side tx busy" | "memory pressure"))
1661    }
1662
1663    fn process_replay_queue_item(&self) -> TelemetryResult<bool> {
1664        let Some(mut item) = ({
1665            let mut st = self.state.lock();
1666            let ready = st
1667                .replay_queue
1668                .iter()
1669                .position(|item| item.retry_after_ms <= self.clock.now_ms());
1670            ready.and_then(|index| st.replay_queue.remove_pos(index))
1671        }) else {
1672            return Ok(false);
1673        };
1674        let frame = wire_format::peek_frame_info(item.bytes.as_ref())?;
1675        let ty = frame.envelope.ty;
1676        let Some(hdr) = frame.reliable else {
1677            return Ok(false);
1678        };
1679        {
1680            let mut st = self.state.lock();
1681            let tx_state = self.reliable_tx_state_mut(&mut st, item.dst, ty);
1682            if !tx_state.sent.contains_key(&hdr.seq) {
1683                return Ok(false);
1684            }
1685        }
1686        if let Err(e) = self.send_reliable_raw_to_side(item.dst, item.bytes.clone()) {
1687            if Self::is_transient_tx_error(&e) {
1688                let mut st = self.state.lock();
1689                item.retry_after_ms = self.clock.now_ms().saturating_add(1);
1690                st.push_replay(item)?;
1691                return Ok(true);
1692            }
1693            return Err(e);
1694        }
1695        let mut st = self.state.lock();
1696        let tx_state = self.reliable_tx_state_mut(&mut st, item.dst, ty);
1697        if let Some(sent) = tx_state.sent.get_mut(&hdr.seq) {
1698            sent.last_send_ms = self.clock.now_ms();
1699            sent.queued = false;
1700        }
1701        Ok(true)
1702    }
1703
1704    fn pop_ready_tx_item(
1705        &self,
1706    ) -> Option<(
1707        Option<RelaySideId>,
1708        RelaySideId,
1709        RelayTxHandlerFn,
1710        RelaySideOptions,
1711        RelayItem,
1712        u8,
1713    )> {
1714        let mut st = self.state.lock();
1715        let mut priority_burst = st.tx_priority_burst;
1716        let item = st.tx_queue.pop_front_fair_if(
1717            &mut priority_burst,
1718            MAX_PRIORITY_BURST,
1719            |queued| queued.priority,
1720            |queued| queued.retry_after_ms <= self.clock.now_ms(),
1721        );
1722        st.tx_priority_burst = priority_burst;
1723        if let Some(item) = item {
1724            let side = st.sides.get(item.dst).and_then(|side| side.clone());
1725            side.map(|s| {
1726                (
1727                    item.src,
1728                    item.dst,
1729                    s.tx_handler,
1730                    s.opts,
1731                    item.data,
1732                    item.priority,
1733                )
1734            })
1735        } else {
1736            None
1737        }
1738    }
1739
1740    fn send_tx_item(
1741        &self,
1742        src: Option<RelaySideId>,
1743        dst: RelaySideId,
1744        handler: RelayTxHandlerFn,
1745        opts: RelaySideOptions,
1746        data: RelayItem,
1747    ) -> TelemetryResult<bool> {
1748        let allowed = {
1749            let mut st = self.state.lock();
1750            let ty = match &data {
1751                RelayItem::Packet(pkt) => Some(pkt.data_type()),
1752                RelayItem::Packed(bytes) => {
1753                    Some(wire_format::peek_routing_envelope(bytes.as_ref())?.ty)
1754                }
1755            };
1756            let route_allowed = self.route_allowed_locked(&st, src, ty, dst);
1757            #[cfg(all(feature = "discovery", feature = "timesync"))]
1758            let timesync_allowed = ty
1759                .map(|ty| {
1760                    Self::timesync_allowed_for_side_locked(&mut st, dst, ty, self.clock.now_ms())
1761                })
1762                .unwrap_or(true);
1763            #[cfg(not(all(feature = "discovery", feature = "timesync")))]
1764            let timesync_allowed = true;
1765            route_allowed && timesync_allowed
1766        };
1767        if !allowed {
1768            return Ok(false);
1769        }
1770        if opts.reliable_enabled && matches!(handler, RelayTxHandlerFn::Packed(_)) {
1771            self.send_reliable_to_side(dst, data)?;
1772            Ok(true)
1773        } else if let Some(adjusted) = self.adjust_reliable_for_side(opts, data)? {
1774            self.call_tx_handler(dst, &handler, &adjusted)?;
1775            Ok(true)
1776        } else {
1777            Ok(false)
1778        }
1779    }
1780
1781    /// Create a new relay with the given clock.
1782    pub fn new(clock: Box<dyn Clock + Send + Sync>) -> Self {
1783        Self::new_with_config(RelayConfig::default(), clock)
1784    }
1785
1786    /// Create a new relay with explicit runtime configuration and clock.
1787    pub fn new_with_config(cfg: RelayConfig, clock: Box<dyn Clock + Send + Sync>) -> Self {
1788        let memory = cfg.memory_config();
1789        Self {
1790            sender: RouterMutex::new(cfg.sender()),
1791            state: RouterMutex::new(RelayInner {
1792                memory,
1793                sides: Vec::new(),
1794                route_overrides: BTreeMap::new(),
1795                typed_route_overrides: BTreeMap::new(),
1796                route_weights: BTreeMap::new(),
1797                route_priorities: BTreeMap::new(),
1798                source_route_modes: BTreeMap::new(),
1799                route_selection_cursors: BTreeMap::new(),
1800                adaptive_route_stats: BTreeMap::new(),
1801                side_runtime_stats: BTreeMap::new(),
1802                side_transport: BTreeMap::new(),
1803                rx_queue: BoundedDeque::new(
1804                    memory.max_queue_budget,
1805                    memory.starting_queue_size,
1806                    memory.queue_grow_step,
1807                ),
1808                tx_queue: BoundedDeque::new(
1809                    memory.max_queue_budget,
1810                    memory.starting_queue_size,
1811                    memory.queue_grow_step,
1812                ),
1813                tx_priority_burst: 0,
1814                replay_queue: BoundedDeque::new(
1815                    memory.max_queue_budget,
1816                    memory.starting_queue_size,
1817                    memory.queue_grow_step,
1818                ),
1819                recent_rx: BoundedDeque::new(
1820                    memory.recent_rx_queue_bytes(),
1821                    memory.recent_rx_queue_bytes(),
1822                    memory.queue_grow_step,
1823                ),
1824                reliable_tx: BTreeMap::new(),
1825                reliable_rx: BTreeMap::new(),
1826                reliable_return_routes: BTreeMap::new(),
1827                reliable_return_route_order: VecDeque::new(),
1828                end_to_end_acked_destinations: BTreeMap::new(),
1829                end_to_end_acked_destination_order: VecDeque::new(),
1830                total_handler_failures: 0,
1831                total_handler_retries: 0,
1832                #[cfg(feature = "discovery")]
1833                discovery_routes: BTreeMap::new(),
1834                #[cfg(feature = "discovery")]
1835                discovery_cadence: DiscoveryCadenceState::default(),
1836                #[cfg(feature = "discovery")]
1837                discovery_side_throttle: BTreeMap::new(),
1838                #[cfg(all(feature = "discovery", feature = "timesync"))]
1839                timesync_side_throttle: BTreeMap::new(),
1840            }),
1841            side_tx_gate: ReentryGate::new(),
1842            clock,
1843        }
1844    }
1845
1846    #[inline]
1847    fn sender_arc(&self) -> Arc<str> {
1848        self.sender.lock().clone()
1849    }
1850
1851    #[inline]
1852    pub fn sender(&self) -> Arc<str> {
1853        self.sender_arc()
1854    }
1855
1856    pub fn set_sender<S: AsRef<str>>(&self, sender: S) {
1857        *self.sender.lock() = Arc::from(sender.as_ref());
1858    }
1859
1860    #[inline]
1861    fn try_enter_side_tx(&self) -> Option<ReentryGuard<'_>> {
1862        self.side_tx_gate.try_enter()
1863    }
1864
1865    #[inline]
1866    fn side_tx_active(&self) -> bool {
1867        self.side_tx_gate.is_active()
1868    }
1869
1870    #[inline]
1871    fn side_ref(st: &RelayInner, side: RelaySideId) -> TelemetryResult<&RelaySide> {
1872        st.sides
1873            .get(side)
1874            .and_then(|side| side.as_ref())
1875            .ok_or(TelemetryError::HandlerError("relay: invalid side id"))
1876    }
1877
1878    fn note_side_tx_success(
1879        &self,
1880        side: RelaySideId,
1881        ty: crate::DataType,
1882        bytes: usize,
1883        attempts: usize,
1884    ) {
1885        let mut st = self.state.lock();
1886        #[cfg(feature = "discovery")]
1887        if ty == crate::DataType::DiscoveryAnnounce
1888            && let Some(throttle) = st.discovery_side_throttle.get_mut(&side)
1889        {
1890            throttle.last_liveness_tx_ms = self.clock.now_ms();
1891        }
1892        let entry = st.side_runtime_stats.entry(side).or_default();
1893        entry.note_tx(ty, bytes, attempts.saturating_sub(1));
1894    }
1895
1896    fn note_side_tx_failure(&self, side: RelaySideId, ty: crate::DataType, attempts: usize) {
1897        let mut st = self.state.lock();
1898        st.total_handler_failures = st.total_handler_failures.saturating_add(1);
1899        st.total_handler_retries = st.total_handler_retries.saturating_add(attempts as u64);
1900        let entry = st.side_runtime_stats.entry(side).or_default();
1901        entry.note_tx_failure(ty, attempts);
1902    }
1903
1904    fn note_side_rx(&self, side: RelaySideId, ty: crate::DataType, bytes: usize) {
1905        let mut st = self.state.lock();
1906        let entry = st.side_runtime_stats.entry(side).or_default();
1907        entry.note_rx(ty, bytes);
1908    }
1909
1910    #[inline]
1911    fn ensure_side_ingress_enabled(&self, side: RelaySideId) -> TelemetryResult<()> {
1912        let st = self.state.lock();
1913        let side_ref = Self::side_ref(&st, side)?;
1914        if side_ref.opts.ingress_enabled {
1915            Ok(())
1916        } else {
1917            Err(TelemetryError::HandlerError(
1918                "relay: ingress disabled for side id",
1919            ))
1920        }
1921    }
1922
1923    #[inline]
1924    fn route_allowed_locked(
1925        &self,
1926        st: &RelayInner,
1927        src: Option<RelaySideId>,
1928        ty: Option<crate::DataType>,
1929        dst: RelaySideId,
1930    ) -> bool {
1931        let Ok(dst_side) = Self::side_ref(st, dst) else {
1932            return false;
1933        };
1934        if !dst_side.opts.egress_enabled {
1935            return false;
1936        }
1937        if let Some(src_id) = src {
1938            let Ok(src_side) = Self::side_ref(st, src_id) else {
1939                return false;
1940            };
1941            if !src_side.opts.ingress_enabled || src_id == dst {
1942                return false;
1943            }
1944        }
1945        let base_allowed = st.route_overrides.get(&(src, dst)).copied().unwrap_or(true);
1946        if !base_allowed {
1947            return false;
1948        }
1949
1950        let Some(ty) = ty else {
1951            return true;
1952        };
1953        if st
1954            .typed_route_overrides
1955            .keys()
1956            .any(|(typed_src, typed_ty, _)| *typed_src == src && *typed_ty == ty.as_u32())
1957        {
1958            return st
1959                .typed_route_overrides
1960                .get(&(src, ty.as_u32(), dst))
1961                .copied()
1962                .unwrap_or(false);
1963        }
1964        true
1965    }
1966
1967    fn has_typed_route_overrides_locked(
1968        st: &RelayInner,
1969        src: Option<RelaySideId>,
1970        ty: crate::DataType,
1971    ) -> bool {
1972        st.typed_route_overrides
1973            .keys()
1974            .any(|(typed_src, typed_ty, _)| *typed_src == src && *typed_ty == ty.as_u32())
1975    }
1976
1977    fn eligible_side_ids_locked(
1978        &self,
1979        st: &RelayInner,
1980        src: Option<RelaySideId>,
1981        ty: Option<crate::DataType>,
1982        restrict_link_local: bool,
1983    ) -> Vec<RelaySideId> {
1984        st.sides
1985            .iter()
1986            .enumerate()
1987            .filter_map(|(side_id, side)| {
1988                let side = side.as_ref()?;
1989                if restrict_link_local && !side.opts.link_local_enabled {
1990                    return None;
1991                }
1992                if self.route_allowed_locked(st, src, ty, side_id) {
1993                    Some(side_id)
1994                } else {
1995                    None
1996                }
1997            })
1998            .collect()
1999    }
2000
2001    fn apply_route_selection_locked(
2002        &self,
2003        st: &mut RelayInner,
2004        src: Option<RelaySideId>,
2005        mut sides: Vec<RelaySideId>,
2006        origin: RouteSelectionOrigin,
2007    ) -> Vec<RelaySideId> {
2008        if sides.len() <= 1 {
2009            return sides;
2010        }
2011
2012        let selection_mode = st.source_route_modes.get(&src).copied();
2013        if selection_mode.is_none() && origin == RouteSelectionOrigin::Discovered {
2014            return self.apply_adaptive_discovery_selection_locked(st, src, sides);
2015        }
2016
2017        match selection_mode.unwrap_or(RouteSelectionMode::Fanout) {
2018            RouteSelectionMode::Fanout => sides,
2019            RouteSelectionMode::Weighted => {
2020                crate::small_sort::sort(&mut sides);
2021                let total_weight = sides.iter().fold(0_u64, |acc, side| {
2022                    acc + u64::from(st.route_weights.get(&(src, *side)).copied().unwrap_or(1))
2023                });
2024                if total_weight == 0 {
2025                    return Vec::new();
2026                }
2027                let cursor = st.route_selection_cursors.entry(src).or_insert(0);
2028                let pick = *cursor % total_weight;
2029                *cursor = cursor.wrapping_add(1);
2030                let mut remaining = pick;
2031                for side in sides {
2032                    let weight =
2033                        u64::from(st.route_weights.get(&(src, side)).copied().unwrap_or(1));
2034                    if remaining < weight {
2035                        return vec![side];
2036                    }
2037                    remaining -= weight;
2038                }
2039                Vec::new()
2040            }
2041            RouteSelectionMode::Failover => {
2042                sides.sort_by_key(|side| {
2043                    (
2044                        st.route_priorities.get(&(src, *side)).copied().unwrap_or(0),
2045                        *side,
2046                    )
2047                });
2048                sides.truncate(1);
2049                sides
2050            }
2051        }
2052    }
2053
2054    fn apply_adaptive_discovery_selection_locked(
2055        &self,
2056        st: &mut RelayInner,
2057        src: Option<RelaySideId>,
2058        mut sides: Vec<RelaySideId>,
2059    ) -> Vec<RelaySideId> {
2060        crate::small_sort::sort(&mut sides);
2061        let mut unmeasured: Vec<_> = sides
2062            .iter()
2063            .copied()
2064            .filter(|side| !st.adaptive_route_stats.contains_key(side))
2065            .collect();
2066        if !unmeasured.is_empty() {
2067            let cursor = st.route_selection_cursors.entry(src).or_insert(0);
2068            let pick = (*cursor as usize) % unmeasured.len();
2069            *cursor = cursor.wrapping_add(1);
2070            return vec![unmeasured.swap_remove(pick)];
2071        }
2072
2073        let now_ms = self.clock.now_ms();
2074        let total_weight = sides.iter().fold(0_u64, |acc, side| {
2075            acc + st
2076                .adaptive_route_stats
2077                .get(side)
2078                .map(|stats| stats.weight(now_ms))
2079                .unwrap_or(1)
2080        });
2081        if total_weight == 0 {
2082            sides.truncate(1);
2083            return sides;
2084        }
2085
2086        let cursor = st.route_selection_cursors.entry(src).or_insert(0);
2087        let pick = *cursor % total_weight;
2088        *cursor = cursor.wrapping_add(1);
2089        let mut remaining = pick;
2090        for side in sides {
2091            let weight = st
2092                .adaptive_route_stats
2093                .get(&side)
2094                .map(|stats| stats.weight(now_ms))
2095                .unwrap_or(1);
2096            if remaining < weight {
2097                return vec![side];
2098            }
2099            remaining -= weight;
2100        }
2101        Vec::new()
2102    }
2103
2104    /// Prefer the discovery side with the shortest advertised topology path
2105    /// to the packet's destination. This prevents a route reflected through
2106    /// another router from competing equally with the direct route.
2107    #[cfg(feature = "discovery")]
2108    fn retain_shortest_discovery_routes_locked(
2109        st: &RelayInner,
2110        sides: &mut Vec<RelaySideId>,
2111        endpoints: &[crate::DataEndpoint],
2112        now_ms: u64,
2113    ) {
2114        if sides.len() <= 1 || endpoints.is_empty() {
2115            return;
2116        }
2117
2118        let route_distance = |side: RelaySideId| -> Option<usize> {
2119            let route = st.discovery_routes.get(&side)?;
2120            route
2121                .announcers
2122                .iter()
2123                .filter(|(_, state)| {
2124                    now_ms.saturating_sub(state.last_seen_ms) <= DISCOVERY_ROUTE_TTL_MS
2125                })
2126                .filter_map(|(announcer, state)| {
2127                    let targets: BTreeSet<&str> = state
2128                        .topology_boards
2129                        .iter()
2130                        .filter(|board| {
2131                            endpoints
2132                                .iter()
2133                                .any(|endpoint| board.reachable_endpoints.contains(endpoint))
2134                        })
2135                        .map(|board| board.sender_id.as_str())
2136                        .collect();
2137                    if targets.is_empty() {
2138                        return state
2139                            .reachable
2140                            .iter()
2141                            .any(|endpoint| endpoints.contains(endpoint))
2142                            .then_some(usize::MAX / 2);
2143                    }
2144
2145                    let mut distances: BTreeMap<&str, usize> = BTreeMap::new();
2146                    let mut pending = VecDeque::from([(announcer.as_str(), 0usize)]);
2147                    while let Some((sender, distance)) = pending.pop_front() {
2148                        if distances.contains_key(sender) {
2149                            continue;
2150                        }
2151                        distances.insert(sender, distance);
2152                        if targets.contains(sender) {
2153                            return Some(distance);
2154                        }
2155                        for board in state.topology_boards.iter() {
2156                            if board.sender_id == sender {
2157                                for peer in &board.connections {
2158                                    if !distances.contains_key(peer.as_str()) {
2159                                        pending.push_back((peer.as_str(), distance + 1));
2160                                    }
2161                                }
2162                            } else if board.connections.iter().any(|peer| peer == sender)
2163                                && !distances.contains_key(board.sender_id.as_str())
2164                            {
2165                                pending.push_back((board.sender_id.as_str(), distance + 1));
2166                            }
2167                        }
2168                    }
2169                    None
2170                })
2171                .min()
2172        };
2173
2174        let distances: Vec<(RelaySideId, usize)> = sides
2175            .iter()
2176            .filter_map(|side| route_distance(*side).map(|distance| (*side, distance)))
2177            .collect();
2178        let Some(best) = distances.iter().map(|(_, distance)| *distance).min() else {
2179            return;
2180        };
2181        sides.retain(|side| {
2182            distances
2183                .iter()
2184                .any(|(candidate, distance)| candidate == side && *distance == best)
2185        });
2186    }
2187
2188    fn record_side_tx_sample(
2189        &self,
2190        side: RelaySideId,
2191        bytes: usize,
2192        started_ms: u64,
2193        ended_ms: u64,
2194    ) {
2195        let sample_ms = ended_ms.saturating_sub(started_ms).max(1);
2196        let sample_bps = ((bytes as u128).saturating_mul(1000) / u128::from(sample_ms))
2197            .min(u128::from(u64::MAX)) as u64;
2198        let mut st = self.state.lock();
2199        st.adaptive_route_stats
2200            .entry(side)
2201            .or_default()
2202            .observe(bytes, sample_bps, ended_ms);
2203    }
2204
2205    /// Seed adaptive route selection with a transport-measured link probe.
2206    ///
2207    /// Call this after a side-specific bring-up probe, or whenever the transport already knows the
2208    /// duration for a frame. The relay does not emit synthetic probe frames by itself.
2209    pub fn note_side_link_probe_sample(
2210        &self,
2211        side: RelaySideId,
2212        bytes: usize,
2213        duration_ms: u64,
2214    ) -> TelemetryResult<()> {
2215        {
2216            let st = self.state.lock();
2217            let _ = Self::side_ref(&st, side).map_err(|_| TelemetryError::BadArg)?;
2218        }
2219        let ended_ms = self.clock.now_ms();
2220        self.record_side_tx_sample(side, bytes, ended_ms.saturating_sub(duration_ms), ended_ms);
2221        Ok(())
2222    }
2223
2224    fn relay_item_wire_len(data: &RelayItem) -> TelemetryResult<usize> {
2225        match data {
2226            RelayItem::Packet(pkt) => Ok(wire_format::packet_wire_size(pkt)),
2227            RelayItem::Packed(bytes) => Ok(bytes.len()),
2228        }
2229    }
2230
2231    #[inline]
2232    fn decode_end_to_end_reliable_ack(payload: &[u8]) -> TelemetryResult<u64> {
2233        if payload.len() != 8 {
2234            return Err(TelemetryError::Unpack("bad reliable e2e ack payload"));
2235        }
2236        Ok(u64::from_le_bytes(payload[0..8].try_into().unwrap()))
2237    }
2238
2239    #[inline]
2240    fn is_end_to_end_ack_sender(sender: &str) -> bool {
2241        sender == Self::END_TO_END_ACK_SENDER || sender.starts_with(Self::END_TO_END_ACK_PREFIX)
2242    }
2243
2244    #[inline]
2245    fn sender_hash(sender: &str) -> u64 {
2246        if let Some(address) = sender
2247            .strip_prefix("@addr:")
2248            .and_then(|value| value.parse::<u32>().ok())
2249        {
2250            return u64::from(address);
2251        }
2252        hash_bytes_u64(0x517C_C1B7_2722_0A95, sender.as_bytes())
2253    }
2254
2255    #[cfg(feature = "discovery")]
2256    fn canonical_sender_locked(st: &RelayInner, sender: &str) -> String {
2257        let Some(address) = sender
2258            .strip_prefix("@addr:")
2259            .and_then(|value| value.parse::<u32>().ok())
2260        else {
2261            return sender.to_string();
2262        };
2263        Self::hostname_for_wire_address_locked(st, address).unwrap_or_else(|| sender.to_string())
2264    }
2265
2266    #[cfg(feature = "discovery")]
2267    fn hostname_for_wire_address_locked(st: &RelayInner, address: u32) -> Option<String> {
2268        Self::hostname_for_wire_address_borrowed_locked(st, address).map(str::to_string)
2269    }
2270
2271    #[cfg(feature = "discovery")]
2272    fn hostname_for_wire_address_borrowed_locked<'a>(
2273        st: &'a RelayInner,
2274        address: u32,
2275    ) -> Option<&'a str> {
2276        let mut found: Option<&'a str> = None;
2277        let mut collision = false;
2278        let mut consider = |candidate: &'a str| {
2279            if sender_address_u32(candidate) != address {
2280                return;
2281            }
2282            match found {
2283                None => found = Some(candidate),
2284                Some(existing) if existing == candidate => {}
2285                Some(_) => collision = true,
2286            }
2287        };
2288        for route in st.discovery_routes.values() {
2289            for (announcer, state) in &route.announcers {
2290                consider(announcer);
2291                for board in state.topology_boards.iter() {
2292                    consider(&board.sender_id);
2293                }
2294            }
2295        }
2296        if collision { None } else { found }
2297    }
2298
2299    fn decode_end_to_end_ack_sender_hash(sender: &str) -> Option<u64> {
2300        sender
2301            .strip_prefix(Self::END_TO_END_ACK_PREFIX)
2302            .filter(|sender| !sender.is_empty())
2303            .map(Self::sender_hash)
2304    }
2305
2306    fn end_to_end_ack_sender_hash(pkt: &Packet) -> Option<u64> {
2307        if pkt.data_type() != crate::DataType::ReliableAck {
2308            return None;
2309        }
2310        pkt.wire_target_senders()
2311            .first()
2312            .copied()
2313            .or_else(|| Self::decode_end_to_end_ack_sender_hash(pkt.sender()))
2314    }
2315
2316    #[cfg(feature = "discovery")]
2317    fn is_end_to_end_destination_sender(&self, sender: &str) -> bool {
2318        sender != self.sender_arc().as_ref() && !Self::is_end_to_end_ack_sender(sender)
2319    }
2320
2321    /// Extract the logical packet ID targeted by an end-to-end reliable ACK item.
2322    ///
2323    /// Relay queues can hold either decoded packets or packed frames. This
2324    /// helper normalizes both forms so relay ACK-routing logic can treat them
2325    /// uniformly.
2326    ///
2327    /// Only relay-visible end-to-end `ReliableAck` packets qualify here.
2328    /// Unrelated traffic returns `Ok(None)`.
2329    fn reliable_control_target_packet_id(data: &RelayItem) -> TelemetryResult<Option<u64>> {
2330        match data {
2331            RelayItem::Packet(pkt) => {
2332                if Self::end_to_end_ack_sender_hash(pkt).is_none() {
2333                    return Ok(None);
2334                }
2335                Self::decode_end_to_end_reliable_ack(pkt.payload()).map(Some)
2336            }
2337            RelayItem::Packed(bytes) => {
2338                if wire_format::peek_routing_frame_info(bytes.as_ref())
2339                    .ok()
2340                    .is_some_and(|frame| {
2341                        frame.envelope.ty != crate::DataType::ReliableAck || frame.ack_only()
2342                    })
2343                {
2344                    return Ok(None);
2345                }
2346                let pkt = wire_format::unpack_packet(bytes.as_ref())?;
2347                if Self::end_to_end_ack_sender_hash(&pkt).is_none() {
2348                    return Ok(None);
2349                }
2350                Self::decode_end_to_end_reliable_ack(pkt.payload()).map(Some)
2351            }
2352        }
2353    }
2354
2355    fn note_reliable_return_route(&self, side: RelaySideId, packet_id: u64) {
2356        let mut st = self.state.lock();
2357        // A retry can remain in RX dedupe after its smaller return-path cache
2358        // entry was evicted. Relearning that path must not restart the replay
2359        // delay on every retry, or sustained cache churn suppresses it forever.
2360        let now = self.clock.now_ms();
2361        let retry_ms = runtime_reliable_retransmit_ms();
2362        let duplicate = st.recent_rx.contains(&packet_id);
2363        let last_replay_ms = if duplicate {
2364            now.saturating_sub(retry_ms)
2365        } else {
2366            now
2367        };
2368        // A reflected copy is not evidence that the original publisher
2369        // moved. Keep the first live ingress during the current retry
2370        // window, otherwise an echo can redirect an already queued ACK into
2371        // the downstream bus. A retry-window expiry or disabled/removed side
2372        // still permits alternate-path recovery without explicit operator action.
2373        let replace_side = !duplicate
2374            || st
2375                .reliable_return_routes
2376                .get(&packet_id)
2377                .is_none_or(|route| {
2378                    now.saturating_sub(route.last_replay_ms) >= retry_ms
2379                        || !Self::side_ref(&st, route.side)
2380                            .is_ok_and(|side| side.opts.egress_enabled)
2381                });
2382        Self::remember_reliable_return_route_locked(&mut st, packet_id);
2383        st.reliable_return_routes
2384            .entry(packet_id)
2385            .and_modify(|route| {
2386                if replace_side {
2387                    route.side = side;
2388                }
2389            })
2390            .or_insert(ReliableReturnRouteState {
2391                side,
2392                last_replay_ms,
2393            });
2394    }
2395
2396    /// Refresh or insert `packet_id` in the bounded reliable return-route cache.
2397    ///
2398    /// The relay uses this cache to route end-to-end acknowledgements back
2399    /// toward the original live ingress of the corresponding
2400    /// reliable data packet.
2401    fn remember_reliable_return_route_locked(st: &mut RelayInner, packet_id: u64) {
2402        let cap = runtime_reliable_max_return_routes().max(1);
2403        st.reliable_return_route_order
2404            .retain(|id| st.reliable_return_routes.contains_key(id) && *id != packet_id);
2405        while st.reliable_return_route_order.len() >= cap {
2406            if let Some(oldest) = st.reliable_return_route_order.pop_front() {
2407                st.reliable_return_routes.remove(&oldest);
2408            } else {
2409                break;
2410            }
2411        }
2412        st.reliable_return_route_order.push_back(packet_id);
2413    }
2414
2415    fn note_end_to_end_acked_destination_locked(
2416        st: &mut RelayInner,
2417        packet_id: u64,
2418        sender_hash: u64,
2419    ) {
2420        let entry_cap = runtime_reliable_max_end_to_end_ack_cache().max(1);
2421        st.end_to_end_acked_destination_order
2422            .retain(|id| st.end_to_end_acked_destinations.contains_key(id) && *id != packet_id);
2423        while st.end_to_end_acked_destination_order.len() >= entry_cap {
2424            if let Some(oldest) = st.end_to_end_acked_destination_order.pop_front() {
2425                st.end_to_end_acked_destinations.remove(&oldest);
2426            } else {
2427                break;
2428            }
2429        }
2430        st.end_to_end_acked_destination_order.push_back(packet_id);
2431
2432        let acked = st
2433            .end_to_end_acked_destinations
2434            .entry(packet_id)
2435            .or_default();
2436        let sender_cap = runtime_reliable_max_end_to_end_pending().max(1);
2437        if acked.len() < sender_cap || acked.contains(&sender_hash) {
2438            acked.insert(sender_hash);
2439        }
2440    }
2441
2442    #[inline]
2443    fn reliable_key(side: RelaySideId, ty: crate::DataType) -> (RelaySideId, u32) {
2444        (side, ty.as_u32())
2445    }
2446
2447    fn reliable_tx_state_mut<'a>(
2448        &'a self,
2449        st: &'a mut RelayInner,
2450        side: RelaySideId,
2451        ty: crate::DataType,
2452    ) -> &'a mut ReliableTxState {
2453        let key = Self::reliable_key(side, ty);
2454        st.reliable_tx
2455            .entry(key)
2456            .or_insert_with(|| ReliableTxState {
2457                next_seq: 1,
2458                sent_order: VecDeque::new(),
2459                sent: BTreeMap::new(),
2460            })
2461    }
2462
2463    fn reliable_rx_state_mut<'a>(
2464        &'a self,
2465        st: &'a mut RelayInner,
2466        side: RelaySideId,
2467        ty: crate::DataType,
2468    ) -> &'a mut ReliableRxState {
2469        let key = Self::reliable_key(side, ty);
2470        st.reliable_rx
2471            .entry(key)
2472            .or_insert_with(|| ReliableRxState {
2473                expected_seq: 1,
2474                buffered: BTreeMap::new(),
2475            })
2476    }
2477
2478    fn handle_reliable_ack(&self, side: RelaySideId, ty: crate::DataType, ack: u32) {
2479        let mut st = self.state.lock();
2480        let tx_state = self.reliable_tx_state_mut(&mut st, side, ty);
2481        if matches!(reliable_mode(ty), crate::ReliableMode::Unordered) {
2482            tx_state.sent.remove(&ack);
2483            tx_state.sent_order.retain(|seq| *seq != ack);
2484            return;
2485        }
2486
2487        while let Some(seq) = tx_state.sent_order.front().copied() {
2488            if seq > ack {
2489                break;
2490            }
2491            tx_state.sent_order.pop_front();
2492            tx_state.sent.remove(&seq);
2493        }
2494    }
2495
2496    fn handle_reliable_partial_ack(&self, side: RelaySideId, ty: crate::DataType, seq: u32) {
2497        let mut st = self.state.lock();
2498        let tx_state = self.reliable_tx_state_mut(&mut st, side, ty);
2499        if let Some(sent) = tx_state.sent.get_mut(&seq) {
2500            sent.partial_acked = true;
2501        }
2502    }
2503
2504    fn reliable_control_packet(
2505        &self,
2506        control_ty: crate::DataType,
2507        ty: crate::DataType,
2508        seq: u32,
2509    ) -> TelemetryResult<Packet> {
2510        let sender = self.sender_arc();
2511        Packet::new(
2512            control_ty,
2513            message_meta(control_ty).endpoints_ref(),
2514            sender.as_ref(),
2515            self.clock.now_ms(),
2516            crate::router::encode_slice_le(&[ty.as_u32(), seq]),
2517        )
2518    }
2519
2520    fn queue_reliable_ack(
2521        &self,
2522        side: RelaySideId,
2523        ty: crate::DataType,
2524        seq: u32,
2525    ) -> TelemetryResult<()> {
2526        let pkt = self.reliable_control_packet(crate::DataType::ReliableAck, ty, seq)?;
2527        let data = RelayItem::Packet(Arc::new(pkt));
2528        let priority = Self::relay_item_priority(&data)?;
2529        let mut st = self.state.lock();
2530        st.push_tx(RelayTxItem {
2531            src: None,
2532            dst: side,
2533            data,
2534            priority,
2535            retry_after_ms: 0,
2536        })?;
2537        Ok(())
2538    }
2539
2540    fn queue_reliable_packet_request(
2541        &self,
2542        side: RelaySideId,
2543        ty: crate::DataType,
2544        seq: u32,
2545    ) -> TelemetryResult<()> {
2546        let pkt = self.reliable_control_packet(crate::DataType::ReliablePacketRequest, ty, seq)?;
2547        let data = RelayItem::Packet(Arc::new(pkt));
2548        let priority = Self::relay_item_priority(&data)?;
2549        let mut st = self.state.lock();
2550        st.push_tx(RelayTxItem {
2551            src: None,
2552            dst: side,
2553            data,
2554            priority,
2555            retry_after_ms: 0,
2556        })?;
2557        Ok(())
2558    }
2559
2560    fn queue_reliable_partial_ack(
2561        &self,
2562        side: RelaySideId,
2563        ty: crate::DataType,
2564        seq: u32,
2565    ) -> TelemetryResult<()> {
2566        let pkt = self.reliable_control_packet(crate::DataType::ReliablePartialAck, ty, seq)?;
2567        let data = RelayItem::Packet(Arc::new(pkt));
2568        let priority = Self::relay_item_priority(&data)?;
2569        let mut st = self.state.lock();
2570        st.push_tx(RelayTxItem {
2571            src: None,
2572            dst: side,
2573            data,
2574            priority,
2575            retry_after_ms: 0,
2576        })?;
2577        Ok(())
2578    }
2579
2580    fn queue_reliable_retransmit(
2581        &self,
2582        side: RelaySideId,
2583        ty: crate::DataType,
2584        seq: u32,
2585    ) -> TelemetryResult<()> {
2586        let mut queued = None;
2587        {
2588            let mut st = self.state.lock();
2589            let tx_state = self.reliable_tx_state_mut(&mut st, side, ty);
2590            if let Some(sent) = tx_state.sent.get_mut(&seq)
2591                && !sent.queued
2592            {
2593                sent.queued = true;
2594                sent.partial_acked = false;
2595                queued = Some(sent.bytes.clone());
2596            }
2597        }
2598
2599        if let Some(bytes) = queued {
2600            let mut st = self.state.lock();
2601            st.push_replay(RelayReplayItem {
2602                dst: side,
2603                bytes,
2604                priority: message_priority(ty).saturating_add(16),
2605                retry_after_ms: 0,
2606            })?;
2607        }
2608        Ok(())
2609    }
2610
2611    fn send_reliable_raw_to_side(
2612        &self,
2613        side: RelaySideId,
2614        bytes: crate::SharedBytes,
2615    ) -> TelemetryResult<()> {
2616        let (handler, opts) = {
2617            let st = self.state.lock();
2618            let side_ref = Self::side_ref(&st, side)?;
2619            if !side_ref.opts.egress_enabled {
2620                return Ok(());
2621            }
2622            (side_ref.tx_handler.clone(), side_ref.opts.clone())
2623        };
2624
2625        let Some(_side_tx_guard) = self.try_enter_side_tx() else {
2626            return Err(TelemetryError::Io("side tx busy"));
2627        };
2628        let started_ms = self.clock.now_ms();
2629        let ty = wire_format::peek_routing_envelope(bytes.as_ref())
2630            .map(|env| env.ty)
2631            .unwrap_or(crate::DataType::ReliableAck);
2632        let result = match handler {
2633            RelayTxHandlerFn::Packed(f) => {
2634                let frames = self.encode_side_transport_frames(side, opts, bytes.clone())?;
2635                let mut sent_bytes = 0usize;
2636                self.send_side_frames_resumable(side, frames, |frame| {
2637                    f(frame)?;
2638                    sent_bytes = sent_bytes.saturating_add(frame.len());
2639
2640                    Ok(())
2641                })?;
2642                self.record_side_tx_sample(side, sent_bytes, started_ms, self.clock.now_ms());
2643                self.note_side_tx_success(side, ty, sent_bytes, 1);
2644                return Ok(());
2645            }
2646            RelayTxHandlerFn::Packet(f) => {
2647                if wire_format::peek_frame_info(bytes.as_ref())
2648                    .ok()
2649                    .is_some_and(|frame| frame.ack_only())
2650                {
2651                    return Ok(());
2652                }
2653                let pkt = wire_format::unpack_packet(bytes.as_ref())?;
2654                f(&pkt)
2655            }
2656        };
2657        if result.is_ok() {
2658            self.record_side_tx_sample(side, bytes.len(), started_ms, self.clock.now_ms());
2659            self.note_side_tx_success(side, ty, bytes.len(), 1);
2660        } else {
2661            self.note_side_tx_failure(side, ty, 1);
2662        }
2663        result
2664    }
2665
2666    #[allow(unused_mut)]
2667    fn send_reliable_to_side(&self, side: RelaySideId, data: RelayItem) -> TelemetryResult<()> {
2668        let ty = match &data {
2669            RelayItem::Packet(pkt) => pkt.data_type(),
2670            RelayItem::Packed(bytes) => wire_format::peek_routing_envelope(bytes)?.ty,
2671        };
2672        let end_to_end_ordered = !self.item_target_senders(&data)?.is_empty()
2673            && matches!(reliable_mode(ty), crate::ReliableMode::Ordered);
2674        let (handler, opts, hop_reliable_enabled) = {
2675            let st = self.state.lock();
2676            let side_ref = Self::side_ref(&st, side)?;
2677            let opts = side_ref.opts.clone();
2678            let hop_reliable_enabled = opts.reliable_enabled
2679                && !end_to_end_ordered
2680                && !self.side_has_multiple_announcers_locked(&st, side, self.clock.now_ms());
2681            (side_ref.tx_handler.clone(), opts, hop_reliable_enabled)
2682        };
2683
2684        let RelayTxHandlerFn::Packed(f) = &handler else {
2685            return self.call_tx_handler(side, &handler, &data);
2686        };
2687
2688        if !hop_reliable_enabled {
2689            let mut adjusted_opts = opts;
2690            adjusted_opts.reliable_enabled = false;
2691            if let Some(adjusted) = self.adjust_reliable_for_side(adjusted_opts, data)? {
2692                return self.call_tx_handler(side, &handler, &adjusted);
2693            }
2694            return Ok(());
2695        }
2696
2697        let ty = match &data {
2698            RelayItem::Packet(pkt) => pkt.data_type(),
2699            RelayItem::Packed(bytes) => {
2700                let Ok(frame) = wire_format::peek_frame_info(bytes.as_ref()) else {
2701                    return self.call_tx_handler(side, &handler, &data);
2702                };
2703                frame.envelope.ty
2704            }
2705        };
2706
2707        if !is_reliable_type(ty) {
2708            if let Some(adjusted) = self.adjust_reliable_for_side(opts, data)? {
2709                self.call_tx_handler(side, &handler, &adjusted)?;
2710            }
2711            return Ok(());
2712        }
2713
2714        let (seq, flags) = {
2715            let mut st = self.state.lock();
2716            let tx_state = self.reliable_tx_state_mut(&mut st, side, ty);
2717            if tx_state.sent.len() >= runtime_reliable_max_pending() {
2718                return Err(TelemetryError::PacketTooLarge(
2719                    "relay reliable history full",
2720                ));
2721            }
2722            let seq = tx_state.next_seq;
2723            let next = tx_state.next_seq.wrapping_add(1);
2724            tx_state.next_seq = if next == 0 { 1 } else { next };
2725            let flags = match reliable_mode(ty) {
2726                crate::ReliableMode::Unordered => wire_format::RELIABLE_FLAG_UNORDERED,
2727                _ => 0,
2728            };
2729            (seq, flags)
2730        };
2731
2732        let mut bytes: crate::SharedBytes = match data {
2733            RelayItem::Packet(pkt) => {
2734                crate::shared_bytes::convert(wire_format::pack_packet_with_reliable(
2735                    &pkt,
2736                    wire_format::ReliableHeader { flags, seq, ack: 0 },
2737                ))
2738            }
2739            RelayItem::Packed(bytes) => {
2740                let Some(rewritten) =
2741                    wire_format::rewrite_reliable_header_owned(bytes.as_ref(), flags, seq, 0)?
2742                else {
2743                    let Some(_side_tx_guard) = self.try_enter_side_tx() else {
2744                        return Err(TelemetryError::Io("side tx busy"));
2745                    };
2746                    let started_ms = self.clock.now_ms();
2747                    let frames = self.encode_side_transport_frames(side, opts, bytes.clone())?;
2748                    let mut sent_bytes = 0usize;
2749                    self.send_side_frames_resumable(side, frames, |frame| {
2750                        f(frame)?;
2751                        sent_bytes = sent_bytes.saturating_add(frame.len());
2752
2753                        Ok(())
2754                    })?;
2755                    self.record_side_tx_sample(side, sent_bytes, started_ms, self.clock.now_ms());
2756                    self.note_side_tx_success(side, ty, sent_bytes, 1);
2757                    return Ok(());
2758                };
2759                crate::shared_bytes::convert(rewritten)
2760            }
2761        };
2762
2763        let Some(_side_tx_guard) = self.try_enter_side_tx() else {
2764            return Err(TelemetryError::Io("side tx busy"));
2765        };
2766        let started_ms = self.clock.now_ms();
2767        let frames = self.encode_side_transport_frames(side, opts, bytes.clone())?;
2768        let mut sent_bytes = 0usize;
2769        self.send_side_frames_resumable(side, frames, |frame| {
2770            f(frame)?;
2771            sent_bytes = sent_bytes.saturating_add(frame.len());
2772
2773            Ok(())
2774        })?;
2775        self.record_side_tx_sample(side, sent_bytes, started_ms, self.clock.now_ms());
2776        self.note_side_tx_success(side, ty, sent_bytes, 1);
2777
2778        #[cfg(feature = "compact-packet-store")]
2779        {
2780            let _ = bytes.park_for_queue();
2781        }
2782
2783        {
2784            let mut st = self.state.lock();
2785            let tx_state = self.reliable_tx_state_mut(&mut st, side, ty);
2786            tx_state.sent_order.push_back(seq);
2787            tx_state.sent.insert(
2788                seq,
2789                ReliableSent {
2790                    bytes: bytes.clone(),
2791                    last_send_ms: self.clock.now_ms(),
2792                    retries: 0,
2793                    queued: false,
2794                    partial_acked: false,
2795                },
2796            );
2797        }
2798
2799        Ok(())
2800    }
2801
2802    fn item_route_info(
2803        &self,
2804        data: &RelayItem,
2805    ) -> TelemetryResult<(Vec<crate::DataEndpoint>, crate::DataType)> {
2806        match data {
2807            RelayItem::Packet(pkt) => {
2808                let mut eps = pkt.endpoints().to_vec();
2809                crate::small_sort::sort(&mut eps);
2810                eps.dedup();
2811                Ok((eps, pkt.data_type()))
2812            }
2813            RelayItem::Packed(bytes) => {
2814                let env = wire_format::peek_envelope(bytes.as_ref())?;
2815                let mut eps: Vec<crate::DataEndpoint> = env.endpoints.iter().copied().collect();
2816                crate::small_sort::sort(&mut eps);
2817                eps.dedup();
2818                Ok((eps, env.ty))
2819            }
2820        }
2821    }
2822
2823    fn endpoints_are_link_local_only(eps: &[crate::DataEndpoint]) -> bool {
2824        !eps.is_empty() && eps.iter().all(|ep| ep.is_link_local_only())
2825    }
2826
2827    fn item_target_senders<'a>(
2828        &self,
2829        data: &'a RelayItem,
2830    ) -> TelemetryResult<alloc::borrow::Cow<'a, [u64]>> {
2831        match data {
2832            RelayItem::Packet(pkt) => Ok(alloc::borrow::Cow::Borrowed(pkt.wire_target_senders())),
2833            RelayItem::Packed(bytes) => {
2834                let env = wire_format::peek_routing_envelope(bytes.as_ref())?;
2835                if env.target_senders().len() == 0 {
2836                    Ok(alloc::borrow::Cow::Borrowed(&[]))
2837                } else {
2838                    Ok(alloc::borrow::Cow::Owned(env.target_senders().collect()))
2839                }
2840            }
2841        }
2842    }
2843
2844    #[cfg(feature = "discovery")]
2845    fn has_explicit_route_policy_locked(
2846        st: &RelayInner,
2847        src: Option<RelaySideId>,
2848        ty: crate::DataType,
2849    ) -> bool {
2850        st.route_overrides
2851            .keys()
2852            .any(|(route_src, _)| *route_src == src)
2853            || Self::has_typed_route_overrides_locked(st, src, ty)
2854    }
2855
2856    #[cfg(feature = "discovery")]
2857    fn side_matches_target_senders_locked(
2858        st: &RelayInner,
2859        side: RelaySideId,
2860        target_senders: &[u64],
2861        now_ms: u64,
2862    ) -> bool {
2863        st.discovery_routes
2864            .get(&side)
2865            .map(|route| {
2866                if now_ms.saturating_sub(route.last_seen_ms) > DISCOVERY_ROUTE_TTL_MS {
2867                    return false;
2868                }
2869                route.announcers.iter().any(|(announcer, sender_state)| {
2870                    if now_ms.saturating_sub(sender_state.last_seen_ms) > DISCOVERY_ROUTE_TTL_MS {
2871                        return false;
2872                    }
2873                    target_senders.contains(&Self::sender_hash(announcer))
2874                        || sender_state
2875                            .advertised_address
2876                            .is_some_and(|address| target_senders.contains(&u64::from(address)))
2877                        || sender_state.topology_boards.iter().any(|board| {
2878                            target_senders.contains(&Self::sender_hash(&board.sender_id))
2879                        })
2880                })
2881            })
2882            .unwrap_or(false)
2883    }
2884
2885    fn remote_side_plan(
2886        &self,
2887        data: &RelayItem,
2888        exclude: RelaySideId,
2889    ) -> TelemetryResult<RemoteSidePlan> {
2890        #[cfg(feature = "discovery")]
2891        {
2892            let (eps, ty) = self.item_route_info(data)?;
2893            let target_senders = self.item_target_senders(data)?;
2894            let preferred_packet_id = Self::reliable_control_target_packet_id(data)?;
2895            if discovery::is_discovery_type(ty) {
2896                // Publish an aggregate split-horizon topology instead of
2897                // leaking far-side topology as direct neighbors. Address
2898                // allocation advertisements still cross transparent relays:
2899                // static/requested address conflicts must be resolved globally.
2900                if matches!(
2901                    ty,
2902                    crate::DataType::DiscoveryAnnounce
2903                        | crate::DataType::DiscoveryTimeSyncSources
2904                        | crate::DataType::DiscoveryTopology
2905                        | crate::DataType::DiscoveryLinkCapabilities
2906                ) {
2907                    return Ok(RemoteSidePlan::Target(Vec::new()));
2908                }
2909                let mut st = self.state.lock();
2910                let sides = self.eligible_side_ids_locked(&st, Some(exclude), Some(ty), false);
2911                return Ok(RemoteSidePlan::Target(self.apply_route_selection_locked(
2912                    &mut st,
2913                    Some(exclude),
2914                    sides,
2915                    RouteSelectionOrigin::Flood,
2916                )));
2917            }
2918
2919            #[cfg(feature = "timesync")]
2920            let preferred_timesync_source = self.preferred_timesync_route_source(data, ty)?;
2921            #[cfg(not(feature = "timesync"))]
2922            let preferred_timesync_source: Option<String> = None;
2923            let mut st = self.state.lock();
2924            if let Some(packet_id) = preferred_packet_id {
2925                let target_side = self.allowed_target_side_locked(
2926                    &st,
2927                    exclude,
2928                    ty,
2929                    st.reliable_return_routes
2930                        .get(&packet_id)
2931                        .map(|route| route.side),
2932                );
2933                if let Some(side) = target_side {
2934                    #[cfg(feature = "timesync")]
2935                    if !Self::timesync_allowed_for_side_locked(
2936                        &mut st,
2937                        side,
2938                        ty,
2939                        self.clock.now_ms(),
2940                    ) {
2941                        return Ok(RemoteSidePlan::Target(Vec::new()));
2942                    }
2943                    return Ok(RemoteSidePlan::Target(vec![side]));
2944                }
2945                let eligible = self.eligible_side_ids_locked(&st, Some(exclude), Some(ty), false);
2946                let original = target_senders.get(1..).unwrap_or(&[]);
2947                let exact: Vec<_> = eligible
2948                    .iter()
2949                    .copied()
2950                    .filter(|side| {
2951                        !original.is_empty()
2952                            && Self::side_matches_target_senders_locked(
2953                                &st,
2954                                *side,
2955                                original,
2956                                self.clock.now_ms(),
2957                            )
2958                    })
2959                    .collect();
2960                if !exact.is_empty() {
2961                    return Ok(RemoteSidePlan::Target(self.apply_route_selection_locked(
2962                        &mut st,
2963                        Some(exclude),
2964                        exact,
2965                        RouteSelectionOrigin::Discovered,
2966                    )));
2967                }
2968                return Ok(RemoteSidePlan::Target(if eligible.len() == 1 {
2969                    eligible
2970                } else {
2971                    Vec::new()
2972                }));
2973            }
2974            let restrict_link_local = Self::endpoints_are_link_local_only(&eps);
2975            let discovered_origin = if is_reliable_type(ty) {
2976                RouteSelectionOrigin::Flood
2977            } else {
2978                RouteSelectionOrigin::Discovered
2979            };
2980            if st.discovery_routes.is_empty() {
2981                let mut fallback = self.eligible_side_ids_locked(
2982                    &st,
2983                    Some(exclude),
2984                    Some(ty),
2985                    restrict_link_local,
2986                );
2987                #[cfg(feature = "timesync")]
2988                {
2989                    fallback = Self::filter_timesync_sides_locked(
2990                        &mut st,
2991                        ty,
2992                        self.clock.now_ms(),
2993                        fallback,
2994                    );
2995                }
2996                return Ok(RemoteSidePlan::Target(if fallback.len() == 1 {
2997                    fallback
2998                } else {
2999                    Vec::new()
3000                }));
3001            }
3002            let now_ms = self.clock.now_ms();
3003            let mut had_exact = false;
3004            let mut exact_targets = Vec::new();
3005            let mut had_known = false;
3006            let mut generic_targets = Vec::new();
3007            let mut unique_endpoint_fallbacks = Vec::new();
3008
3009            for (&side, route) in st.discovery_routes.iter() {
3010                if side == exclude
3011                    || now_ms.saturating_sub(route.last_seen_ms) > DISCOVERY_ROUTE_TTL_MS
3012                {
3013                    continue;
3014                }
3015                if restrict_link_local
3016                    && st
3017                        .sides
3018                        .get(side)
3019                        .and_then(|side| side.as_ref())
3020                        .map(|s| !s.opts.link_local_enabled)
3021                        .unwrap_or(true)
3022                {
3023                    continue;
3024                }
3025                if !self.route_allowed_locked(&st, Some(exclude), Some(ty), side) {
3026                    continue;
3027                }
3028                if !target_senders.is_empty() {
3029                    if !Self::side_matches_target_senders_locked(&st, side, &target_senders, now_ms)
3030                    {
3031                        if eps.iter().copied().any(|ep| route.reachable.contains(&ep)) {
3032                            unique_endpoint_fallbacks.push(side);
3033                        }
3034                        continue;
3035                    }
3036                    had_known = true;
3037                    generic_targets.push(side);
3038                    continue;
3039                }
3040                if preferred_timesync_source.as_deref().is_some_and(|source| {
3041                    route.reachable_timesync_sources.iter().any(|s| s == source)
3042                }) {
3043                    had_exact = true;
3044                    exact_targets.push(side);
3045                    continue;
3046                }
3047                if eps.iter().copied().any(|ep| route.reachable.contains(&ep)) {
3048                    had_known = true;
3049                    generic_targets.push(side);
3050                }
3051            }
3052
3053            // Compact/DHCP links can temporarily disagree on whether a frozen
3054            // destination is represented by hostname hash or assigned address.
3055            // Preserve autonomous endpoint routing only when discovery gives
3056            // one unambiguous non-ingress route; never fan an unknown target
3057            // out across multiple sides.
3058            if !target_senders.is_empty() && !had_known && unique_endpoint_fallbacks.len() == 1 {
3059                had_known = true;
3060                generic_targets = unique_endpoint_fallbacks;
3061            }
3062
3063            if had_exact {
3064                #[cfg(feature = "timesync")]
3065                {
3066                    exact_targets = Self::filter_timesync_sides_locked(
3067                        &mut st,
3068                        ty,
3069                        self.clock.now_ms(),
3070                        exact_targets,
3071                    );
3072                }
3073                let targets = self.filter_end_to_end_satisfied_sides_locked(
3074                    &st,
3075                    data,
3076                    exact_targets,
3077                    &eps,
3078                    ty,
3079                )?;
3080                Ok(RemoteSidePlan::Target(self.apply_route_selection_locked(
3081                    &mut st,
3082                    Some(exclude),
3083                    targets,
3084                    discovered_origin,
3085                )))
3086            } else if had_known {
3087                Self::retain_shortest_discovery_routes_locked(
3088                    &st,
3089                    &mut generic_targets,
3090                    &eps,
3091                    now_ms,
3092                );
3093                #[cfg(feature = "timesync")]
3094                {
3095                    generic_targets = Self::filter_timesync_sides_locked(
3096                        &mut st,
3097                        ty,
3098                        self.clock.now_ms(),
3099                        generic_targets,
3100                    );
3101                }
3102                let targets = self.filter_end_to_end_satisfied_sides_locked(
3103                    &st,
3104                    data,
3105                    generic_targets,
3106                    &eps,
3107                    ty,
3108                )?;
3109                Ok(RemoteSidePlan::Target(self.apply_route_selection_locked(
3110                    &mut st,
3111                    Some(exclude),
3112                    targets,
3113                    discovered_origin,
3114                )))
3115            } else {
3116                let fallback = self.eligible_side_ids_locked(
3117                    &st,
3118                    Some(exclude),
3119                    Some(ty),
3120                    restrict_link_local,
3121                );
3122                if fallback.len() == 1 {
3123                    return Ok(RemoteSidePlan::Target(fallback));
3124                }
3125                if Self::has_explicit_route_policy_locked(&st, Some(exclude), ty) {
3126                    let mut sides = self.eligible_side_ids_locked(
3127                        &st,
3128                        Some(exclude),
3129                        Some(ty),
3130                        restrict_link_local,
3131                    );
3132                    #[cfg(feature = "timesync")]
3133                    {
3134                        sides = Self::filter_timesync_sides_locked(
3135                            &mut st,
3136                            ty,
3137                            self.clock.now_ms(),
3138                            sides,
3139                        );
3140                    }
3141                    Ok(RemoteSidePlan::Target(self.apply_route_selection_locked(
3142                        &mut st,
3143                        Some(exclude),
3144                        sides,
3145                        RouteSelectionOrigin::Flood,
3146                    )))
3147                } else {
3148                    Ok(RemoteSidePlan::Target(Vec::new()))
3149                }
3150            }
3151        }
3152        #[cfg(not(feature = "discovery"))]
3153        {
3154            let (_, ty) = self.item_route_info(data)?;
3155            let mut st = self.state.lock();
3156            if let Some(packet_id) = Self::reliable_control_target_packet_id(data)? {
3157                let target_side = self.allowed_target_side_locked(
3158                    &st,
3159                    exclude,
3160                    ty,
3161                    st.reliable_return_routes
3162                        .get(&packet_id)
3163                        .map(|route| route.side),
3164                );
3165                if let Some(side) = target_side {
3166                    return Ok(RemoteSidePlan::Target(vec![side]));
3167                }
3168                let sides = self.eligible_side_ids_locked(&st, Some(exclude), Some(ty), false);
3169                return Ok(RemoteSidePlan::Target(if sides.len() == 1 {
3170                    sides
3171                } else {
3172                    Vec::new()
3173                }));
3174            }
3175            let sides = self.eligible_side_ids_locked(&st, Some(exclude), Some(ty), false);
3176            Ok(RemoteSidePlan::Target(self.apply_route_selection_locked(
3177                &mut st,
3178                Some(exclude),
3179                sides,
3180                RouteSelectionOrigin::Flood,
3181            )))
3182        }
3183    }
3184
3185    #[inline]
3186    fn allowed_target_side_locked(
3187        &self,
3188        st: &RelayInner,
3189        exclude: RelaySideId,
3190        ty: crate::DataType,
3191        target_side: Option<RelaySideId>,
3192    ) -> Option<RelaySideId> {
3193        target_side.filter(|side| self.route_allowed_locked(st, Some(exclude), Some(ty), *side))
3194    }
3195
3196    #[cfg(feature = "discovery")]
3197    fn filter_end_to_end_satisfied_sides_locked(
3198        &self,
3199        st: &RelayInner,
3200        data: &RelayItem,
3201        sides: Vec<RelaySideId>,
3202        eps: &[crate::DataEndpoint],
3203        ty: crate::DataType,
3204    ) -> TelemetryResult<Vec<RelaySideId>> {
3205        if !is_reliable_type(ty) || Self::reliable_control_target_packet_id(data)?.is_some() {
3206            return Ok(sides);
3207        }
3208        let packet_id = match data {
3209            RelayItem::Packet(pkt) => pkt.packet_id(),
3210            RelayItem::Packed(bytes) => match wire_format::packet_id_from_wire(bytes.as_ref()) {
3211                Ok(packet_id) => packet_id,
3212                Err(TelemetryError::Unpack("reliable control frame")) => return Ok(sides),
3213                Err(err) => return Err(err),
3214            },
3215        };
3216        let Some(acked) = st.end_to_end_acked_destinations.get(&packet_id) else {
3217            return Ok(sides);
3218        };
3219        let now_ms = self.clock.now_ms();
3220        let mut filtered = Vec::new();
3221        for side in sides {
3222            let Some(route) = st.discovery_routes.get(&side) else {
3223                filtered.push(side);
3224                continue;
3225            };
3226            let mut still_pending = false;
3227            let mut had_destination_board = false;
3228            for sender_state in route.announcers.values() {
3229                if now_ms.saturating_sub(sender_state.last_seen_ms) > DISCOVERY_ROUTE_TTL_MS {
3230                    continue;
3231                }
3232                for board in sender_state.topology_boards.iter() {
3233                    if !self.is_end_to_end_destination_sender(&board.sender_id) {
3234                        continue;
3235                    }
3236                    had_destination_board = true;
3237                    let sender_hash = Self::sender_hash(&board.sender_id);
3238                    if acked.contains(&sender_hash) {
3239                        continue;
3240                    }
3241                    if eps
3242                        .iter()
3243                        .copied()
3244                        .any(|ep| board.reachable_endpoints.contains(&ep))
3245                    {
3246                        still_pending = true;
3247                        break;
3248                    }
3249                    // Keep forwarding while any discovered destination sender for this packet
3250                    // remains unacked, even if topology/schema metadata changed for new packets.
3251                    still_pending = true;
3252                    break;
3253                }
3254                if still_pending {
3255                    break;
3256                }
3257            }
3258            if still_pending || !had_destination_board {
3259                filtered.push(side);
3260            }
3261        }
3262        Ok(filtered)
3263    }
3264
3265    #[cfg(not(feature = "discovery"))]
3266    fn filter_end_to_end_satisfied_sides_locked(
3267        &self,
3268        _st: &RelayInner,
3269        _data: &RelayItem,
3270        sides: Vec<RelaySideId>,
3271        _eps: &[crate::DataEndpoint],
3272        _ty: crate::DataType,
3273    ) -> TelemetryResult<Vec<RelaySideId>> {
3274        Ok(sides)
3275    }
3276
3277    #[cfg(feature = "discovery")]
3278    fn side_has_multiple_announcers_locked(
3279        &self,
3280        st: &RelayInner,
3281        side: RelaySideId,
3282        now_ms: u64,
3283    ) -> bool {
3284        st.discovery_routes
3285            .get(&side)
3286            .map(|route| {
3287                route
3288                    .announcers
3289                    .values()
3290                    .filter(|sender| {
3291                        now_ms.saturating_sub(sender.last_seen_ms) <= DISCOVERY_ROUTE_TTL_MS
3292                    })
3293                    .take(2)
3294                    .count()
3295                    > 1
3296            })
3297            .unwrap_or(false)
3298    }
3299
3300    #[cfg(not(feature = "discovery"))]
3301    fn side_has_multiple_announcers_locked(
3302        &self,
3303        _st: &RelayInner,
3304        _side: RelaySideId,
3305        _now_ms: u64,
3306    ) -> bool {
3307        false
3308    }
3309
3310    #[cfg(feature = "discovery")]
3311    fn sender_topology_board_mut<'a>(
3312        sender_state: &'a mut DiscoverySenderState,
3313        sender_id: &str,
3314    ) -> &'a mut TopologyBoardNode {
3315        let boards = Arc::make_mut(&mut sender_state.topology_boards);
3316        if let Some(idx) = boards.iter().position(|board| board.sender_id == sender_id) {
3317            return &mut boards[idx];
3318        }
3319        boards.push(TopologyBoardNode {
3320            sender_id: sender_id.to_string(),
3321            reachable_endpoints: Vec::new(),
3322            reachable_timesync_sources: Vec::new(),
3323            connections: Vec::new(),
3324        });
3325        boards.last_mut().expect("inserted above")
3326    }
3327
3328    #[cfg(feature = "discovery")]
3329    fn refresh_sender_topology_state(sender_state: &mut DiscoverySenderState) {
3330        if let Some(boards) = Arc::get_mut(&mut sender_state.topology_boards) {
3331            discovery::normalize_topology_boards(boards);
3332        }
3333        let (mut reachable, mut reachable_timesync_sources) =
3334            discovery::summarize_topology_boards(&sender_state.topology_boards);
3335        reachable.extend(sender_state.advertised_reachable.iter().copied());
3336        crate::small_sort::sort(&mut reachable);
3337        reachable.dedup();
3338        reachable_timesync_sources.extend(
3339            sender_state
3340                .advertised_reachable_timesync_sources
3341                .iter()
3342                .cloned(),
3343        );
3344        reachable_timesync_sources.sort_unstable();
3345        reachable_timesync_sources.dedup();
3346        sender_state.reachable = reachable;
3347        sender_state.reachable_timesync_sources = reachable_timesync_sources;
3348    }
3349
3350    #[cfg(feature = "discovery")]
3351    fn recompute_discovery_side_state(route: &mut DiscoverySideState) {
3352        let mut reachable = Vec::new();
3353        let mut reachable_timesync_sources = Vec::new();
3354        let mut last_seen_ms = 0u64;
3355        for sender in route.announcers.values() {
3356            reachable.extend(sender.reachable.iter().copied());
3357            reachable_timesync_sources.extend(sender.reachable_timesync_sources.iter().cloned());
3358            last_seen_ms = last_seen_ms.max(sender.last_seen_ms);
3359        }
3360        crate::small_sort::sort(&mut reachable);
3361        reachable.dedup();
3362        reachable_timesync_sources.sort_unstable();
3363        reachable_timesync_sources.dedup();
3364        route.reachable = reachable;
3365        route.reachable_timesync_sources = reachable_timesync_sources;
3366        route.last_seen_ms = last_seen_ms;
3367    }
3368
3369    #[cfg(feature = "discovery")]
3370    fn local_discovery_topology_board(&self, st: &RelayInner, now_ms: u64) -> TopologyBoardNode {
3371        let mut connections = Vec::new();
3372        for route in st.discovery_routes.values() {
3373            if now_ms.saturating_sub(route.last_seen_ms) > DISCOVERY_ROUTE_TTL_MS {
3374                continue;
3375            }
3376            for (sender, sender_state) in route.announcers.iter() {
3377                if now_ms.saturating_sub(sender_state.last_seen_ms) <= DISCOVERY_ROUTE_TTL_MS {
3378                    connections.push(sender.clone());
3379                }
3380            }
3381        }
3382        connections.sort_unstable();
3383        connections.dedup();
3384        let sender = self.sender_arc();
3385        TopologyBoardNode {
3386            sender_id: sender.to_string(),
3387            reachable_endpoints: Vec::new(),
3388            reachable_timesync_sources: Vec::new(),
3389            connections,
3390        }
3391    }
3392
3393    #[cfg(feature = "discovery")]
3394    fn advertised_discovery_topology_for_link_locked(
3395        &self,
3396        st: &RelayInner,
3397        now_ms: u64,
3398        link_local_enabled: bool,
3399        exclude_side: Option<RelaySideId>,
3400    ) -> Vec<TopologyBoardNode> {
3401        let mut boards = vec![self.local_discovery_topology_board(st, now_ms)];
3402        for (&route_side, route) in st.discovery_routes.iter() {
3403            if exclude_side == Some(route_side) {
3404                continue;
3405            }
3406            if now_ms.saturating_sub(route.last_seen_ms) > DISCOVERY_ROUTE_TTL_MS {
3407                continue;
3408            }
3409            for (announcer, sender_state) in route.announcers.iter() {
3410                if now_ms.saturating_sub(sender_state.last_seen_ms) > DISCOVERY_ROUTE_TTL_MS {
3411                    continue;
3412                }
3413                if sender_state.topology_boards.is_empty() {
3414                    let sender = self.sender_arc();
3415                    let fallback = TopologyBoardNode {
3416                        sender_id: announcer.clone(),
3417                        reachable_endpoints: sender_state.reachable.clone(),
3418                        reachable_timesync_sources: sender_state.reachable_timesync_sources.clone(),
3419                        connections: vec![sender.to_string()],
3420                    };
3421                    discovery::merge_topology_boards(&mut boards, core::slice::from_ref(&fallback));
3422                } else {
3423                    // Merge the retained view directly. Cloning it first and
3424                    // cloning again into the output doubled temporary strings.
3425                    discovery::merge_topology_boards(&mut boards, &sender_state.topology_boards);
3426                    if let Some(board) = boards
3427                        .iter_mut()
3428                        .find(|board| board.sender_id == *announcer)
3429                    {
3430                        let sender = self.sender_arc();
3431                        if !board.connections.iter().any(|peer| peer == sender.as_ref()) {
3432                            board.connections.push(sender.to_string());
3433                        }
3434                    }
3435                }
3436            }
3437        }
3438        if !link_local_enabled {
3439            for board in &mut boards {
3440                board
3441                    .reachable_endpoints
3442                    .retain(|ep| !ep.is_link_local_only());
3443            }
3444        }
3445        discovery::normalize_topology_boards(&mut boards);
3446        boards
3447    }
3448
3449    #[cfg(feature = "discovery")]
3450    fn note_discovery_topology_change_locked(st: &mut RelayInner, now_ms: u64) {
3451        st.discovery_cadence.on_topology_change(now_ms);
3452        for side_id in st
3453            .sides
3454            .iter()
3455            .enumerate()
3456            .filter_map(|(side_id, side)| side.as_ref().map(|_| side_id))
3457        {
3458            let throttle = st.discovery_side_throttle.entry(side_id).or_default();
3459            throttle.pending_incremental = true;
3460            throttle.incremental_retries_remaining = DISCOVERY_INCREMENTAL_RETRY_COUNT;
3461        }
3462        for transport in st.side_transport.values_mut() {
3463            transport.refresh_transmit_header_templates();
3464        }
3465    }
3466
3467    #[cfg(feature = "discovery")]
3468    fn prune_discovery_routes_locked(st: &mut RelayInner, now_ms: u64) -> bool {
3469        // Match router maintenance: unchanged routes require no heap work.
3470        let mut changed = false;
3471        st.discovery_routes.retain(|_, route| {
3472            let before_peers = route.announcers.len();
3473            route.announcers.retain(|_, sender| {
3474                now_ms.saturating_sub(sender.last_seen_ms) <= DISCOVERY_ROUTE_TTL_MS
3475            });
3476            if before_peers != route.announcers.len() {
3477                Self::recompute_discovery_side_state(route);
3478                changed = true;
3479            }
3480            let keep = !route.announcers.is_empty();
3481            changed |= !keep;
3482            keep
3483        });
3484        changed
3485    }
3486
3487    #[cfg(feature = "discovery")]
3488    fn reconcile_end_to_end_acked_destinations_locked(&self, st: &mut RelayInner) {
3489        if st.end_to_end_acked_destinations.is_empty() {
3490            return;
3491        }
3492        let mut active_senders = BTreeSet::new();
3493        for route in st.discovery_routes.values() {
3494            for sender_state in route.announcers.values() {
3495                for board in sender_state.topology_boards.iter() {
3496                    if self.is_end_to_end_destination_sender(&board.sender_id) {
3497                        active_senders.insert(Self::sender_hash(&board.sender_id));
3498                    }
3499                }
3500            }
3501        }
3502        st.end_to_end_acked_destinations.retain(|_, acked| {
3503            acked.retain(|sender_hash| active_senders.contains(sender_hash));
3504            !acked.is_empty()
3505        });
3506    }
3507
3508    #[cfg(feature = "discovery")]
3509    fn advertised_summaries_from_topology_locked(
3510        &self,
3511        st: &RelayInner,
3512        now_ms: u64,
3513        link_local_enabled: bool,
3514        exclude_side: Option<RelaySideId>,
3515        topology: &[TopologyBoardNode],
3516    ) -> (Vec<crate::DataEndpoint>, Vec<String>) {
3517        let (mut endpoints, mut sources) = discovery::summarize_topology_boards(topology);
3518        for (&side, route) in &st.discovery_routes {
3519            if exclude_side == Some(side)
3520                || now_ms.saturating_sub(route.last_seen_ms) > DISCOVERY_ROUTE_TTL_MS
3521            {
3522                continue;
3523            }
3524            for peer in route.announcers.values() {
3525                if now_ms.saturating_sub(peer.last_seen_ms) > DISCOVERY_ROUTE_TTL_MS {
3526                    continue;
3527                }
3528                for endpoint in &peer.advertised_reachable {
3529                    if !discovery::is_discovery_endpoint(*endpoint)
3530                        && (link_local_enabled || !endpoint.is_link_local_only())
3531                        && !endpoints.contains(endpoint)
3532                    {
3533                        endpoints.push(*endpoint);
3534                    }
3535                }
3536                for source in &peer.advertised_reachable_timesync_sources {
3537                    if !sources.contains(source) {
3538                        sources.push(source.clone());
3539                    }
3540                }
3541            }
3542        }
3543        endpoints.retain(|endpoint| {
3544            !discovery::is_discovery_endpoint(*endpoint)
3545                && (link_local_enabled || !endpoint.is_link_local_only())
3546        });
3547        crate::small_sort::sort(&mut endpoints);
3548        sources.sort_unstable();
3549        (endpoints, sources)
3550    }
3551
3552    #[cfg(feature = "discovery")]
3553    fn advertised_discovery_endpoints_for_link_locked(
3554        &self,
3555        st: &RelayInner,
3556        now_ms: u64,
3557        link_local_enabled: bool,
3558        exclude_side: Option<RelaySideId>,
3559    ) -> Vec<crate::DataEndpoint> {
3560        let (mut reachable_endpoints, _) = discovery::summarize_topology_boards(
3561            &self.advertised_discovery_topology_for_link_locked(
3562                st,
3563                now_ms,
3564                link_local_enabled,
3565                exclude_side,
3566            ),
3567        );
3568        // Compact advertisements can know routes before detailed topology arrives.
3569        // Preserve that reachability without assigning downstream endpoints to the bridge.
3570        for (&side, route) in &st.discovery_routes {
3571            if exclude_side == Some(side)
3572                || now_ms.saturating_sub(route.last_seen_ms) > DISCOVERY_ROUTE_TTL_MS
3573            {
3574                continue;
3575            }
3576            for sender in route.announcers.values() {
3577                if now_ms.saturating_sub(sender.last_seen_ms) <= DISCOVERY_ROUTE_TTL_MS {
3578                    reachable_endpoints.extend(sender.advertised_reachable.iter().copied());
3579                }
3580            }
3581        }
3582        crate::small_sort::sort(&mut reachable_endpoints);
3583        reachable_endpoints.dedup();
3584        reachable_endpoints
3585            .into_iter()
3586            .filter(|ep| {
3587                !discovery::is_discovery_endpoint(*ep)
3588                    && (link_local_enabled || !ep.is_link_local_only())
3589            })
3590            .collect()
3591    }
3592
3593    #[cfg(feature = "discovery")]
3594    fn advertised_discovery_timesync_sources_for_link_locked(
3595        &self,
3596        st: &RelayInner,
3597        now_ms: u64,
3598        exclude_side: Option<RelaySideId>,
3599    ) -> Vec<String> {
3600        let (_, mut sources) = discovery::summarize_topology_boards(
3601            &self.advertised_discovery_topology_for_link_locked(st, now_ms, true, exclude_side),
3602        );
3603        for (&side, route) in &st.discovery_routes {
3604            if exclude_side == Some(side)
3605                || now_ms.saturating_sub(route.last_seen_ms) > DISCOVERY_ROUTE_TTL_MS
3606            {
3607                continue;
3608            }
3609            for sender in route.announcers.values() {
3610                if now_ms.saturating_sub(sender.last_seen_ms) <= DISCOVERY_ROUTE_TTL_MS {
3611                    sources.extend(sender.advertised_reachable_timesync_sources.iter().cloned());
3612                }
3613            }
3614        }
3615        sources.sort_unstable();
3616        sources.dedup();
3617        sources
3618    }
3619
3620    #[cfg(feature = "discovery")]
3621    #[cfg(feature = "timesync")]
3622    fn preferred_timesync_route_source(
3623        &self,
3624        data: &RelayItem,
3625        ty: crate::DataType,
3626    ) -> TelemetryResult<Option<String>> {
3627        if !matches!(
3628            ty,
3629            crate::DataType::TimeSyncAnnounce | crate::DataType::TimeSyncResponse
3630        ) {
3631            return Ok(None);
3632        }
3633
3634        let sender = match data {
3635            RelayItem::Packet(pkt) => pkt.sender().to_owned(),
3636            RelayItem::Packed(bytes) => {
3637                if wire_format::peek_frame_info(bytes.as_ref())
3638                    .ok()
3639                    .is_some_and(|frame| frame.ack_only())
3640                {
3641                    return Ok(None);
3642                }
3643                wire_format::unpack_packet(bytes.as_ref())?
3644                    .sender()
3645                    .to_owned()
3646            }
3647        };
3648        Ok(Some(sender))
3649    }
3650
3651    #[cfg(all(feature = "discovery", feature = "timesync"))]
3652    #[inline]
3653    fn side_is_slow_control_link_locked(
3654        st: &RelayInner,
3655        side_id: RelaySideId,
3656        now_ms: u64,
3657    ) -> bool {
3658        st.adaptive_route_stats.get(&side_id).is_some_and(|stats| {
3659            let recent_slow = stats.last_slow_observed_ms > 0
3660                && now_ms.saturating_sub(stats.last_slow_observed_ms)
3661                    <= discovery::DISCOVERY_SLOW_LINK_FULL_INTERVAL_MS;
3662            stats.sample_count > 0
3663                && ((stats.estimated_bandwidth_bps > 0
3664                    && stats.estimated_bandwidth_bps <= CONTROL_SLOW_LINK_CAPACITY_BPS)
3665                    || recent_slow)
3666        })
3667    }
3668
3669    #[cfg(feature = "discovery")]
3670    fn discovery_level_for_side_locked(
3671        st: &mut RelayInner,
3672        side_id: RelaySideId,
3673        now_ms: u64,
3674    ) -> Option<DiscoveryAdvertiseLevel> {
3675        let throttle = st.discovery_side_throttle.entry(side_id).or_default();
3676        if !throttle.has_sent_full {
3677            throttle.has_sent_full = true;
3678            throttle.pending_incremental = false;
3679            throttle.incremental_retries_remaining = 0;
3680            throttle.next_ping_ms = now_ms.saturating_add(DISCOVERY_SLOW_LINK_PING_INTERVAL_MS);
3681            return Some(DiscoveryAdvertiseLevel::Full);
3682        }
3683        if throttle.pending_incremental {
3684            throttle.incremental_retries_remaining =
3685                throttle.incremental_retries_remaining.saturating_sub(1);
3686            throttle.pending_incremental = throttle.incremental_retries_remaining > 0;
3687            return Some(DiscoveryAdvertiseLevel::Incremental);
3688        }
3689        // Stable links carry only a small liveness announcement. Route and
3690        // schema snapshots are emitted on changes or explicit recovery requests.
3691        if throttle.liveness_due(now_ms) {
3692            throttle.next_ping_ms = now_ms.saturating_add(DISCOVERY_SLOW_LINK_PING_INTERVAL_MS);
3693            return Some(DiscoveryAdvertiseLevel::MinimalPing);
3694        }
3695        None
3696    }
3697
3698    #[cfg(all(feature = "discovery", feature = "timesync"))]
3699    #[inline]
3700    fn is_timesync_type(ty: crate::DataType) -> bool {
3701        matches!(
3702            ty,
3703            crate::DataType::TimeSyncAnnounce
3704                | crate::DataType::TimeSyncRequest
3705                | crate::DataType::TimeSyncResponse
3706        )
3707    }
3708
3709    #[cfg(all(feature = "discovery", feature = "timesync"))]
3710    fn timesync_allowed_for_side_locked(
3711        st: &mut RelayInner,
3712        side_id: RelaySideId,
3713        ty: crate::DataType,
3714        now_ms: u64,
3715    ) -> bool {
3716        if !Self::is_timesync_type(ty) {
3717            return true;
3718        }
3719        if !Self::side_is_slow_control_link_locked(st, side_id, now_ms) {
3720            st.timesync_side_throttle.remove(&side_id);
3721            return true;
3722        }
3723
3724        let throttle = st.timesync_side_throttle.entry(side_id).or_default();
3725        if now_ms >= throttle.next_allowed_ms {
3726            throttle.next_allowed_ms = now_ms.saturating_add(TIMESYNC_SLOW_LINK_MIN_INTERVAL_MS);
3727            return true;
3728        }
3729        false
3730    }
3731
3732    #[cfg(all(feature = "discovery", feature = "timesync"))]
3733    fn filter_timesync_sides_locked(
3734        st: &mut RelayInner,
3735        ty: crate::DataType,
3736        now_ms: u64,
3737        sides: Vec<RelaySideId>,
3738    ) -> Vec<RelaySideId> {
3739        sides
3740            .into_iter()
3741            .filter(|side| Self::timesync_allowed_for_side_locked(st, *side, ty, now_ms))
3742            .collect()
3743    }
3744
3745    #[cfg(feature = "discovery")]
3746    fn queue_discovery_announce(
3747        &self,
3748        include_schema: bool,
3749        skip_in_flight_sides: bool,
3750    ) -> TelemetryResult<()> {
3751        self.queue_discovery_announce_on_side(include_schema, skip_in_flight_sides, None)
3752    }
3753
3754    #[cfg(feature = "discovery")]
3755    fn refresh_known_discovery_liveness(
3756        &self,
3757        pkt: &Packet,
3758        side: RelaySideId,
3759    ) -> TelemetryResult<bool> {
3760        if pkt.data_type() == crate::DataType::DiscoveryTopology {
3761            // No decoded graph or retained-side clone is needed for a retry
3762            // whose payload already describes precisely the retained state.
3763            // Keep changed topology behind the full admission guard.
3764            crate::memory_admission::check(512, 128)?;
3765            let now = self.clock.now_ms();
3766            let mut st = self.state.lock();
3767            let canonical = Self::canonical_sender_locked(&st, pkt.sender());
3768            let Some(route) = st.discovery_routes.get_mut(&side) else {
3769                return Ok(false);
3770            };
3771            let Some(peer) = route.announcers.get_mut(canonical.as_str()) else {
3772                return Ok(false);
3773            };
3774            let Some(full) =
3775                discovery::topology_payload_matches_retained(pkt.payload(), &peer.topology_boards)
3776            else {
3777                return Ok(false);
3778            };
3779            peer.has_full_topology |= full;
3780            peer.last_seen_ms = now;
3781            route.last_seen_ms = route.last_seen_ms.max(now);
3782            return Ok(true);
3783        }
3784        let empty_ping =
3785            pkt.data_type() == crate::DataType::DiscoveryAnnounce && pkt.payload().is_empty();
3786        if !empty_ping && pkt.data_type() != crate::DataType::DiscoveryAddress {
3787            return Ok(false);
3788        }
3789        crate::memory_admission::check(
3790            if empty_ping {
3791                512
3792            } else {
3793                1024 + pkt.payload().len().saturating_mul(4)
3794            },
3795            pkt.payload().len().saturating_mul(2).max(128),
3796        )?;
3797        let mut address = if empty_ping {
3798            None
3799        } else {
3800            Some(discovery::decode_discovery_address(pkt)?)
3801        };
3802        let now = self.clock.now_ms();
3803        let mut st = self.state.lock();
3804        if let Some(ad) = &mut address {
3805            let link_local = st
3806                .sides
3807                .get(side)
3808                .and_then(|entry| entry.as_ref())
3809                .is_some_and(|entry| entry.opts.link_local_enabled);
3810            if !link_local {
3811                ad.reachable_endpoints.retain(|ep| !ep.is_link_local_only());
3812            }
3813        }
3814        let canonical;
3815        let sender = if let Some(ad) = &address {
3816            ad.hostname.as_str()
3817        } else {
3818            canonical = Self::canonical_sender_locked(&st, pkt.sender());
3819            canonical.as_str()
3820        };
3821        let Some(route) = st.discovery_routes.get_mut(&side) else {
3822            return Ok(false);
3823        };
3824        let Some(peer) = route.announcers.get_mut(sender) else {
3825            return Ok(false);
3826        };
3827        if let Some(ad) = &address
3828            && (peer.advertised_address != Some(ad.address)
3829                || peer.advertised_reachable != ad.reachable_endpoints
3830                || peer.advertised_reachable_timesync_sources != ad.reachable_timesync_sources)
3831        {
3832            return Ok(false);
3833        }
3834        // A liveness update cannot add ownership, change identity, or enlarge
3835        // discovery state. Leave new/changed summaries behind the full guard.
3836        peer.last_seen_ms = now;
3837        route.last_seen_ms = route.last_seen_ms.max(now);
3838
3839        Ok(true)
3840    }
3841
3842    #[cfg(feature = "discovery")]
3843    fn queue_pressure_keepalives(
3844        &self,
3845        requested_side: Option<RelaySideId>,
3846    ) -> TelemetryResult<()> {
3847        let now = self.clock.now_ms();
3848        let sender = self.sender_arc();
3849        crate::memory_admission::check(
3850            256 + sender.len().saturating_mul(4),
3851            sender.len().max(256),
3852        )?;
3853        let side_count = self.state.lock().sides.len();
3854        for side in 0..side_count {
3855            let due = {
3856                let st = self.state.lock();
3857                requested_side.is_none_or(|wanted| wanted == side)
3858                    && st.sides[side].is_some()
3859                    && self.route_allowed_locked(
3860                        &st,
3861                        None,
3862                        Some(crate::DataType::DiscoveryAnnounce),
3863                        side,
3864                    )
3865                    && st
3866                        .discovery_side_throttle
3867                        .get(&side)
3868                        .is_none_or(|throttle| now >= throttle.next_ping_ms)
3869            };
3870            if !due {
3871                continue;
3872            }
3873            let pkt = discovery::build_discovery_announce(sender.as_ref(), now, &[])?;
3874            self.state.lock().push_tx(RelayTxItem {
3875                src: None,
3876                dst: side,
3877                data: RelayItem::Packet(Arc::new(pkt)),
3878                priority: 255,
3879                retry_after_ms: 0,
3880            })?;
3881            self.state
3882                .lock()
3883                .discovery_side_throttle
3884                .entry(side)
3885                .or_default()
3886                .next_ping_ms = now.saturating_add(discovery::DISCOVERY_SLOW_INTERVAL_MS);
3887        }
3888        Ok(())
3889    }
3890
3891    #[cfg(feature = "discovery")]
3892    fn queue_discovery_announce_on_side(
3893        &self,
3894        include_schema: bool,
3895        skip_in_flight_sides: bool,
3896        requested_side: Option<RelaySideId>,
3897    ) -> TelemetryResult<()> {
3898        #[cfg(not(feature = "std"))]
3899        let _ = include_schema;
3900        if let Some(side) = requested_side {
3901            return self.queue_discovery_announce_one_side(
3902                include_schema,
3903                skip_in_flight_sides,
3904                Some(side),
3905                true,
3906            );
3907        }
3908        let count = self.state.lock().sides.len();
3909        let mut advertised = false;
3910        for side in 0..count {
3911            if self
3912                .state
3913                .lock()
3914                .sides
3915                .get(side)
3916                .is_some_and(|side| side.is_some())
3917            {
3918                self.queue_discovery_announce_one_side(
3919                    include_schema,
3920                    skip_in_flight_sides,
3921                    Some(side),
3922                    false,
3923                )?;
3924                advertised = true;
3925            }
3926        }
3927        // The shared cadence advances once per announcement, regardless of
3928        // side count. Advancing it once per side silently doubled backoff.
3929        if advertised {
3930            self.state
3931                .lock()
3932                .discovery_cadence
3933                .on_announce_sent(self.clock.now_ms());
3934        }
3935        Ok(())
3936    }
3937
3938    #[cfg(feature = "discovery")]
3939    fn queue_discovery_announce_one_side(
3940        &self,
3941        include_schema: bool,
3942        skip_in_flight_sides: bool,
3943        requested_side: Option<RelaySideId>,
3944        advance_cadence: bool,
3945    ) -> TelemetryResult<()> {
3946        let (scratch, largest) = {
3947            let now = self.clock.now_ms();
3948            let sender = self.sender_arc();
3949            let st = self.state.lock();
3950            discovery::topology_snapshot_scratch(
3951                sender.as_ref(),
3952                0,
3953                0,
3954                4,
3955                true,
3956                st.discovery_routes
3957                    .iter()
3958                    .filter(|(side, route)| {
3959                        Some(**side) != requested_side
3960                            && now.saturating_sub(route.last_seen_ms) <= DISCOVERY_ROUTE_TTL_MS
3961                    })
3962                    .flat_map(|(_, route)| route.announcers.iter())
3963                    .filter(|(_, peer)| {
3964                        now.saturating_sub(peer.last_seen_ms) <= DISCOVERY_ROUTE_TTL_MS
3965                    })
3966                    .map(|(name, peer)| {
3967                        (
3968                            name.as_str(),
3969                            peer.reachable.as_slice(),
3970                            peer.reachable_timesync_sources.as_slice(),
3971                            peer.topology_boards.as_slice(),
3972                        )
3973                    }),
3974            )
3975        };
3976        // Full metadata also carries the learned topology. Its working set
3977        // must never bypass the graph estimate merely because schema is due.
3978        let (scratch, largest) = if include_schema {
3979            (scratch.max(8192), largest.max(2048))
3980        } else {
3981            (scratch, largest)
3982        };
3983        // Snapshot construction may clone several route/schema summaries.
3984        // Reclaim ordinary queued telemetry before refusing discovery scratch.
3985        // Priority >= 200 commands, ACKs and metadata stay protected; do not
3986        // evict routes or reliable receive/reassembly state.
3987        while let Err(err) = crate::memory_admission::check(scratch, largest) {
3988            if matches!(err, TelemetryError::Io("memory pressure"))
3989                && crate::memory_admission::check(512, 256).is_ok()
3990            {
3991                let mut st = self.state.lock();
3992                if st.pop_lowest_packet_queue_item(200) {
3993                    continue;
3994                }
3995            }
3996            if !include_schema && matches!(err, TelemetryError::Io("memory pressure")) {
3997                return self.queue_pressure_keepalives(requested_side);
3998            }
3999            return Err(err);
4000        }
4001        let now_ms = self.clock.now_ms();
4002        let per_side = {
4003            let mut st = self.state.lock();
4004            if Self::prune_discovery_routes_locked(&mut st, now_ms) {
4005                self.reconcile_end_to_end_acked_destinations_locked(&mut st);
4006                Self::note_discovery_topology_change_locked(&mut st, now_ms);
4007            }
4008            st.fit_discovery_budget();
4009            if !st.sides.iter().any(|side| side.is_some()) {
4010                return Ok(());
4011            }
4012            if include_schema {
4013                for (side, throttle) in st.discovery_side_throttle.iter_mut() {
4014                    if requested_side.is_none_or(|requested| requested == *side) {
4015                        throttle.has_sent_full = false;
4016                    }
4017                }
4018            }
4019            if advance_cadence {
4020                st.discovery_cadence.on_announce_sent(now_ms);
4021            }
4022            // The caller selects one side; keep its plan on the stack.
4023            let mut per_side = None;
4024            for side_id in 0..st.sides.len() {
4025                let Some(side) = st.sides[side_id].as_ref() else {
4026                    continue;
4027                };
4028                let opts = side.opts.clone();
4029                let link_local_enabled = opts.link_local_enabled;
4030                if requested_side.is_some_and(|requested| requested != side_id) {
4031                    continue;
4032                }
4033                let discovery_in_flight = skip_in_flight_sides
4034                    && st.reliable_tx.iter().any(|((pending_side, ty), tx_state)| {
4035                        *pending_side == side_id
4036                            && !tx_state.sent.is_empty()
4037                            && crate::DataType::try_from_u32(*ty)
4038                                .is_some_and(discovery::is_discovery_type)
4039                    });
4040                if !self.route_allowed_locked(
4041                    &st,
4042                    None,
4043                    Some(crate::DataType::DiscoveryAnnounce),
4044                    side_id,
4045                ) {
4046                    continue;
4047                }
4048                // Preserve neighbor liveness even if a reliable discovery
4049                // baseline is still waiting for its hop ACK.
4050                let level = if discovery_in_flight {
4051                    let throttle = st.discovery_side_throttle.entry(side_id).or_default();
4052                    if !throttle.liveness_due(now_ms) {
4053                        None
4054                    } else {
4055                        throttle.next_ping_ms =
4056                            now_ms.saturating_add(DISCOVERY_SLOW_LINK_PING_INTERVAL_MS);
4057                        Some(DiscoveryAdvertiseLevel::MinimalPing)
4058                    }
4059                } else {
4060                    Self::discovery_level_for_side_locked(&mut st, side_id, now_ms)
4061                };
4062                let Some(level) = level else {
4063                    continue;
4064                };
4065                let capabilities = opts.link_capabilities();
4066                if level == DiscoveryAdvertiseLevel::MinimalPing {
4067                    per_side = Some((
4068                        side_id,
4069                        level,
4070                        Vec::new(),
4071                        Vec::new(),
4072                        Vec::new(),
4073                        Vec::new(),
4074                        capabilities,
4075                    ));
4076                    continue;
4077                }
4078                let current_topology = self.advertised_discovery_topology_for_link_locked(
4079                    &st,
4080                    now_ms,
4081                    link_local_enabled,
4082                    Some(side_id),
4083                );
4084                let (endpoints, timesync_sources) = self.advertised_summaries_from_topology_locked(
4085                    &st,
4086                    now_ms,
4087                    link_local_enabled,
4088                    Some(side_id),
4089                    &current_topology,
4090                );
4091                let (topology, removed_topology) = match level {
4092                    DiscoveryAdvertiseLevel::Full => (current_topology.clone(), Vec::new()),
4093                    DiscoveryAdvertiseLevel::Incremental => {
4094                        let previous = &st
4095                            .discovery_side_throttle
4096                            .get(&side_id)
4097                            .expect("discovery throttle exists")
4098                            .last_topology;
4099                        discovery::topology_delta(previous, &current_topology)
4100                    }
4101                    DiscoveryAdvertiseLevel::MinimalPing => (Vec::new(), Vec::new()),
4102                };
4103                let throttle = st
4104                    .discovery_side_throttle
4105                    .get_mut(&side_id)
4106                    .expect("discovery throttle exists");
4107                if level == DiscoveryAdvertiseLevel::Full
4108                    || (level == DiscoveryAdvertiseLevel::Incremental
4109                        && throttle.incremental_retries_remaining == 0)
4110                {
4111                    throttle.last_topology = current_topology;
4112                }
4113                per_side = Some((
4114                    side_id,
4115                    level,
4116                    endpoints,
4117                    timesync_sources,
4118                    topology,
4119                    removed_topology,
4120                    capabilities,
4121                ));
4122            }
4123            per_side
4124        };
4125        let mut st = self.state.lock();
4126        if let Some((
4127            dst,
4128            level,
4129            endpoints,
4130            timesync_sources,
4131            topology,
4132            removed_topology,
4133            capabilities,
4134        )) = per_side
4135        {
4136            let sender = self.sender_arc();
4137            if level == DiscoveryAdvertiseLevel::Full {
4138                let pkt = discovery::build_discovery_link_capabilities(
4139                    sender.as_ref(),
4140                    now_ms,
4141                    capabilities,
4142                )?;
4143                let data = RelayItem::Packet(Arc::new(pkt));
4144                let priority = Self::relay_item_priority(&data)?;
4145                st.push_tx(RelayTxItem {
4146                    src: None,
4147                    dst,
4148                    data,
4149                    priority,
4150                    retry_after_ms: 0,
4151                })?;
4152            }
4153            if level != DiscoveryAdvertiseLevel::Full || !endpoints.is_empty() {
4154                let pkt = discovery::build_discovery_announce(
4155                    sender.as_ref(),
4156                    now_ms,
4157                    endpoints.as_slice(),
4158                )?;
4159                let data = RelayItem::Packet(Arc::new(pkt));
4160                let priority = Self::relay_item_priority(&data)?;
4161                st.push_tx(RelayTxItem {
4162                    src: None,
4163                    dst,
4164                    data,
4165                    priority,
4166                    retry_after_ms: 0,
4167                })?;
4168            }
4169            if level != DiscoveryAdvertiseLevel::MinimalPing && !timesync_sources.is_empty() {
4170                let pkt = discovery::build_discovery_timesync_sources(
4171                    sender.as_ref(),
4172                    now_ms,
4173                    timesync_sources.as_slice(),
4174                )?;
4175                let data = RelayItem::Packet(Arc::new(pkt));
4176                let priority = Self::relay_item_priority(&data)?;
4177                st.push_tx(RelayTxItem {
4178                    src: None,
4179                    dst,
4180                    data,
4181                    priority,
4182                    retry_after_ms: 0,
4183                })?;
4184            }
4185            if level != DiscoveryAdvertiseLevel::MinimalPing
4186                && (!topology.is_empty() || !removed_topology.is_empty())
4187            {
4188                let pkt = if level == DiscoveryAdvertiseLevel::Full {
4189                    discovery::build_discovery_topology(sender.as_ref(), now_ms, &topology)?
4190                } else {
4191                    discovery::build_discovery_topology_delta(
4192                        sender.as_ref(),
4193                        now_ms,
4194                        &topology,
4195                        &removed_topology,
4196                    )?
4197                };
4198                let data = RelayItem::Packet(Arc::new(pkt));
4199                let priority = Self::relay_item_priority(&data)?;
4200                st.push_tx(RelayTxItem {
4201                    src: None,
4202                    dst,
4203                    data,
4204                    priority,
4205                    retry_after_ms: 0,
4206                })?;
4207            }
4208            // Match the router's phase ordering: route information first,
4209            // then release snapshot buffers before constructing schema.
4210            drop((endpoints, timesync_sources, topology, removed_topology));
4211            if include_schema && level == DiscoveryAdvertiseLevel::Full {
4212                let pkt = discovery::build_discovery_schema(sender.as_ref(), now_ms)?;
4213                let data = RelayItem::Packet(Arc::new(pkt));
4214                let priority = Self::relay_item_priority(&data)?;
4215                st.push_tx(RelayTxItem {
4216                    src: None,
4217                    dst,
4218                    data,
4219                    priority,
4220                    retry_after_ms: 0,
4221                })?;
4222            }
4223        }
4224        Ok(())
4225    }
4226
4227    #[cfg(feature = "discovery")]
4228    fn note_schema_received(&self, side: RelaySideId, sender: &str) {
4229        let mut st = self.state.lock();
4230        // Schema snapshots can be forwarded. They must never introduce a new
4231        // link-local announcer or change shared-bus reliability semantics.
4232        // If schema precedes topology, bounded recovery will request it again.
4233        let address = sender
4234            .strip_prefix("@addr:")
4235            .and_then(|value| value.parse::<u32>().ok());
4236        if let Some(route) = st.discovery_routes.get_mut(&side) {
4237            for (name, peer) in &mut route.announcers {
4238                if name == sender
4239                    || address.is_some_and(|address| {
4240                        peer.advertised_address == Some(address)
4241                            || sender_address_u32(name) == address
4242                    })
4243                {
4244                    peer.has_schema = true;
4245                    break;
4246                }
4247            }
4248        }
4249    }
4250
4251    #[cfg(feature = "discovery")]
4252    fn poll_discovery_announce(&self) -> TelemetryResult<bool> {
4253        let now_ms = self.clock.now_ms();
4254        let recovering = {
4255            let mut st = self.state.lock();
4256            let mut recovering = false;
4257            for side in 0..st.sides.len() {
4258                let missing = st.discovery_routes.get(&side).is_some_and(|route| {
4259                    route.announcers.iter().any(|(name, peer)| {
4260                        !peer.has_full_topology
4261                            && now_ms.saturating_sub(peer.last_seen_ms) < DISCOVERY_ROUTE_TTL_MS
4262                            && !route.announcers.values().any(|bridge| {
4263                                bridge.has_full_topology
4264                                    && now_ms.saturating_sub(bridge.last_seen_ms)
4265                                        < DISCOVERY_ROUTE_TTL_MS
4266                                    && bridge
4267                                        .topology_boards
4268                                        .iter()
4269                                        .any(|board| board.sender_id == *name)
4270                            })
4271                    })
4272                });
4273                if !missing {
4274                    continue;
4275                }
4276                let throttle = st.discovery_side_throttle.entry(side).or_default();
4277                if throttle.next_topology_request_ms == 0 {
4278                    throttle.next_topology_request_ms =
4279                        now_ms.saturating_add(discovery::DISCOVERY_BASELINE_GRACE_MS);
4280                    continue;
4281                }
4282                if now_ms < throttle.next_topology_request_ms {
4283                    continue;
4284                }
4285                throttle.next_topology_request_ms =
4286                    now_ms.saturating_add(discovery::DISCOVERY_BASELINE_RETRY_MS);
4287                let pkt = discovery::build_discovery_topology_request(
4288                    self.sender_arc().as_ref(),
4289                    now_ms,
4290                )?;
4291                let data = RelayItem::Packet(Arc::new(pkt));
4292                let priority = Self::relay_item_priority(&data)?;
4293                st.push_tx(RelayTxItem {
4294                    src: None,
4295                    dst: side,
4296                    data,
4297                    priority,
4298                    retry_after_ms: 0,
4299                })?;
4300                recovering = true;
4301            }
4302            recovering
4303        };
4304        let schema_recovery_sides = {
4305            let mut st = self.state.lock();
4306            let mut due = Vec::new();
4307            for side in 0..st.sides.len() {
4308                let missing = st.discovery_routes.get(&side).is_some_and(|route| {
4309                    route.announcers.values().any(|peer| {
4310                        peer.has_full_topology
4311                            && !peer.has_schema
4312                            && now_ms.saturating_sub(peer.last_seen_ms) < DISCOVERY_ROUTE_TTL_MS
4313                    })
4314                });
4315                if !missing {
4316                    continue;
4317                }
4318                let throttle = st.discovery_side_throttle.entry(side).or_default();
4319                if throttle.next_schema_request_ms == 0 {
4320                    throttle.next_schema_request_ms =
4321                        now_ms.saturating_add(discovery::DISCOVERY_BASELINE_GRACE_MS);
4322                } else if now_ms >= throttle.next_schema_request_ms {
4323                    throttle.next_schema_request_ms =
4324                        now_ms.saturating_add(discovery::DISCOVERY_BASELINE_RETRY_MS);
4325                    due.push(side);
4326                }
4327            }
4328            due
4329        };
4330        let recovering = recovering || !schema_recovery_sides.is_empty();
4331        for side in schema_recovery_sides {
4332            let pkt =
4333                discovery::build_discovery_schema_request(self.sender_arc().as_ref(), now_ms)?;
4334            let data = RelayItem::Packet(Arc::new(pkt));
4335            let priority = Self::relay_item_priority(&data)?;
4336            self.state.lock().push_tx(RelayTxItem {
4337                src: None,
4338                dst: side,
4339                data,
4340                priority,
4341                retry_after_ms: 0,
4342            })?;
4343        }
4344        let due = {
4345            let mut st = self.state.lock();
4346            let removed = Self::prune_discovery_routes_locked(&mut st, now_ms);
4347            if removed {
4348                self.reconcile_end_to_end_acked_destinations_locked(&mut st);
4349                Self::note_discovery_topology_change_locked(&mut st, now_ms);
4350            }
4351            st.fit_discovery_budget();
4352            let has_any = st.sides.iter().enumerate().any(|(side_id, side)| {
4353                let Some(side) = side.as_ref() else {
4354                    return false;
4355                };
4356                if !self.route_allowed_locked(
4357                    &st,
4358                    None,
4359                    Some(crate::DataType::DiscoveryAnnounce),
4360                    side_id,
4361                ) {
4362                    return false;
4363                }
4364                let _ = side;
4365                let in_flight = st.reliable_tx.iter().any(|((pending_side, ty), tx_state)| {
4366                    *pending_side == side_id
4367                        && !tx_state.sent.is_empty()
4368                        && crate::DataType::try_from_u32(*ty)
4369                            .is_some_and(discovery::is_discovery_type)
4370                });
4371                st.discovery_side_throttle
4372                    .get(&side_id)
4373                    .is_none_or(|throttle| {
4374                        (!in_flight && (!throttle.has_sent_full || throttle.pending_incremental))
4375                            || throttle.liveness_due(now_ms)
4376                    })
4377            });
4378            if !st.sides.iter().any(|side| side.is_some()) || !has_any {
4379                return Ok(recovering);
4380            }
4381            st.discovery_cadence.due(now_ms)
4382        };
4383        if !due {
4384            return Ok(recovering);
4385        }
4386        // Keep periodic discovery lightweight so a hosted relay cannot fill a
4387        // constrained link with repeated schema snapshots.
4388        self.queue_discovery_announce(false, true)?;
4389        Ok(true)
4390    }
4391
4392    #[cfg(feature = "discovery")]
4393    fn learn_discovery_item(&self, src: RelaySideId, data: &RelayItem) -> TelemetryResult<()> {
4394        let pkt = match data {
4395            RelayItem::Packet(pkt) => {
4396                if !discovery::is_discovery_type(pkt.data_type()) {
4397                    return Ok(());
4398                }
4399                pkt.as_ref().clone()
4400            }
4401            RelayItem::Packed(bytes) => {
4402                let env = wire_format::peek_routing_envelope(bytes.as_ref())?;
4403                if !discovery::is_discovery_type(env.ty) {
4404                    return Ok(());
4405                }
4406                if wire_format::peek_frame_info(bytes.as_ref())
4407                    .ok()
4408                    .is_some_and(|frame| frame.ack_only())
4409                {
4410                    return Ok(());
4411                }
4412                wire_format::unpack_packet(bytes.as_ref())?
4413            }
4414        };
4415
4416        if self.refresh_known_discovery_liveness(&pkt, src)? {
4417            return Ok(());
4418        }
4419        #[cfg(not(feature = "std"))]
4420        if pkt.data_type() == crate::DataType::DiscoverySchema
4421            && discovery::discovery_schema_payload_is_fully_known(pkt.payload())?
4422        {
4423            self.note_schema_received(src, pkt.sender());
4424            return Ok(());
4425        }
4426        // A compact address summary does not clone the complete topology or
4427        // schema. Account for its ingress-side aggregates instead; charging
4428        // every new peer for a full snapshot can prevent restart recovery.
4429        if matches!(
4430            pkt.data_type(),
4431            crate::DataType::DiscoveryTopologyRequest | crate::DataType::DiscoverySchemaRequest
4432        ) {
4433            crate::memory_admission::check(1024, 256)?;
4434            // A restarted receiver has no compact-header dictionary even if
4435            // discovery still has exactly the same routes. Refresh TX only on
4436            // its ingress link; do not erase RX templates or routing state.
4437            if let Some(transport) = self.state.lock().side_transport.get_mut(&src) {
4438                transport.refresh_transmit_header_templates();
4439            }
4440            // Like routers, relays retain a split-horizon baseline and must
4441            // answer a restarting neighbor without waiting for master election.
4442            {
4443                let mut st = self.state.lock();
4444                let throttle = st.discovery_side_throttle.entry(src).or_default();
4445                throttle.has_sent_full = false;
4446                // A full reply replaces this derived delta baseline. Release
4447                // it before snapshot admission, preserving live routing state.
4448                throttle.last_topology = Vec::new();
4449            }
4450            return self.queue_discovery_announce_on_side(
4451                pkt.data_type() == crate::DataType::DiscoverySchemaRequest,
4452                false,
4453                Some(src),
4454            );
4455        }
4456        // Standalone relays forward variable requests; they do not own a
4457        // subscription registry or decode a schema for this four-byte payload.
4458        if pkt.data_type() == crate::DataType::ManagedVariableRequest {
4459            discovery::decode_managed_variable_request(&pkt)?;
4460            return Ok(());
4461        }
4462        let small_topology_scratch = if pkt.data_type() == crate::DataType::DiscoveryTopology
4463            && pkt.payload().len() <= 128
4464        {
4465            let st = self.state.lock();
4466            let sender = pkt
4467                .sender()
4468                .strip_prefix("@addr:")
4469                .and_then(|value| value.parse::<u32>().ok())
4470                .and_then(|address| Self::hostname_for_wire_address_borrowed_locked(&st, address))
4471                .unwrap_or(pkt.sender());
4472            st.discovery_routes.get(&src).and_then(|route| {
4473                let peer = route.announcers.get(sender)?;
4474                let mut shallow = discovery::CloneScratch::default();
4475                shallow.buffer(route.reachable.len().saturating_mul(4));
4476                shallow.strings(&route.reachable_timesync_sources);
4477                for (name, value) in &route.announcers {
4478                    shallow.buffer(name.len());
4479                    shallow.buffer(value.reachable.len().saturating_mul(4));
4480                    shallow.buffer(value.advertised_reachable.len().saturating_mul(4));
4481                    shallow.strings(&value.reachable_timesync_sources);
4482                    shallow.strings(&value.advertised_reachable_timesync_sources);
4483                }
4484                let entry =
4485                    core::mem::size_of::<String>() + core::mem::size_of::<DiscoverySenderState>();
4486                shallow.bytes = shallow
4487                    .bytes
4488                    .saturating_add(route.announcers.len().saturating_mul(entry));
4489                shallow.largest = shallow
4490                    .largest
4491                    .max(entry.saturating_mul(11).saturating_add(128));
4492                let mut deep = discovery::CloneScratch::default();
4493                if discovery::topology_payload_is_incremental(pkt.payload()) {
4494                    deep.graph(&peer.topology_boards, pkt.payload().len() / 16);
4495                }
4496                Some((
4497                    2048usize
4498                        .saturating_add(shallow.bytes.saturating_mul(3))
4499                        .saturating_add(deep.bytes)
4500                        .saturating_add(pkt.payload().len().saturating_mul(4)),
4501                    shallow.largest.max(deep.largest).max(2048),
4502                ))
4503            })
4504        } else {
4505            None
4506        };
4507        let retained_summary = if small_topology_scratch.is_none()
4508            && matches!(
4509                pkt.data_type(),
4510                crate::DataType::DiscoveryAddress
4511                    | crate::DataType::DiscoveryTopology
4512                    | crate::DataType::DiscoveryAnnounce
4513                    | crate::DataType::DiscoveryTimeSyncSources
4514                    | crate::DataType::ManagedVariableRequest
4515            ) {
4516            self.state
4517                .lock()
4518                .discovery_routes
4519                .get(&src)
4520                .map_or(0, RelayInner::discovery_route_byte_cost)
4521        } else {
4522            0
4523        };
4524        let (decode_bytes, largest_block) = if let Some(scratch) = small_topology_scratch {
4525            scratch
4526        } else {
4527            let scratch = if pkt.data_type() == crate::DataType::DiscoveryAddress {
4528                4096usize.saturating_add(retained_summary)
4529            } else {
4530                8192usize.saturating_add(retained_summary)
4531            };
4532            let decode_bytes = pkt
4533                .payload()
4534                .len()
4535                .saturating_mul(4)
4536                .saturating_add(scratch);
4537            let largest_block = pkt.payload().len().saturating_mul(2).max(2048);
4538            (decode_bytes, largest_block)
4539        };
4540        while let Err(err) = crate::memory_admission::check(decode_bytes, largest_block) {
4541            if !matches!(err, TelemetryError::Io("memory pressure")) {
4542                return Err(err);
4543            }
4544            let mut st = self.state.lock();
4545            if !st.pop_lowest_packet_queue_item(256) {
4546                return Err(err);
4547            }
4548        }
4549        let now_ms = self.clock.now_ms();
4550
4551        if pkt.data_type() == crate::DataType::DiscoverySchema {
4552            let snapshot = discovery::decode_discovery_schema(&pkt)?;
4553            let mut st = self.state.lock();
4554            let budget = st.schema_growth_budget();
4555            let report = crate::config::merge_owned_schema_snapshot_with_budget(snapshot, budget)?;
4556            if report.changed() {
4557                st.fit_discovery_budget();
4558                Self::note_discovery_topology_change_locked(&mut st, now_ms);
4559            }
4560            drop(st);
4561            self.note_schema_received(src, pkt.sender());
4562            return Ok(());
4563        }
4564        if pkt.data_type() == crate::DataType::DiscoveryLinkCapabilities {
4565            let _ = discovery::decode_discovery_link_capabilities(&pkt)?;
4566            return Ok(());
4567        }
4568        let mut st = self.state.lock();
4569        if pkt.data_type() == crate::DataType::DiscoveryLeave {
4570            let leaving = pkt.sender();
4571            let before = st.discovery_routes.clone();
4572            for route in st.discovery_routes.values_mut() {
4573                route.announcers.remove(leaving);
4574                for sender_state in route.announcers.values_mut() {
4575                    Arc::make_mut(&mut sender_state.topology_boards)
4576                        .retain(|board| board.sender_id != leaving);
4577                    for board in Arc::make_mut(&mut sender_state.topology_boards).iter_mut() {
4578                        board.connections.retain(|peer| peer != leaving);
4579                    }
4580                    Self::refresh_sender_topology_state(sender_state);
4581                }
4582                Self::recompute_discovery_side_state(route);
4583            }
4584            st.discovery_routes
4585                .retain(|_, route| !route.announcers.is_empty());
4586            if st.discovery_routes != before {
4587                Self::note_discovery_topology_change_locked(&mut st, now_ms);
4588            }
4589            let _ = Self::prune_discovery_routes_locked(&mut st, now_ms);
4590            self.reconcile_end_to_end_acked_destinations_locked(&mut st);
4591            return Ok(());
4592        }
4593        let address_ad = if pkt.data_type() == crate::DataType::DiscoveryAddress {
4594            Some(discovery::decode_discovery_address(&pkt)?)
4595        } else {
4596            None
4597        };
4598        let mut topology_ad = if pkt.data_type() == crate::DataType::DiscoveryTopology {
4599            Some(discovery::decode_discovery_topology_update(&pkt)?)
4600        } else {
4601            None
4602        };
4603        let announcer_id = if let Some(ad) = address_ad.as_ref() {
4604            ad.hostname.clone()
4605        } else {
4606            let canonical = Self::canonical_sender_locked(&st, pkt.sender());
4607            if canonical != pkt.sender() {
4608                canonical
4609            } else if let Some(address) = pkt
4610                .sender()
4611                .strip_prefix("@addr:")
4612                .and_then(|value| value.parse::<u32>().ok())
4613            {
4614                topology_ad
4615                    .as_ref()
4616                    .and_then(|update| {
4617                        update
4618                            .boards
4619                            .iter()
4620                            .find(|board| sender_address_u32(&board.sender_id) == address)
4621                    })
4622                    .map(|board| board.sender_id.clone())
4623                    .unwrap_or(canonical)
4624            } else {
4625                canonical
4626            }
4627        };
4628        // Parse fallible payloads and resolve aliases before moving retained state.
4629        let announced = if pkt.data_type() == crate::DataType::DiscoveryAnnounce {
4630            Some(discovery::decode_discovery_announce(&pkt)?)
4631        } else {
4632            None
4633        };
4634        let timesync_sources = if pkt.data_type() == crate::DataType::DiscoveryTimeSyncSources {
4635            Some(discovery::decode_discovery_timesync_sources(&pkt)?)
4636        } else {
4637            None
4638        };
4639        if let Some(update) = topology_ad.as_mut() {
4640            for board in &mut update.boards {
4641                board.sender_id = Self::canonical_sender_locked(&st, &board.sender_id);
4642                for peer in &mut board.connections {
4643                    *peer = Self::canonical_sender_locked(&st, peer);
4644                }
4645            }
4646        }
4647        let mut route = st.discovery_routes.get(&src).cloned().unwrap_or_default();
4648        if pkt.sender() != announcer_id {
4649            route.announcers.remove(pkt.sender());
4650        }
4651        let side_link_local_enabled = st
4652            .sides
4653            .get(src)
4654            .and_then(|entry| entry.as_ref())
4655            .map(|side_ref| side_ref.opts.link_local_enabled)
4656            .unwrap_or(false);
4657        let mut sender_state =
4658            core::mem::take(route.announcers.entry(announcer_id.clone()).or_default());
4659        let changed = match pkt.data_type() {
4660            crate::DataType::DiscoveryAddress => {
4661                let ad = address_ad.expect("decoded above");
4662                let advertised_address = ad.address;
4663                let mut reachable = ad.reachable_endpoints;
4664                if !side_link_local_enabled {
4665                    reachable.retain(|ep| !ep.is_link_local_only());
4666                }
4667                // This packet is an aggregate route summary, not a claim that
4668                // the announcing bridge owns every endpoint behind it.
4669                let changed = sender_state.advertised_address != Some(advertised_address)
4670                    || sender_state.advertised_reachable != reachable
4671                    || sender_state.advertised_reachable_timesync_sources
4672                        != ad.reachable_timesync_sources;
4673                sender_state.advertised_address = Some(advertised_address);
4674                sender_state.advertised_reachable = reachable;
4675                sender_state.advertised_reachable_timesync_sources = ad.reachable_timesync_sources;
4676                Self::refresh_sender_topology_state(&mut sender_state);
4677                changed
4678            }
4679            crate::DataType::DiscoveryAnnounce => {
4680                let mut reachable = announced.expect("parsed before moving route state");
4681                // Empty announcements are slow-link keepalives, not endpoint
4682                // withdrawals. Preserve ownership until an explicit topology
4683                // update, leave, or expiry removes it. The common receive tail
4684                // still refreshes last_seen_ms without scheduling rediscovery.
4685                if reachable.is_empty() {
4686                    false
4687                } else {
4688                    if !side_link_local_enabled {
4689                        reachable.retain(|ep| !ep.is_link_local_only());
4690                    }
4691                    let board = Self::sender_topology_board_mut(&mut sender_state, &announcer_id);
4692                    let changed = board.reachable_endpoints != reachable;
4693                    board.reachable_endpoints = reachable;
4694                    Self::refresh_sender_topology_state(&mut sender_state);
4695                    changed
4696                }
4697            }
4698            crate::DataType::DiscoveryTimeSyncSources => {
4699                let sources = timesync_sources.expect("parsed before moving route state");
4700                let board = Self::sender_topology_board_mut(&mut sender_state, &announcer_id);
4701                let changed = board.reachable_timesync_sources != sources;
4702                board.reachable_timesync_sources = sources;
4703                Self::refresh_sender_topology_state(&mut sender_state);
4704                changed
4705            }
4706            crate::DataType::DiscoveryTopology => {
4707                let mut update = topology_ad
4708                    .take()
4709                    .expect("topology packet was decoded before route selection");
4710                if !side_link_local_enabled {
4711                    for board in update.boards.iter_mut() {
4712                        board
4713                            .reachable_endpoints
4714                            .retain(|ep| !ep.is_link_local_only());
4715                    }
4716                }
4717                if !update.incremental {
4718                    sender_state.has_full_topology = true;
4719                }
4720                let changed = discovery::apply_shared_topology_update(
4721                    &mut sender_state.topology_boards,
4722                    update,
4723                );
4724                Self::refresh_sender_topology_state(&mut sender_state);
4725                changed
4726            }
4727            crate::DataType::DiscoverySchema => false,
4728            _ => false,
4729        };
4730        sender_state.last_seen_ms = now_ms;
4731        route.announcers.insert(announcer_id, sender_state);
4732        Self::recompute_discovery_side_state(&mut route);
4733        st.commit_discovery_route(src, route)?;
4734        if changed {
4735            Self::note_discovery_topology_change_locked(&mut st, now_ms);
4736        }
4737        let _ = Self::prune_discovery_routes_locked(&mut st, now_ms);
4738        self.reconcile_end_to_end_acked_destinations_locked(&mut st);
4739        Ok(())
4740    }
4741
4742    #[cfg(not(feature = "discovery"))]
4743    fn learn_discovery_item(&self, _src: RelaySideId, _data: &RelayItem) -> TelemetryResult<()> {
4744        Ok(())
4745    }
4746
4747    #[cfg(not(feature = "discovery"))]
4748    fn queue_discovery_announce(&self) -> TelemetryResult<()> {
4749        Ok(())
4750    }
4751
4752    #[cfg(not(feature = "discovery"))]
4753    fn poll_discovery_announce(&self) -> TelemetryResult<bool> {
4754        Ok(false)
4755    }
4756
4757    fn process_reliable_timeouts(&self) -> TelemetryResult<()> {
4758        let now = self.clock.now_ms();
4759        let mut requeue: Vec<(RelaySideId, crate::DataType, u32)> = Vec::new();
4760
4761        {
4762            let mut st = self.state.lock();
4763            if st.reliable_tx.is_empty() {
4764                return Ok(());
4765            }
4766
4767            for ((side, ty_u32), tx_state) in st.reliable_tx.iter_mut() {
4768                let Some(ty) = crate::DataType::try_from_u32(*ty_u32) else {
4769                    continue;
4770                };
4771                let sent_order: Vec<u32> = tx_state.sent_order.iter().copied().collect();
4772                for seq in sent_order {
4773                    let Some(sent) = tx_state.sent.get_mut(&seq) else {
4774                        continue;
4775                    };
4776                    if sent.queued
4777                        || now.wrapping_sub(sent.last_send_ms) < runtime_reliable_retransmit_ms()
4778                    {
4779                        continue;
4780                    }
4781                    if sent.partial_acked {
4782                        // Do not let a lost packet-request pin a partially
4783                        // acknowledged entry forever. Allow one interval for
4784                        // the explicit request, then return to bounded replay.
4785                        sent.partial_acked = false;
4786                        sent.last_send_ms = now;
4787                        continue;
4788                    }
4789                    if sent.retries >= runtime_reliable_max_retries() {
4790                        tx_state.sent.remove(&seq);
4791                        tx_state.sent_order.retain(|existing| *existing != seq);
4792                        continue;
4793                    }
4794                    sent.retries += 1;
4795                    requeue.push((*side, ty, seq));
4796                }
4797            }
4798        }
4799
4800        for (side, ty, seq) in requeue {
4801            self.queue_reliable_retransmit(side, ty, seq)?;
4802        }
4803
4804        Ok(())
4805    }
4806
4807    /// Compute a de-dupe hash for a QueueItem.
4808    /// Uses packet ID for Packet items, and attempts to extract packet ID from
4809    /// packed bytes. If extraction fails, hashes raw bytes as a fallback.
4810    fn get_hash(item: &RelayRxItem) -> u64 {
4811        match &item.data {
4812            RelayItem::Packet(pkt) => pkt.packet_id(),
4813            RelayItem::Packed(bytes) => {
4814                let reliable_seq = wire_format::peek_frame_info(bytes.as_ref())
4815                    .ok()
4816                    .and_then(|frame| frame.reliable)
4817                    .and_then(|hdr| {
4818                        if (hdr.flags & wire_format::RELIABLE_FLAG_ACK_ONLY) != 0 {
4819                            None
4820                        } else {
4821                            Some(hdr.seq)
4822                        }
4823                    });
4824
4825                match wire_format::packet_id_from_wire(bytes.as_ref()) {
4826                    Ok(id) => {
4827                        if let Some(seq) = reliable_seq {
4828                            hash_bytes_u64(id, &seq.to_le_bytes())
4829                        } else {
4830                            id
4831                        }
4832                    }
4833                    Err(_e) => {
4834                        // Fallback: if bytes are malformed (or compression feature mismatch),
4835                        // hash raw bytes so we can still dedupe identical network duplicates.
4836                        let h: u64 = 0x9E37_79B9_7F4A_7C15;
4837                        hash_bytes_u64(h, bytes.as_ref())
4838                    }
4839                }
4840            }
4841        }
4842    }
4843
4844    /// Compute a dedupe ID for an incoming RelayRxItem.
4845    /// Note: we intentionally do *not* include `src` so that the same
4846    /// packet coming from multiple sides is only processed once.
4847    fn is_duplicate_pkt(&self, item: &RelayRxItem) -> TelemetryResult<bool> {
4848        let id = Self::get_hash(item);
4849
4850        let mut st = self.state.lock();
4851        if st.recent_rx.contains(&id) {
4852            Ok(true)
4853        } else {
4854            st.push_recent_rx(id)?;
4855            Ok(false)
4856        }
4857    }
4858
4859    fn should_forward_duplicate_reliable_item(&self, item: &RelayRxItem) -> TelemetryResult<bool> {
4860        let (_, ty) = self.item_route_info(&item.data)?;
4861        if (!is_reliable_type(ty) && self.item_target_senders(&item.data)?.is_empty())
4862            || matches!(
4863                ty,
4864                crate::DataType::ReliableAck
4865                    | crate::DataType::ReliablePartialAck
4866                    | crate::DataType::ReliablePacketRequest
4867            )
4868        {
4869            return Ok(false);
4870        }
4871
4872        if !self.item_target_senders(&item.data)?.is_empty() {
4873            let id = match &item.data {
4874                RelayItem::Packet(pkt) => pkt.packet_id(),
4875                RelayItem::Packed(bytes) => wire_format::packet_id_from_wire(bytes)?,
4876            };
4877            let mut st = self.state.lock();
4878            return Ok(st.reliable_return_routes.get_mut(&id).is_some_and(|route| {
4879                let now = self.clock.now_ms();
4880                if now.saturating_sub(route.last_replay_ms) < runtime_reliable_retransmit_ms() {
4881                    return false;
4882                }
4883                route.last_replay_ms = now;
4884                true
4885            }));
4886        }
4887
4888        let RemoteSidePlan::Target(sides) = self.remote_side_plan(&item.data, item.src)?;
4889        let st = self.state.lock();
4890        let now_ms = self.clock.now_ms();
4891        Ok(sides
4892            .into_iter()
4893            .any(|side| self.side_has_multiple_announcers_locked(&st, side, now_ms)))
4894    }
4895
4896    /// Register a side whose TX callback consumes packed packet bytes.
4897    ///
4898    /// Returns the side id later used for ingress APIs such as `rx_packed_from_side`.
4899    /// The default options disable the relay's per-link reliable framing on this side.
4900    pub fn add_side_packed<N, F>(&self, name: N, tx: F) -> RelaySideId
4901    where
4902        N: AsRef<str>,
4903        F: Fn(&[u8]) -> TelemetryResult<()> + Send + Sync + 'static,
4904    {
4905        self.add_side_packed_with_options(name, tx, RelaySideOptions::default())
4906    }
4907
4908    /// Register a packed side with bounded-frame transport enabled.
4909    ///
4910    /// `max_frame_bytes == 0` leaves frames unbounded.
4911    pub fn add_side_packed_small_packets<N, F>(
4912        &self,
4913        name: N,
4914        tx: F,
4915        max_frame_bytes: usize,
4916    ) -> RelaySideId
4917    where
4918        N: AsRef<str>,
4919        F: Fn(&[u8]) -> TelemetryResult<()> + Send + Sync + 'static,
4920    {
4921        self.add_side_packed_with_options(
4922            name,
4923            tx,
4924            RelaySideOptions::default().with_small_packet_transport(max_frame_bytes),
4925        )
4926    }
4927
4928    /// Register a packed-output side with explicit side options.
4929    ///
4930    /// `opts.reliable_enabled` enables relay-managed per-hop ACK/retransmit behavior on this side.
4931    /// `opts.link_local_enabled` gates link-local-only forwarding and discovery use of this side.
4932    /// `ingress_enabled` and `egress_enabled` set the initial directional policy.
4933    pub fn add_side_packed_with_options<N, F>(
4934        &self,
4935        name: N,
4936        tx: F,
4937        opts: RelaySideOptions,
4938    ) -> RelaySideId
4939    where
4940        N: AsRef<str>,
4941        F: Fn(&[u8]) -> TelemetryResult<()> + Send + Sync + 'static,
4942    {
4943        let mut st = self.state.lock();
4944        let side = Some(RelaySide {
4945            name: Arc::from(name.as_ref()),
4946            tx_handler: RelayTxHandlerFn::Packed(Arc::new(tx)),
4947            opts,
4948        });
4949        let id = if let Some(id) = st.sides.iter().position(Option::is_none) {
4950            st.sides[id] = side;
4951            id
4952        } else {
4953            let id = st.sides.len();
4954            st.sides.push(side);
4955            id
4956        };
4957        st.side_runtime_stats
4958            .insert(id, Box::<SideRuntimeStatsInner>::default());
4959        st.side_transport
4960            .insert(id, Box::<SideTransportState>::default());
4961        #[cfg(feature = "discovery")]
4962        Self::note_discovery_topology_change_locked(&mut st, self.clock.now_ms());
4963        id
4964    }
4965
4966    /// Register a side whose TX callback receives decoded [`Packet`] values.
4967    ///
4968    /// Packet-output sides do not preserve the relay's packed reliable hop framing, so use a
4969    /// packed side when this hop should participate in relay-managed per-link reliability.
4970    pub fn add_side_packet<N, F>(&self, name: N, tx: F) -> RelaySideId
4971    where
4972        N: AsRef<str>,
4973        F: Fn(&Packet) -> TelemetryResult<()> + Send + Sync + 'static,
4974    {
4975        self.add_side_packet_with_options(name, tx, RelaySideOptions::default())
4976    }
4977
4978    /// Register a packet-output side with explicit side options.
4979    pub fn add_side_packet_with_options<N, F>(
4980        &self,
4981        name: N,
4982        tx: F,
4983        opts: RelaySideOptions,
4984    ) -> RelaySideId
4985    where
4986        N: AsRef<str>,
4987        F: Fn(&Packet) -> TelemetryResult<()> + Send + Sync + 'static,
4988    {
4989        let mut st = self.state.lock();
4990        let side = Some(RelaySide {
4991            name: Arc::from(name.as_ref()),
4992            tx_handler: RelayTxHandlerFn::Packet(Arc::new(tx)),
4993            opts,
4994        });
4995        let id = if let Some(id) = st.sides.iter().position(Option::is_none) {
4996            st.sides[id] = side;
4997            id
4998        } else {
4999            let id = st.sides.len();
5000            st.sides.push(side);
5001            id
5002        };
5003        st.side_runtime_stats
5004            .insert(id, Box::<SideRuntimeStatsInner>::default());
5005        st.side_transport
5006            .insert(id, Box::<SideTransportState>::default());
5007        #[cfg(feature = "discovery")]
5008        Self::note_discovery_topology_change_locked(&mut st, self.clock.now_ms());
5009        id
5010    }
5011
5012    /// Remove a side while keeping existing side IDs stable.
5013    ///
5014    /// `side` must be an id returned by one of the `add_side_*` calls. Remaining side ids are not
5015    /// renumbered.
5016    pub fn remove_side(&self, side: RelaySideId) -> TelemetryResult<()> {
5017        let now_ms = self.clock.now_ms();
5018        let mut st = self.state.lock();
5019        let slot = st.sides.get_mut(side).ok_or(TelemetryError::BadArg)?;
5020        if slot.is_none() {
5021            return Err(TelemetryError::BadArg);
5022        }
5023        *slot = None;
5024        while st.sides.last().is_some_and(Option::is_none) {
5025            st.sides.pop();
5026        }
5027        // Preserve capacity for link churn. It is bounded by the maximum number
5028        // of sides seen by this relay and is reclaimed when the relay drops.
5029        st.route_overrides
5030            .retain(|(src_side, dst_side), _| *src_side != Some(side) && *dst_side != side);
5031        st.typed_route_overrides
5032            .retain(|(src_side, _, dst_side), _| *src_side != Some(side) && *dst_side != side);
5033        st.route_weights
5034            .retain(|(src_side, dst_side), _| *src_side != Some(side) && *dst_side != side);
5035        st.route_priorities
5036            .retain(|(src_side, dst_side), _| *src_side != Some(side) && *dst_side != side);
5037        st.source_route_modes.remove(&Some(side));
5038        st.route_selection_cursors.remove(&Some(side));
5039        st.adaptive_route_stats.remove(&side);
5040        #[cfg(feature = "discovery")]
5041        st.discovery_side_throttle.remove(&side);
5042        #[cfg(all(feature = "discovery", feature = "timesync"))]
5043        st.timesync_side_throttle.remove(&side);
5044        st.side_runtime_stats.remove(&side);
5045        st.reliable_return_routes
5046            .retain(|_, route| route.side != side);
5047        st.rx_queue.retain(|queued| queued.src != side);
5048        st.tx_queue
5049            .retain(|queued| queued.dst != side && queued.src != Some(side));
5050        st.replay_queue.retain(|queued| queued.dst != side);
5051        st.reliable_tx.retain(|(side_id, _), _| *side_id != side);
5052        st.reliable_rx.retain(|(side_id, _), _| *side_id != side);
5053        #[cfg(feature = "discovery")]
5054        {
5055            st.discovery_routes.remove(&side);
5056            Self::note_discovery_topology_change_locked(&mut st, now_ms);
5057        }
5058        Ok(())
5059    }
5060
5061    /// Enable or disable ingress processing for a registered side.
5062    pub fn set_side_ingress_enabled(
5063        &self,
5064        side: RelaySideId,
5065        enabled: bool,
5066    ) -> TelemetryResult<()> {
5067        let now_ms = self.clock.now_ms();
5068        let mut st = self.state.lock();
5069        let side_ref = st
5070            .sides
5071            .get_mut(side)
5072            .and_then(|side| side.as_mut())
5073            .ok_or(TelemetryError::BadArg)?;
5074        side_ref.opts.ingress_enabled = enabled;
5075        #[cfg(feature = "discovery")]
5076        Self::note_discovery_topology_change_locked(&mut st, now_ms);
5077        Ok(())
5078    }
5079
5080    /// Enable or disable egress toward a registered side.
5081    pub fn set_side_egress_enabled(&self, side: RelaySideId, enabled: bool) -> TelemetryResult<()> {
5082        let now_ms = self.clock.now_ms();
5083        let mut st = self.state.lock();
5084        let side_ref = st
5085            .sides
5086            .get_mut(side)
5087            .and_then(|side| side.as_mut())
5088            .ok_or(TelemetryError::BadArg)?;
5089        side_ref.opts.egress_enabled = enabled;
5090        if !enabled {
5091            st.tx_queue.retain(|queued| queued.dst != side);
5092            st.replay_queue.retain(|queued| queued.dst != side);
5093        }
5094        #[cfg(feature = "discovery")]
5095        Self::note_discovery_topology_change_locked(&mut st, now_ms);
5096        Ok(())
5097    }
5098
5099    /// Set the route-selection policy for traffic originating from `src`.
5100    ///
5101    /// `src == None` targets locally-originated relay traffic such as discovery output.
5102    pub fn set_source_route_mode(
5103        &self,
5104        src: Option<RelaySideId>,
5105        mode: RouteSelectionMode,
5106    ) -> TelemetryResult<()> {
5107        let now_ms = self.clock.now_ms();
5108        let mut st = self.state.lock();
5109        if let Some(src) = src {
5110            let _ = Self::side_ref(&st, src).map_err(|_| TelemetryError::BadArg)?;
5111        }
5112        // Preserve explicit Fanout so discovery does not replace it with its
5113        // adaptive single-path default. Clearing is a separate operation.
5114        st.source_route_modes.insert(src, mode);
5115        st.route_selection_cursors.remove(&src);
5116        #[cfg(feature = "discovery")]
5117        Self::note_discovery_topology_change_locked(&mut st, now_ms);
5118        Ok(())
5119    }
5120
5121    /// Clear a source-specific route-selection override.
5122    pub fn clear_source_route_mode(&self, src: Option<RelaySideId>) -> TelemetryResult<()> {
5123        let now_ms = self.clock.now_ms();
5124        let mut st = self.state.lock();
5125        if let Some(src) = src {
5126            let _ = Self::side_ref(&st, src).map_err(|_| TelemetryError::BadArg)?;
5127        }
5128        st.source_route_modes.remove(&src);
5129        st.route_selection_cursors.remove(&src);
5130        #[cfg(feature = "discovery")]
5131        Self::note_discovery_topology_change_locked(&mut st, now_ms);
5132        Ok(())
5133    }
5134
5135    /// Set the weighted-routing weight from `src` toward `dst`.
5136    pub fn set_route_weight(
5137        &self,
5138        src: Option<RelaySideId>,
5139        dst: RelaySideId,
5140        weight: u32,
5141    ) -> TelemetryResult<()> {
5142        let now_ms = self.clock.now_ms();
5143        let mut st = self.state.lock();
5144        let _ = Self::side_ref(&st, dst).map_err(|_| TelemetryError::BadArg)?;
5145        if let Some(src) = src {
5146            let _ = Self::side_ref(&st, src).map_err(|_| TelemetryError::BadArg)?;
5147        }
5148        st.route_weights.insert((src, dst), weight);
5149        st.route_selection_cursors.remove(&src);
5150        #[cfg(feature = "discovery")]
5151        Self::note_discovery_topology_change_locked(&mut st, now_ms);
5152        Ok(())
5153    }
5154
5155    /// Clear a previously configured weighted-routing weight override.
5156    pub fn clear_route_weight(
5157        &self,
5158        src: Option<RelaySideId>,
5159        dst: RelaySideId,
5160    ) -> TelemetryResult<()> {
5161        let now_ms = self.clock.now_ms();
5162        let mut st = self.state.lock();
5163        let _ = Self::side_ref(&st, dst).map_err(|_| TelemetryError::BadArg)?;
5164        if let Some(src) = src {
5165            let _ = Self::side_ref(&st, src).map_err(|_| TelemetryError::BadArg)?;
5166        }
5167        st.route_weights.remove(&(src, dst));
5168        st.route_selection_cursors.remove(&src);
5169        #[cfg(feature = "discovery")]
5170        Self::note_discovery_topology_change_locked(&mut st, now_ms);
5171        Ok(())
5172    }
5173
5174    /// Set the failover priority from `src` toward `dst`.
5175    pub fn set_route_priority(
5176        &self,
5177        src: Option<RelaySideId>,
5178        dst: RelaySideId,
5179        priority: u32,
5180    ) -> TelemetryResult<()> {
5181        let now_ms = self.clock.now_ms();
5182        let mut st = self.state.lock();
5183        let _ = Self::side_ref(&st, dst).map_err(|_| TelemetryError::BadArg)?;
5184        if let Some(src) = src {
5185            let _ = Self::side_ref(&st, src).map_err(|_| TelemetryError::BadArg)?;
5186        }
5187        st.route_priorities.insert((src, dst), priority);
5188        #[cfg(feature = "discovery")]
5189        Self::note_discovery_topology_change_locked(&mut st, now_ms);
5190        Ok(())
5191    }
5192
5193    /// Clear a previously configured failover priority override.
5194    pub fn clear_route_priority(
5195        &self,
5196        src: Option<RelaySideId>,
5197        dst: RelaySideId,
5198    ) -> TelemetryResult<()> {
5199        let now_ms = self.clock.now_ms();
5200        let mut st = self.state.lock();
5201        let _ = Self::side_ref(&st, dst).map_err(|_| TelemetryError::BadArg)?;
5202        if let Some(src) = src {
5203            let _ = Self::side_ref(&st, src).map_err(|_| TelemetryError::BadArg)?;
5204        }
5205        st.route_priorities.remove(&(src, dst));
5206        #[cfg(feature = "discovery")]
5207        Self::note_discovery_topology_change_locked(&mut st, now_ms);
5208        Ok(())
5209    }
5210
5211    /// Allow or block routing from `src` toward `dst`.
5212    pub fn set_route(
5213        &self,
5214        src: Option<RelaySideId>,
5215        dst: RelaySideId,
5216        enabled: bool,
5217    ) -> TelemetryResult<()> {
5218        let now_ms = self.clock.now_ms();
5219        let mut st = self.state.lock();
5220        let _ = Self::side_ref(&st, dst).map_err(|_| TelemetryError::BadArg)?;
5221        if let Some(src) = src {
5222            let _ = Self::side_ref(&st, src).map_err(|_| TelemetryError::BadArg)?;
5223        }
5224        st.route_overrides.insert((src, dst), enabled);
5225        #[cfg(feature = "discovery")]
5226        Self::note_discovery_topology_change_locked(&mut st, now_ms);
5227        Ok(())
5228    }
5229
5230    /// Allow or block routing for a specific `DataType` from `src` toward `dst`.
5231    pub fn set_typed_route(
5232        &self,
5233        src: Option<RelaySideId>,
5234        ty: crate::DataType,
5235        dst: RelaySideId,
5236        enabled: bool,
5237    ) -> TelemetryResult<()> {
5238        let now_ms = self.clock.now_ms();
5239        let mut st = self.state.lock();
5240        let _ = Self::side_ref(&st, dst).map_err(|_| TelemetryError::BadArg)?;
5241        if let Some(src) = src {
5242            let _ = Self::side_ref(&st, src).map_err(|_| TelemetryError::BadArg)?;
5243        }
5244        st.typed_route_overrides
5245            .insert((src, ty.as_u32(), dst), enabled);
5246        #[cfg(feature = "discovery")]
5247        Self::note_discovery_topology_change_locked(&mut st, now_ms);
5248        Ok(())
5249    }
5250
5251    /// Clear a typed route override for the `(src, ty, dst)` triple.
5252    pub fn clear_typed_route(
5253        &self,
5254        src: Option<RelaySideId>,
5255        ty: crate::DataType,
5256        dst: RelaySideId,
5257    ) -> TelemetryResult<()> {
5258        let now_ms = self.clock.now_ms();
5259        let mut st = self.state.lock();
5260        let _ = Self::side_ref(&st, dst).map_err(|_| TelemetryError::BadArg)?;
5261        if let Some(src) = src {
5262            let _ = Self::side_ref(&st, src).map_err(|_| TelemetryError::BadArg)?;
5263        }
5264        st.typed_route_overrides.remove(&(src, ty.as_u32(), dst));
5265        #[cfg(feature = "discovery")]
5266        Self::note_discovery_topology_change_locked(&mut st, now_ms);
5267        Ok(())
5268    }
5269
5270    /// Clear a non-typed route override so the relay falls back to default behavior.
5271    pub fn clear_route(&self, src: Option<RelaySideId>, dst: RelaySideId) -> TelemetryResult<()> {
5272        let now_ms = self.clock.now_ms();
5273        let mut st = self.state.lock();
5274        let _ = Self::side_ref(&st, dst).map_err(|_| TelemetryError::BadArg)?;
5275        if let Some(src) = src {
5276            let _ = Self::side_ref(&st, src).map_err(|_| TelemetryError::BadArg)?;
5277        }
5278        st.route_overrides.remove(&(src, dst));
5279        #[cfg(feature = "discovery")]
5280        Self::note_discovery_topology_change_locked(&mut st, now_ms);
5281        Ok(())
5282    }
5283
5284    #[cfg(feature = "discovery")]
5285    /// Queues an immediate discovery announcement for this relay.
5286    pub fn announce_discovery(&self) -> TelemetryResult<()> {
5287        self.queue_discovery_announce(true, false)
5288    }
5289
5290    #[cfg(feature = "discovery")]
5291    /// Broadcast that this relay is leaving so peers can prune topology immediately.
5292    pub fn announce_leave(&self) -> TelemetryResult<()> {
5293        let pkt = discovery::build_discovery_leave("relay", self.clock.now_ms())?;
5294        let mut st = self.state.lock();
5295        let dsts: Vec<usize> = st
5296            .sides
5297            .iter()
5298            .enumerate()
5299            .filter_map(|(idx, side)| side.as_ref().map(|_| idx))
5300            .collect();
5301        for dst in dsts {
5302            let data = RelayItem::Packet(Arc::new(pkt.clone()));
5303            let priority = Self::relay_item_priority(&data)?;
5304            st.push_tx(RelayTxItem {
5305                src: None,
5306                dst,
5307                data,
5308                priority,
5309                retry_after_ms: 0,
5310            })?;
5311        }
5312        Ok(())
5313    }
5314
5315    #[cfg(feature = "discovery")]
5316    /// Polls discovery state and queues an announce if the cadence says one is due.
5317    pub fn poll_discovery(&self) -> TelemetryResult<bool> {
5318        self.poll_discovery_announce()
5319    }
5320
5321    #[cfg(feature = "discovery")]
5322    /// Exports the relay's current discovered topology snapshot.
5323    pub fn export_topology(&self) -> TopologySnapshot {
5324        let now_ms = self.clock.now_ms();
5325        let mut st = self.state.lock();
5326        if Self::prune_discovery_routes_locked(&mut st, now_ms) {
5327            self.reconcile_end_to_end_acked_destinations_locked(&mut st);
5328            Self::note_discovery_topology_change_locked(&mut st, now_ms);
5329        }
5330        let routes = st
5331            .discovery_routes
5332            .iter()
5333            .filter_map(|(&side_id, route)| {
5334                let side = st.sides.get(side_id).and_then(|side| side.as_ref())?;
5335                let announcers = route
5336                    .announcers
5337                    .iter()
5338                    .map(|(sender_id, sender_state)| TopologyAnnouncerRoute {
5339                        sender_id: sender_id.clone(),
5340                        reachable_endpoints: sender_state
5341                            .reachable
5342                            .iter()
5343                            .copied()
5344                            .filter(|ep| !discovery::is_router_control_endpoint(*ep))
5345                            .collect(),
5346                        reachable_network_variables: Vec::new(),
5347                        reachable_timesync_sources: sender_state.reachable_timesync_sources.clone(),
5348                        routers: sender_state.topology_boards.as_ref().clone(),
5349                        last_seen_ms: sender_state.last_seen_ms,
5350                        age_ms: now_ms.saturating_sub(sender_state.last_seen_ms),
5351                    })
5352                    .collect();
5353                Some(TopologySideRoute {
5354                    side_id,
5355                    side_name: side.name.to_string(),
5356                    reachable_endpoints: route
5357                        .reachable
5358                        .iter()
5359                        .copied()
5360                        .filter(|ep| !discovery::is_router_control_endpoint(*ep))
5361                        .collect(),
5362                    reachable_network_variables: Vec::new(),
5363                    reachable_timesync_sources: route.reachable_timesync_sources.clone(),
5364                    announcers,
5365                    last_seen_ms: route.last_seen_ms,
5366                    age_ms: now_ms.saturating_sub(route.last_seen_ms),
5367                })
5368            })
5369            .collect();
5370        let routers = self.advertised_discovery_topology_for_link_locked(&st, now_ms, true, None);
5371        let advertised_endpoints =
5372            self.advertised_discovery_endpoints_for_link_locked(&st, now_ms, true, None);
5373        let advertised_timesync_sources =
5374            self.advertised_discovery_timesync_sources_for_link_locked(&st, now_ms, None);
5375        let links = discovery::topology_links_from_boards(&routers);
5376        TopologySnapshot {
5377            advertised_endpoints,
5378            advertised_timesync_sources,
5379            routers,
5380            links,
5381            routes,
5382            current_announce_interval_ms: st.discovery_cadence.current_interval_ms,
5383            next_announce_ms: st.discovery_cadence.next_announce_ms,
5384        }
5385    }
5386
5387    #[cfg(feature = "discovery")]
5388    pub fn client_stats(&self, sender_id: &str) -> Option<ClientStatsSnapshot> {
5389        let now_ms = self.clock.now_ms();
5390        let st = self.state.lock();
5391        let mut side_ids = Vec::new();
5392        let mut side_names = Vec::new();
5393        let mut last_seen_ms = None::<u64>;
5394        let mut reachable_endpoints = Vec::new();
5395        let mut reachable_timesync_sources = Vec::new();
5396        let mut packets_sent = 0u64;
5397        let mut packets_received = 0u64;
5398        let mut bytes_sent = 0u64;
5399        let mut bytes_received = 0u64;
5400
5401        for (side_id, route) in &st.discovery_routes {
5402            let Some(sender_state) = route.announcers.get(sender_id) else {
5403                continue;
5404            };
5405            side_ids.push(*side_id);
5406            if let Some(side_name) = st
5407                .sides
5408                .get(*side_id)
5409                .and_then(|side| side.as_ref())
5410                .map(|side| side.name.clone())
5411            {
5412                side_names.push(side_name.to_string());
5413            }
5414            last_seen_ms = Some(last_seen_ms.unwrap_or(0).max(sender_state.last_seen_ms));
5415            reachable_endpoints.extend(sender_state.reachable.iter().copied());
5416            reachable_timesync_sources
5417                .extend(sender_state.reachable_timesync_sources.iter().cloned());
5418            if let Some(stats) = st.side_runtime_stats.get(side_id) {
5419                packets_sent = packets_sent.saturating_add(stats.tx_packets);
5420                packets_received = packets_received.saturating_add(stats.rx_packets);
5421                bytes_sent = bytes_sent.saturating_add(stats.tx_bytes);
5422                bytes_received = bytes_received.saturating_add(stats.rx_bytes);
5423            }
5424        }
5425
5426        if side_ids.is_empty() {
5427            return None;
5428        }
5429        reachable_endpoints.retain(|ep| !discovery::is_router_control_endpoint(*ep));
5430        crate::small_sort::sort(&mut reachable_endpoints);
5431        reachable_endpoints.dedup();
5432        reachable_timesync_sources.sort_unstable();
5433        reachable_timesync_sources.dedup();
5434        crate::small_sort::sort(&mut side_ids);
5435        side_ids.dedup();
5436        side_names.sort_unstable();
5437        side_names.dedup();
5438        let age_ms = last_seen_ms.map(|seen| now_ms.saturating_sub(seen));
5439        Some(ClientStatsSnapshot {
5440            sender_id: sender_id.to_string(),
5441            connected: age_ms.is_some_and(|age| age <= DISCOVERY_ROUTE_TTL_MS),
5442            side_ids,
5443            side_names,
5444            last_seen_ms,
5445            age_ms,
5446            reachable_endpoints,
5447            reachable_timesync_sources,
5448            packets_sent,
5449            packets_received,
5450            bytes_sent,
5451            bytes_received,
5452        })
5453    }
5454
5455    pub fn export_runtime_stats(&self) -> RuntimeStatsSnapshot {
5456        let now_ms = self.clock.now_ms();
5457        let st = self.state.lock();
5458
5459        let mut sides = Vec::new();
5460        for (side_id, side) in st.sides.iter().enumerate() {
5461            let Some(side) = side.as_ref() else { continue };
5462            let empty_stats = SideRuntimeStatsInner::default();
5463            let stats = st
5464                .side_runtime_stats
5465                .get(&side_id)
5466                .map(Box::as_ref)
5467                .unwrap_or(&empty_stats);
5468            let adaptive = st
5469                .adaptive_route_stats
5470                .get(&side_id)
5471                .cloned()
5472                .unwrap_or_default()
5473                .snapshot(now_ms, true);
5474            let (tx_template_count, rx_template_count) = st
5475                .side_transport
5476                .get(&side_id)
5477                .map(|state| (state.tx_template_count(), state.rx_template_count()))
5478                .unwrap_or((0, 0));
5479            let mut data_types: Vec<RuntimeTypeStats> = stats
5480                .data_types
5481                .iter()
5482                .map(|(&ty, item)| {
5483                    let item = item.counters();
5484                    RuntimeTypeStats {
5485                        data_type: crate::DataType(ty),
5486                        tx_packets: item.tx_packets,
5487                        tx_bytes: item.tx_bytes,
5488                        rx_packets: item.rx_packets,
5489                        rx_bytes: item.rx_bytes,
5490                        relayed_tx_packets: item.relayed_tx_packets,
5491                        relayed_tx_bytes: item.relayed_tx_bytes,
5492                        relayed_rx_packets: item.relayed_rx_packets,
5493                        relayed_rx_bytes: item.relayed_rx_bytes,
5494                        tx_retries: item.tx_retries,
5495                        handler_failures: item.handler_failures,
5496                    }
5497                })
5498                .collect();
5499            data_types.sort_unstable_by_key(|item| item.data_type.as_u32());
5500            sides.push(RuntimeSideStats {
5501                side_id,
5502                side_name: side.name.to_string(),
5503                reliable_enabled: side.opts.reliable_enabled,
5504                link_local_enabled: side.opts.link_local_enabled,
5505                header_template_enabled: side.opts.header_template_enabled,
5506                max_frame_bytes: side.opts.max_frame_bytes,
5507                compact_header_target_bytes: side.opts.compact_header_target_bytes,
5508                side_transport_profile: side.opts.effective_transport_profile().as_str(),
5509                ingress_enabled: side.opts.ingress_enabled,
5510                egress_enabled: side.opts.egress_enabled,
5511                tx_packets: stats.tx_packets,
5512                tx_bytes: stats.tx_bytes,
5513                rx_packets: stats.rx_packets,
5514                rx_bytes: stats.rx_bytes,
5515                relayed_tx_packets: stats.relayed_tx_packets,
5516                relayed_tx_bytes: stats.relayed_tx_bytes,
5517                relayed_rx_packets: stats.relayed_rx_packets,
5518                relayed_rx_bytes: stats.relayed_rx_bytes,
5519                local_delivery_packets: 0,
5520                tx_retries: stats.tx_retries,
5521                tx_handler_failures: stats.tx_handler_failures,
5522                local_handler_failures: 0,
5523                total_handler_retries: stats.total_handler_retries,
5524                side_transport_full_frames: stats.side_transport_full_frames,
5525                side_transport_compact_frames: stats.side_transport_compact_frames,
5526                side_transport_compact_delta_frames: stats.side_transport_compact_delta_frames,
5527                side_transport_compact_omitted_timestamp_frames: stats
5528                    .side_transport_compact_omitted_timestamp_frames,
5529                side_transport_chunk_frames: stats.side_transport_chunk_frames,
5530                side_transport_chunk_reclaims: st
5531                    .side_transport
5532                    .get(&side_id)
5533                    .map_or(0, |state| u64::from(state.rx_chunk_reclaims)),
5534                side_transport_raw_bytes: stats.side_transport_raw_bytes,
5535                side_transport_wire_bytes: stats.side_transport_wire_bytes,
5536                side_transport_bytes_saved: stats.side_transport_bytes_saved,
5537                side_transport_min_compact_overhead_bytes: stats
5538                    .side_transport_min_compact_overhead_bytes,
5539                side_transport_max_compact_overhead_bytes: stats
5540                    .side_transport_max_compact_overhead_bytes,
5541                side_transport_compact_target_misses: stats.side_transport_compact_target_misses,
5542                side_transport_template_evictions: stats.side_transport_template_evictions,
5543                side_transport_tx_template_count: tx_template_count,
5544                side_transport_rx_template_count: rx_template_count,
5545                max_side_transport_templates: side.opts.max_side_transport_templates,
5546                adaptive,
5547                data_types,
5548            });
5549        }
5550
5551        let mut route_modes: Vec<RouteModeStats> = st
5552            .route_selection_cursors
5553            .iter()
5554            .map(|(src, cursor)| RouteModeStats {
5555                src_side_id: *src,
5556                selection_mode: st.source_route_modes.get(src).copied(),
5557                cursor: *cursor,
5558            })
5559            .collect();
5560        for src in st.source_route_modes.keys() {
5561            if !route_modes.iter().any(|mode| mode.src_side_id == *src) {
5562                route_modes.push(RouteModeStats {
5563                    src_side_id: *src,
5564                    selection_mode: st.source_route_modes.get(src).copied(),
5565                    cursor: 0,
5566                });
5567            }
5568        }
5569        route_modes.sort_unstable_by_key(|mode| mode.src_side_id.unwrap_or(usize::MAX));
5570
5571        let mut route_overrides: Vec<RouteOverrideStats> = st
5572            .route_overrides
5573            .iter()
5574            .map(|((src, dst), enabled)| RouteOverrideStats {
5575                src_side_id: *src,
5576                dst_side_id: *dst,
5577                enabled: *enabled,
5578            })
5579            .collect();
5580        route_overrides.sort_unstable_by_key(|item| {
5581            (item.src_side_id.unwrap_or(usize::MAX), item.dst_side_id)
5582        });
5583
5584        let mut typed_route_overrides: Vec<TypedRouteOverrideStats> = st
5585            .typed_route_overrides
5586            .iter()
5587            .map(|((src, ty, dst), enabled)| TypedRouteOverrideStats {
5588                src_side_id: *src,
5589                data_type: crate::DataType(*ty),
5590                dst_side_id: *dst,
5591                enabled: *enabled,
5592            })
5593            .collect();
5594        typed_route_overrides.sort_unstable_by_key(|item| {
5595            (
5596                item.src_side_id.unwrap_or(usize::MAX),
5597                item.data_type.as_u32(),
5598                item.dst_side_id,
5599            )
5600        });
5601
5602        let mut route_weights: Vec<RouteWeightStats> = st
5603            .route_weights
5604            .iter()
5605            .map(|((src, dst), weight)| RouteWeightStats {
5606                src_side_id: *src,
5607                dst_side_id: *dst,
5608                weight: *weight,
5609            })
5610            .collect();
5611        route_weights.sort_unstable_by_key(|item| {
5612            (item.src_side_id.unwrap_or(usize::MAX), item.dst_side_id)
5613        });
5614
5615        let mut route_priorities: Vec<RoutePriorityStats> = st
5616            .route_priorities
5617            .iter()
5618            .map(|((src, dst), priority)| RoutePriorityStats {
5619                src_side_id: *src,
5620                dst_side_id: *dst,
5621                priority: *priority,
5622            })
5623            .collect();
5624        route_priorities.sort_unstable_by_key(|item| {
5625            (item.src_side_id.unwrap_or(usize::MAX), item.dst_side_id)
5626        });
5627
5628        #[cfg(feature = "discovery")]
5629        let discovery = DiscoveryRuntimeStats {
5630            route_count: st.discovery_routes.len(),
5631            announcer_count: st
5632                .discovery_routes
5633                .values()
5634                .map(|route| route.announcers.len())
5635                .sum(),
5636            current_announce_interval_ms: Some(st.discovery_cadence.current_interval_ms),
5637            next_announce_ms: Some(st.discovery_cadence.next_announce_ms),
5638        };
5639        #[cfg(not(feature = "discovery"))]
5640        let discovery = DiscoveryRuntimeStats {
5641            route_count: 0,
5642            announcer_count: 0,
5643            current_announce_interval_ms: None,
5644            next_announce_ms: None,
5645        };
5646
5647        RuntimeStatsSnapshot {
5648            sides,
5649            route_modes,
5650            route_overrides,
5651            typed_route_overrides,
5652            route_weights,
5653            route_priorities,
5654            queues: QueueRuntimeStats {
5655                rx_len: st.rx_queue.len(),
5656                rx_bytes: st.rx_queue.bytes_used(),
5657                tx_len: st.tx_queue.len(),
5658                tx_bytes: st.tx_queue.bytes_used(),
5659                replay_len: st.replay_queue.len(),
5660                replay_bytes: st.replay_queue.bytes_used(),
5661                recent_rx_len: st.recent_rx.len(),
5662                recent_rx_bytes: st.recent_rx.bytes_used(),
5663                reliable_rx_buffered_len: st.reliable_rx_buffer_len(),
5664                reliable_rx_buffered_bytes: st.reliable_rx_buffered_bytes(),
5665                shared_queue_bytes_used: st.shared_queue_bytes_used(),
5666            },
5667            reliable: ReliableRuntimeStats {
5668                reliable_return_route_count: st.reliable_return_routes.len(),
5669                end_to_end_pending_count: 0,
5670                end_to_end_pending_destination_count: 0,
5671                end_to_end_acked_cache_count: st.end_to_end_acked_destinations.len(),
5672            },
5673            discovery,
5674            total_handler_failures: st.total_handler_failures,
5675            total_handler_retries: st.total_handler_retries,
5676        }
5677    }
5678
5679    /// Export current relay memory usage/layout as JSON for profiling.
5680    pub fn export_memory_layout_json(&self) -> String {
5681        let st = self.state.lock();
5682        #[cfg(feature = "discovery")]
5683        let discovery_bytes = st.discovery_bytes_used();
5684        #[cfg(not(feature = "discovery"))]
5685        let discovery_bytes = 0usize;
5686        let schema_bytes = crate::config::schema_bytes_used();
5687        let memory = st.memory;
5688        let mut out = String::new();
5689        let _ = core::fmt::Write::write_fmt(
5690            &mut out,
5691            format_args!(
5692                "{{\"kind\":\"relay\",\
5693                 \"shared_queue_bytes_used\":{},\"shared_queue_bytes_allocated\":{},\
5694                 \"rx_queue_bytes_used\":{},\"rx_queue_bytes_allocated\":{},\"rx_queue_len\":{},\
5695                 \"tx_queue_bytes_used\":{},\"tx_queue_bytes_allocated\":{},\"tx_queue_len\":{},\
5696                 \"replay_queue_bytes_used\":{},\"replay_queue_bytes_allocated\":{},\"replay_queue_len\":{},\
5697                 \"recent_rx_bytes_used\":{},\"recent_rx_bytes_allocated\":{},\"recent_rx_len\":{},\
5698                 \"reliable_rx_buffer_bytes_used\":{},\"reliable_rx_buffer_bytes_allocated\":{},\"reliable_rx_buffer_len\":{},\
5699                 \"discovery_bytes_used\":{},\"discovery_bytes_allocated\":{},\
5700                 \"schema_bytes_used\":{},\"schema_bytes_allocated\":{}}}",
5701                st.shared_queue_bytes_used(),
5702                memory.max_queue_budget,
5703                st.rx_queue.bytes_used(),
5704                st.rx_queue.max_bytes(),
5705                st.rx_queue.len(),
5706                st.tx_queue.bytes_used(),
5707                st.tx_queue.max_bytes(),
5708                st.tx_queue.len(),
5709                st.replay_queue.bytes_used(),
5710                st.replay_queue.max_bytes(),
5711                st.replay_queue.len(),
5712                st.recent_rx.bytes_used(),
5713                st.recent_rx.max_bytes(),
5714                st.recent_rx.len(),
5715                st.reliable_rx_buffered_bytes(),
5716                memory.max_queue_budget,
5717                st.reliable_rx_buffer_len(),
5718                discovery_bytes,
5719                memory.max_queue_budget,
5720                schema_bytes,
5721                memory.max_queue_budget,
5722            ),
5723        );
5724        out
5725    }
5726
5727    #[cfg(test)]
5728    pub(crate) fn debug_end_to_end_acked_destination_count(&self, packet_id: u64) -> Option<usize> {
5729        let st = self.state.lock();
5730        st.end_to_end_acked_destinations
5731            .get(&packet_id)
5732            .map(BTreeSet::len)
5733    }
5734
5735    #[cfg(test)]
5736    pub(crate) fn debug_end_to_end_acked_packet_count(&self) -> usize {
5737        let st = self.state.lock();
5738        st.end_to_end_acked_destinations.len()
5739    }
5740
5741    #[cfg(test)]
5742    pub(crate) fn debug_reliable_return_route_count(&self) -> usize {
5743        let st = self.state.lock();
5744        st.reliable_return_routes.len()
5745    }
5746
5747    #[cfg(test)]
5748    pub(crate) fn debug_side_storage(&self) -> (usize, usize) {
5749        let st = self.state.lock();
5750        (st.sides.len(), st.sides.capacity())
5751    }
5752
5753    /// Enqueue packed bytes that originated from `src` into the relay RX queue.
5754    ///
5755    /// Note: `Arc::from(bytes)` allocates and copies `len` bytes into a new `crate::SharedBytes`.
5756    /// This is still “fast enough” for many cases, but it is not allocation-free / ISR-safe.
5757    pub fn rx_packed_from_side(&self, src: RelaySideId, bytes: &[u8]) -> TelemetryResult<()> {
5758        self.ensure_side_ingress_enabled(src)?;
5759        let Some(bytes) = self.decode_side_transport_frame(src, bytes)? else {
5760            return Ok(());
5761        };
5762        let mut st = self.state.lock();
5763
5764        let data = RelayItem::Packed(bytes);
5765        let priority = Self::relay_item_priority(&data)?;
5766        st.push_rx(RelayRxItem {
5767            src,
5768            data,
5769            priority,
5770        })
5771    }
5772
5773    /// Enqueue a full packet that originated from `src` into the relay RX queue.
5774    ///
5775    /// The packet is wrapped in `Arc<Packet>` so fanout can clone the pointer cheaply.
5776    pub fn rx_from_side(&self, src: RelaySideId, packet: Packet) -> TelemetryResult<()> {
5777        self.ensure_side_ingress_enabled(src)?;
5778        let mut st = self.state.lock();
5779
5780        let data = RelayItem::Packet(Arc::new(packet));
5781        let priority = Self::relay_item_priority(&data)?;
5782        st.push_rx(RelayRxItem {
5783            src,
5784            data,
5785            priority,
5786        })
5787    }
5788
5789    /// Clear both RX and TX queues.
5790    pub fn clear_queues(&self) {
5791        let mut st = self.state.lock();
5792        st.rx_queue.clear();
5793        st.tx_queue.clear();
5794        st.tx_priority_burst = 0;
5795    }
5796
5797    /// Clear only RX queue.
5798    pub fn clear_rx_queue(&self) {
5799        let mut st = self.state.lock();
5800        st.rx_queue.clear();
5801    }
5802
5803    /// Clear only TX queue.
5804    pub fn clear_tx_queue(&self) {
5805        let mut st = self.state.lock();
5806        st.tx_queue.clear();
5807        st.tx_priority_burst = 0;
5808        st.replay_queue.clear();
5809    }
5810
5811    /// Internal: expand one RX item into TX items for all other sides.
5812    ///
5813    /// Fanout is cheap: the `RelayItem` is cloned (Arc bump) and reused across all destinations.
5814    fn drain_queued_receipts_before_timeouts(
5815        &self,
5816        start: u64,
5817        timeout_ms: u32,
5818    ) -> TelemetryResult<bool> {
5819        let mut did_any = false;
5820        loop {
5821            let item = {
5822                let mut st = self.state.lock();
5823                let index = st.rx_queue.iter().position(|item| {
5824                    let ty = match &item.data {
5825                        RelayItem::Packet(pkt) => Some(pkt.data_type()),
5826                        RelayItem::Packed(bytes) => wire_format::scheduling_type(bytes.as_ref()),
5827                    };
5828                    matches!(
5829                        ty,
5830                        Some(
5831                            crate::DataType::ReliableAck
5832                                | crate::DataType::ReliablePartialAck
5833                                | crate::DataType::ReliablePacketRequest
5834                        )
5835                    )
5836                });
5837                index.and_then(|index| st.rx_queue.remove_pos(index))
5838            };
5839            let Some(item) = item else {
5840                return Ok(did_any);
5841            };
5842            self.process_rx_queue_item(item)?;
5843            did_any = true;
5844            if timeout_ms != 0 && self.clock.now_ms().wrapping_sub(start) >= u64::from(timeout_ms) {
5845                return Ok(did_any);
5846            }
5847        }
5848    }
5849
5850    fn process_rx_queue_item(&self, item: RelayRxItem) -> TelemetryResult<()> {
5851        if crate::memory_admission::supports_work_kinds()
5852            && let RelayItem::Packed(bytes) = &item.data
5853        {
5854            self.admit_received_frame(item.src, bytes)?;
5855        }
5856        self.ensure_side_ingress_enabled(item.src)?;
5857        match &item.data {
5858            RelayItem::Packet(pkt) => {
5859                pkt.validate()?;
5860                #[cfg(feature = "discovery")]
5861                self.refresh_peer_liveness(item.src, sender_address_u32(pkt.sender()));
5862                let bytes = wire_format::packet_wire_size(pkt);
5863                self.note_side_rx(item.src, pkt.data_type(), bytes);
5864            }
5865            RelayItem::Packed(bytes) => {
5866                if let Ok(env) = wire_format::peek_routing_envelope(bytes.as_ref()) {
5867                    self.note_side_rx(item.src, env.ty, bytes.len());
5868                }
5869            }
5870        }
5871        match &item.data {
5872            RelayItem::Packet(pkt) => {
5873                if (is_reliable_type(pkt.data_type()) || !pkt.wire_target_senders().is_empty())
5874                    && !is_internal_control_type(pkt.data_type())
5875                {
5876                    self.note_reliable_return_route(item.src, pkt.packet_id());
5877                }
5878            }
5879            RelayItem::Packed(bytes) => {
5880                if let Ok(env) = wire_format::peek_routing_envelope(bytes.as_ref())
5881                    && (is_reliable_type(env.ty) || env.target_senders().len() != 0)
5882                    && !is_internal_control_type(env.ty)
5883                    && let Ok(packet_id) = wire_format::packet_id_from_wire(bytes.as_ref())
5884                {
5885                    self.note_reliable_return_route(item.src, packet_id);
5886                }
5887            }
5888        }
5889        let mut released_buffered: Vec<crate::SharedBytes> = Vec::new();
5890        if let RelayItem::Packed(bytes) = &item.data {
5891            let (_opts, handler_is_packed, hop_reliable_enabled) = {
5892                let st = self.state.lock();
5893                let side_ref = Self::side_ref(&st, item.src)?;
5894                let opts = side_ref.opts.clone();
5895                (
5896                    opts.clone(),
5897                    matches!(side_ref.tx_handler, RelayTxHandlerFn::Packed(_)),
5898                    opts.reliable_enabled
5899                        && !self.side_has_multiple_announcers_locked(
5900                            &st,
5901                            item.src,
5902                            self.clock.now_ms(),
5903                        ),
5904                )
5905            };
5906
5907            let frame = match wire_format::peek_frame_info(bytes.as_ref()) {
5908                Ok(frame) => frame,
5909                Err(e) => {
5910                    if matches!(e, TelemetryError::Unpack(msg) if msg == "crc32 mismatch")
5911                        && hop_reliable_enabled
5912                        && handler_is_packed
5913                        && let Ok(frame) = wire_format::peek_frame_info_unchecked(bytes.as_ref())
5914                    {
5915                        if is_reliable_type(frame.envelope.ty)
5916                            && let Some(hdr) = frame.reliable
5917                        {
5918                            let unordered = (hdr.flags & wire_format::RELIABLE_FLAG_UNORDERED) != 0;
5919                            let unsequenced =
5920                                (hdr.flags & wire_format::RELIABLE_FLAG_UNSEQUENCED) != 0;
5921
5922                            if !unsequenced {
5923                                let requested = if unordered {
5924                                    hdr.seq
5925                                } else {
5926                                    let mut st = self.state.lock();
5927                                    let rx_state = self.reliable_rx_state_mut(
5928                                        &mut st,
5929                                        item.src,
5930                                        frame.envelope.ty,
5931                                    );
5932                                    rx_state.expected_seq.min(hdr.seq)
5933                                };
5934                                self.queue_reliable_packet_request(
5935                                    item.src,
5936                                    frame.envelope.ty,
5937                                    requested,
5938                                )?;
5939                            }
5940                        }
5941                        return Ok(());
5942                    }
5943                    return Err(e);
5944                }
5945            };
5946
5947            if hop_reliable_enabled
5948                && handler_is_packed
5949                && is_reliable_type(frame.envelope.ty)
5950                && let Some(hdr) = frame.reliable
5951            {
5952                if frame.ack_only() {
5953                    self.handle_reliable_ack(item.src, frame.envelope.ty, hdr.ack);
5954                    return Ok(());
5955                }
5956                let unordered = (hdr.flags & wire_format::RELIABLE_FLAG_UNORDERED) != 0;
5957                let unsequenced = (hdr.flags & wire_format::RELIABLE_FLAG_UNSEQUENCED) != 0;
5958
5959                if !unsequenced {
5960                    if unordered {
5961                        self.queue_reliable_ack(item.src, frame.envelope.ty, hdr.seq)?;
5962                    } else {
5963                        let mut release: Vec<crate::SharedBytes> = Vec::new();
5964                        let mut last_delivered = None;
5965                        let mut ack_old = None;
5966                        let mut request_missing = None;
5967                        let mut partial_ack = None;
5968                        {
5969                            let mut st = self.state.lock();
5970                            let rx_state =
5971                                self.reliable_rx_state_mut(&mut st, item.src, frame.envelope.ty);
5972                            let expected_seq = rx_state.expected_seq;
5973                            if hdr.seq < expected_seq {
5974                                ack_old = Some(expected_seq.saturating_sub(1));
5975                            } else if hdr.seq > expected_seq {
5976                                request_missing = Some(expected_seq);
5977                                partial_ack = Some(hdr.seq);
5978                                st.buffer_reliable_rx(
5979                                    item.src,
5980                                    frame.envelope.ty,
5981                                    hdr.seq,
5982                                    bytes.clone(),
5983                                )?;
5984                            } else {
5985                                release.push(bytes.clone());
5986                                last_delivered = Some(hdr.seq);
5987                                let mut next_expected = hdr.seq.wrapping_add(1);
5988                                while let Some(buf) = rx_state.buffered.remove(&next_expected) {
5989                                    release.push(buf);
5990                                    last_delivered = Some(next_expected);
5991                                    let next = next_expected.wrapping_add(1);
5992                                    next_expected = if next == 0 { 1 } else { next };
5993                                }
5994                                rx_state.expected_seq = next_expected;
5995                            }
5996                        }
5997
5998                        if let Some(ack_seq) = ack_old {
5999                            self.queue_reliable_ack(item.src, frame.envelope.ty, ack_seq)?;
6000                            return Ok(());
6001                        }
6002                        if let Some(request_seq) = request_missing {
6003                            if let Some(partial_seq) = partial_ack {
6004                                self.queue_reliable_partial_ack(
6005                                    item.src,
6006                                    frame.envelope.ty,
6007                                    partial_seq,
6008                                )?;
6009                            }
6010                            self.queue_reliable_packet_request(
6011                                item.src,
6012                                frame.envelope.ty,
6013                                request_seq,
6014                            )?;
6015                            return Ok(());
6016                        }
6017                        if let Some(ack_seq) = last_delivered {
6018                            self.queue_reliable_ack(item.src, frame.envelope.ty, ack_seq)?;
6019                        }
6020                        released_buffered.extend(release.into_iter().skip(1));
6021                    }
6022                }
6023            }
6024        }
6025
6026        if self.is_duplicate_pkt(&item)? && !self.should_forward_duplicate_reliable_item(&item)? {
6027            // Already fanned out this packet recently; skip.
6028            return Ok(());
6029        }
6030
6031        self.dispatch_relay_rx_item(&item)?;
6032
6033        for release_bytes in released_buffered {
6034            let release_item = RelayRxItem {
6035                src: item.src,
6036                priority: Self::relay_item_priority(&RelayItem::Packed(release_bytes.clone()))?,
6037                data: RelayItem::Packed(release_bytes),
6038            };
6039            if self.is_duplicate_pkt(&release_item)?
6040                && !self.should_forward_duplicate_reliable_item(&release_item)?
6041            {
6042                continue;
6043            }
6044            self.dispatch_relay_rx_item(&release_item)?;
6045        }
6046        Ok(())
6047    }
6048
6049    fn dispatch_relay_rx_item(&self, item: &RelayRxItem) -> TelemetryResult<()> {
6050        match &item.data {
6051            RelayItem::Packet(pkt) => {
6052                if matches!(
6053                    pkt.data_type(),
6054                    crate::DataType::ReliableAck
6055                        | crate::DataType::ReliablePartialAck
6056                        | crate::DataType::ReliablePacketRequest
6057                ) {
6058                    if Self::end_to_end_ack_sender_hash(pkt).is_some()
6059                        && Self::decode_end_to_end_reliable_ack(pkt.payload()).is_ok()
6060                    {
6061                        if let Ok(packet_id) = Self::decode_end_to_end_reliable_ack(pkt.payload())
6062                            && let Some(sender_hash) = Self::end_to_end_ack_sender_hash(pkt)
6063                        {
6064                            let mut st = self.state.lock();
6065                            Self::note_end_to_end_acked_destination_locked(
6066                                &mut st,
6067                                packet_id,
6068                                sender_hash,
6069                            );
6070                        }
6071                    } else {
6072                        let vals = pkt.data_as_u32()?;
6073                        if vals.len() != 2 {
6074                            return Err(TelemetryError::Unpack("bad reliable control payload"));
6075                        }
6076                        let ty = crate::DataType::try_from_u32(vals[0])
6077                            .ok_or(TelemetryError::InvalidType)?;
6078                        let seq = vals[1];
6079                        match pkt.data_type() {
6080                            crate::DataType::ReliableAck => {
6081                                self.handle_reliable_ack(item.src, ty, seq)
6082                            }
6083                            crate::DataType::ReliablePartialAck => {
6084                                self.handle_reliable_partial_ack(item.src, ty, seq)
6085                            }
6086                            crate::DataType::ReliablePacketRequest => {
6087                                self.queue_reliable_retransmit(item.src, ty, seq)?
6088                            }
6089                            _ => {}
6090                        }
6091                        return Ok(());
6092                    }
6093                }
6094            }
6095            RelayItem::Packed(bytes) => {
6096                let env = wire_format::peek_routing_envelope(bytes.as_ref())?;
6097                if matches!(
6098                    env.ty,
6099                    crate::DataType::ReliableAck
6100                        | crate::DataType::ReliablePacketRequest
6101                        | crate::DataType::ReliablePartialAck
6102                ) {
6103                    let pkt = wire_format::unpack_packet(bytes.as_ref())?;
6104                    return self.dispatch_relay_rx_item(&RelayRxItem {
6105                        src: item.src,
6106                        data: RelayItem::Packet(Arc::new(pkt)),
6107                        priority: item.priority,
6108                    });
6109                }
6110            }
6111        }
6112
6113        let src = item.src;
6114        let data = item.data.clone();
6115        self.learn_discovery_item(src, &data)?;
6116
6117        let plan = self.remote_side_plan(&data, src)?;
6118        let mut st = self.state.lock();
6119        let RemoteSidePlan::Target(sides) = plan;
6120        for dst in sides {
6121            let priority = Self::relay_item_priority(&data)?;
6122            st.push_tx(RelayTxItem {
6123                src: Some(src),
6124                dst,
6125                data: data.clone(),
6126                priority,
6127                retry_after_ms: 0,
6128            })?;
6129        }
6130        Ok(())
6131    }
6132
6133    fn run_side_chunk_transfer<T>(
6134        &self,
6135        side: RelaySideId,
6136        key: u64,
6137        send: impl FnOnce(&mut usize) -> TelemetryResult<T>,
6138    ) -> TelemetryResult<T> {
6139        let mut next = {
6140            let mut st = self.state.lock();
6141            let transport = st
6142                .side_transport
6143                .get_mut(&side)
6144                .ok_or(TelemetryError::BadArg)?;
6145            if transport.tx_chunk_progress.is_none() {
6146                crate::memory_admission::check(
6147                    512,
6148                    core::mem::size_of::<crate::side_transport::SideTxProgress>(),
6149                )?;
6150                transport.tx_chunk_progress =
6151                    Some(Box::new(crate::side_transport::SideTxProgress::default()));
6152            }
6153            transport
6154                .tx_chunk_progress
6155                .as_mut()
6156                .expect("initialized")
6157                .begin(key, self.clock.now_ms())?
6158        };
6159        let result = send(&mut next);
6160        if let Some(progress) = self
6161            .state
6162            .lock()
6163            .side_transport
6164            .get_mut(&side)
6165            .and_then(|transport| transport.tx_chunk_progress.as_mut())
6166        {
6167            progress.finish(key, next, self.clock.now_ms(), &result);
6168        }
6169        result
6170    }
6171
6172    fn send_side_frames_resumable(
6173        &self,
6174        side: RelaySideId,
6175        mut frames: SideTransportFrames,
6176        mut send: impl FnMut(&[u8]) -> TelemetryResult<()>,
6177    ) -> TelemetryResult<()> {
6178        let bootstraps_template = frames.bootstraps_template();
6179        let result = if frames.len() == 1 {
6180            frames.send_resuming(&mut 0, send)
6181        } else {
6182            let key = frames.progress_key();
6183            self.run_side_chunk_transfer(side, key, |next| frames.send_resuming(next, &mut send))
6184        };
6185        if result.is_err() && bootstraps_template {
6186            // Encoding has already inserted the TX template, but a busy or
6187            // failed handler did not accept its bootstrap. Keep dictionary
6188            // eviction history and force a full refresh on the next attempt.
6189            // Never clear RX templates or allocate during this recovery.
6190            if let Some(state) = self.state.lock().side_transport.get_mut(&side) {
6191                state.refresh_transmit_header_templates();
6192            }
6193        }
6194        result
6195    }
6196
6197    fn split_side_transport_frame(
6198        &self,
6199        _side: RelaySideId,
6200        frame: crate::SharedBytes,
6201        max_frame_bytes: usize,
6202    ) -> TelemetryResult<SideTransportFrames> {
6203        SideTransportFrames::split(frame, max_frame_bytes, self.sender_arc().as_bytes())
6204    }
6205
6206    fn encode_side_transport_frames(
6207        &self,
6208        side: RelaySideId,
6209        opts: RelaySideOptions,
6210        raw: crate::SharedBytes,
6211    ) -> TelemetryResult<SideTransportFrames> {
6212        if !opts.header_template_enabled && opts.max_frame_bytes == 0 {
6213            return Ok(SideTransportFrames::single(raw));
6214        }
6215        let raw_len = raw.len();
6216        if opts.max_frame_bytes != 0 && raw_len > opts.max_frame_bytes {
6217            // Canonical bytes keep a partially accepted transfer independent
6218            // of dictionary churn while other traffic is serviced.
6219            let frames = self.split_side_transport_frame(side, raw, opts.max_frame_bytes)?;
6220            let mut st = self.state.lock();
6221            let stats = st.side_runtime_stats.entry(side).or_default();
6222            stats.note_side_transport_full(raw_len, frames.wire_len());
6223            stats.note_side_transport_chunks(frames.len());
6224            return Ok(frames);
6225        }
6226
6227        if opts.header_template_enabled {
6228            // Shared buses cannot retain one coherent dictionary across
6229            // independent producers. Do not build owned templates or wrap
6230            // canonical bytes when no dictionary can be used.
6231            let frame = wire_format::peek_routing_frame_info(raw.as_ref())?;
6232            let without_dictionary = opts.max_side_transport_templates == 0 || {
6233                let st = self.state.lock();
6234                self.side_has_multiple_announcers_locked(&st, side, self.clock.now_ms())
6235            };
6236            let reliable = is_reliable_type(frame.envelope.ty)
6237                || frame.reliable.is_some()
6238                || frame.envelope.target_senders().len() != 0
6239                || matches!(
6240                    frame.envelope.ty,
6241                    crate::DataType::ReliableAck
6242                        | crate::DataType::ReliablePartialAck
6243                        | crate::DataType::ReliablePacketRequest
6244                );
6245            if without_dictionary || reliable {
6246                let mut st = self.state.lock();
6247                if without_dictionary {
6248                    let removed = st
6249                        .side_transport
6250                        .get_mut(&side)
6251                        .ok_or(TelemetryError::BadArg)?
6252                        .trim_tx_templates_to(0);
6253                    for _ in 0..removed {
6254                        st.side_runtime_stats
6255                            .entry(side)
6256                            .or_default()
6257                            .note_side_transport_template_eviction();
6258                    }
6259                }
6260                st.side_runtime_stats
6261                    .entry(side)
6262                    .or_default()
6263                    .note_side_transport_full(raw_len, raw_len);
6264                return Ok(SideTransportFrames::single(raw));
6265            }
6266        }
6267
6268        #[cfg(any(feature = "std", feature = "fair-admission"))]
6269        if opts.header_template_enabled && crate::memory_admission::supports_work_kinds() {
6270            let (template, ..) = extract_side_header_template_view(raw.as_ref())?;
6271            let cached = self
6272                .state
6273                .lock()
6274                .side_transport
6275                .get(&side)
6276                .is_some_and(|transport| {
6277                    transport.tx_template_ids.contains_key(&template.hash)
6278                        && transport.tx_templates.contains_key(&template.hash)
6279                });
6280            if !cached
6281                && matches!(
6282                    crate::memory_admission::check_receive(raw.as_ref()),
6283                    Err(TelemetryError::Io("memory pressure"))
6284                )
6285            {
6286                // Compression is optional. Retain the canonical shared frame
6287                // instead of allocating a dictionary entry under pressure.
6288                // Oversized frames returned through the normal chunker above.
6289                self.state
6290                    .lock()
6291                    .side_runtime_stats
6292                    .entry(side)
6293                    .or_default()
6294                    .note_side_transport_full(raw_len, raw_len);
6295                return Ok(SideTransportFrames::single(raw));
6296            }
6297        }
6298
6299        let mut compact_payload_len = None;
6300        let mut used_compact = false;
6301        let used_timestamp_delta = false;
6302        let omitted_timestamp = false;
6303        let (template, _ty, flags, data_size, timestamp, nonce, reliable_seq_ack, payload) =
6304            extract_side_header_template_view(raw.as_ref())?;
6305        // Reliable frames never use dictionary compression. Send the native,
6306        // self-describing wire packet without allocating/retaining an unused
6307        // template on either peer. Raw packets are accepted by both decoders.
6308        if is_reliable_type(_ty)
6309            || reliable_seq_ack.is_some()
6310            || matches!(
6311                _ty,
6312                crate::DataType::ReliableAck
6313                    | crate::DataType::ReliablePartialAck
6314                    | crate::DataType::ReliablePacketRequest
6315            )
6316        {
6317            let frames = if opts.max_frame_bytes != 0 && raw_len > opts.max_frame_bytes {
6318                self.split_side_transport_frame(side, raw, opts.max_frame_bytes)?
6319            } else {
6320                SideTransportFrames::single(raw)
6321            };
6322            let wire_len = frames.wire_len();
6323            let mut st = self.state.lock();
6324            let stats = st.side_runtime_stats.entry(side).or_default();
6325            stats.note_side_transport_full(raw_len, wire_len);
6326            if frames.len() > 1 {
6327                stats.note_side_transport_chunks(frames.len());
6328            }
6329            return Ok(frames);
6330        }
6331        let (template_id, use_compact, _previous_timestamp) = {
6332            let mut st = self.state.lock();
6333            let side_state = st
6334                .side_transport
6335                .get_mut(&side)
6336                .ok_or(TelemetryError::BadArg)?;
6337            if let Some(id) = side_state.tx_template_ids.get(&template.hash).copied() {
6338                let previous = side_state.tx_last_timestamps.get(&id).copied();
6339                const FULL_REFRESH_AFTER_COMPACT_FRAMES: u8 = 8;
6340                let compact_uses = side_state.tx_compact_uses.entry(id).or_default();
6341                // Discovery must bootstrap even when a receiver lost its dictionary.
6342                #[cfg(feature = "discovery")]
6343                let discovery_refresh = discovery::is_discovery_type(_ty);
6344                #[cfg(not(feature = "discovery"))]
6345                let discovery_refresh = false;
6346                let use_compact =
6347                    *compact_uses < FULL_REFRESH_AFTER_COMPACT_FRAMES && !discovery_refresh;
6348                if use_compact {
6349                    *compact_uses = compact_uses.saturating_add(1);
6350                } else {
6351                    *compact_uses = 0;
6352                }
6353                (id, use_compact, previous)
6354            } else {
6355                let next = side_state.next_template_id.wrapping_add(1).max(1);
6356                side_state.next_template_id = next;
6357                let evicted = side_state.insert_tx_template(
6358                    template.into_owned(),
6359                    next,
6360                    opts.max_side_transport_templates,
6361                );
6362                if evicted {
6363                    st.side_runtime_stats
6364                        .entry(side)
6365                        .or_default()
6366                        .note_side_transport_template_eviction();
6367                }
6368                if let Some(side_state) = st.side_transport.get_mut(&side)
6369                    && opts.max_side_transport_templates > 0
6370                {
6371                    side_state.tx_last_timestamps.insert(next, timestamp);
6372                }
6373                (next, false, None)
6374            }
6375        };
6376        let wrapped = if use_compact {
6377            used_compact = true;
6378            compact_payload_len = Some(payload.len());
6379            // A previous transmitted frame is not a confirmed receive base.
6380            // Absolute timestamps preserve identity after loss or reordering.
6381            let timestamp_field = Some(timestamp);
6382            let mut body = Vec::with_capacity(payload.len() + 32);
6383            body.push(flags);
6384            write_uleb128_local(u64::from(template_id), &mut body);
6385            write_uleb128_local(data_size, &mut body);
6386            if let Some(timestamp_field) = timestamp_field {
6387                write_uleb128_local(timestamp_field, &mut body);
6388            }
6389            if (flags & SIDE_TRANSPORT_FLAG_PACKET_NONCE) != 0 {
6390                write_uleb128_local(u64::from(nonce), &mut body);
6391            }
6392            if let Some((seq, ack)) = reliable_seq_ack {
6393                write_uleb128_local(u64::from(seq), &mut body);
6394                write_uleb128_local(u64::from(ack), &mut body);
6395            }
6396            body.extend_from_slice(payload);
6397            {
6398                let mut st = self.state.lock();
6399                if let Some(side_state) = st.side_transport.get_mut(&side) {
6400                    side_state.tx_last_timestamps.insert(template_id, timestamp);
6401                }
6402            }
6403            let kind = if omitted_timestamp {
6404                SIDE_TRANSPORT_KIND_COMPACT_SAME_TIMESTAMP
6405            } else if used_timestamp_delta {
6406                SIDE_TRANSPORT_KIND_COMPACT_DELTA
6407            } else {
6408                SIDE_TRANSPORT_KIND_COMPACT
6409            };
6410            wrap_side_transport_frame(kind, &body)
6411        } else {
6412            let mut body = Vec::with_capacity(raw.len() + 4);
6413            write_uleb128_local(u64::from(template_id), &mut body);
6414            body.extend_from_slice(raw.as_ref());
6415            wrap_side_transport_frame(SIDE_TRANSPORT_KIND_FULL, &body)
6416        };
6417        let frames = if opts.max_frame_bytes != 0 && wrapped.len() > opts.max_frame_bytes {
6418            self.split_side_transport_frame(side, wrapped, opts.max_frame_bytes)
6419        } else {
6420            Ok(SideTransportFrames::single(wrapped))
6421        }?;
6422        let wire_len = frames.wire_len();
6423        let mut st = self.state.lock();
6424        let stats = st.side_runtime_stats.entry(side).or_default();
6425        if used_compact {
6426            let overhead = compact_payload_len
6427                .map(|payload_len| wire_len.saturating_sub(payload_len))
6428                .unwrap_or(wire_len);
6429            stats.note_side_transport_compact(
6430                raw_len,
6431                wire_len,
6432                overhead,
6433                used_timestamp_delta,
6434                omitted_timestamp,
6435            );
6436            if opts.compact_header_target_bytes != 0 && overhead > opts.compact_header_target_bytes
6437            {
6438                stats.note_side_transport_compact_target_miss();
6439            }
6440        } else {
6441            stats.note_side_transport_full(raw_len, wire_len);
6442        }
6443        if frames.len() > 1 {
6444            stats.note_side_transport_chunks(frames.len());
6445        }
6446        Ok(frames)
6447    }
6448
6449    fn expire_side_chunks(&self) {
6450        let now = self.clock.now_ms();
6451        let mut st = self.state.lock();
6452        for side in st.side_transport.values_mut() {
6453            let side = side.as_mut();
6454            let reclaimed =
6455                SideChunkAssembly::expire(&mut side.rx_chunks, &mut side.rx_chunk_bytes, now);
6456            side.rx_chunk_reclaims = side.rx_chunk_reclaims.saturating_add(reclaimed);
6457        }
6458    }
6459
6460    // A validated frame from a previously discovered direct peer proves that
6461    // its link is alive, even when allocation admission refuses its payload.
6462    // Update existing timestamps only: no allocation, schema merge or new route.
6463    #[cfg(feature = "discovery")]
6464    fn refresh_ingress_peer_liveness(&self, side: RelaySideId, bytes: &[u8]) {
6465        let Ok(frame) = wire_format::peek_routing_frame_info(bytes) else {
6466            return;
6467        };
6468        let address = frame.envelope.source_address;
6469        self.refresh_peer_liveness(side, address);
6470    }
6471
6472    #[cfg(feature = "discovery")]
6473    #[cfg_attr(not(feature = "std"), inline(never))]
6474    fn refresh_peer_liveness(&self, side: RelaySideId, address: u32) {
6475        let now = self.clock.now_ms();
6476        let mut st = self.state.lock();
6477        let Some(route) = st.discovery_routes.get_mut(&side) else {
6478            return;
6479        };
6480        let mut found = None;
6481        for (name, peer) in &mut route.announcers {
6482            if peer.advertised_address == Some(address) || sender_address_u32(name) == address {
6483                // Ambiguous numeric identities must be resolved by discovery.
6484                if found.is_some() {
6485                    return;
6486                }
6487                found = Some(peer);
6488            }
6489        }
6490        let Some(peer) = found else {
6491            return;
6492        };
6493        peer.last_seen_ms = now;
6494        route.last_seen_ms = route.last_seen_ms.max(now);
6495    }
6496
6497    /// Consume only an exact unsequenced topology retry on its ingress link.
6498    /// CRC, shape, payload length and reliable-header restrictions are checked
6499    /// before this path; it cannot bypass receipts, update a graph or allocate.
6500    #[cfg(feature = "discovery")]
6501    fn consume_known_packed_topology(
6502        &self,
6503        side: RelaySideId,
6504        bytes: &[u8],
6505    ) -> TelemetryResult<bool> {
6506        let Some((address, payload)) =
6507            wire_format::peek_plain_discovery_payload(bytes, crate::DataType::DiscoveryTopology)?
6508        else {
6509            return Ok(false);
6510        };
6511        let now = self.clock.now_ms();
6512        let mut st = self.state.lock();
6513        let Some(route) = st.discovery_routes.get_mut(&side) else {
6514            return Ok(false);
6515        };
6516        let mut found = None;
6517        for (name, peer) in &mut route.announcers {
6518            if peer.advertised_address == Some(address) || sender_address_u32(name) == address {
6519                if found.is_some() {
6520                    return Ok(false);
6521                }
6522                found = Some(peer);
6523            }
6524        }
6525        let Some(peer) = found else {
6526            return Ok(false);
6527        };
6528        let Some(full) =
6529            discovery::topology_payload_matches_retained(payload, &peer.topology_boards)
6530        else {
6531            return Ok(false);
6532        };
6533        peer.has_full_topology |= full;
6534        peer.last_seen_ms = now;
6535        route.last_seen_ms = route.last_seen_ms.max(now);
6536        Ok(true)
6537    }
6538
6539    #[cfg(all(feature = "discovery", not(feature = "std")))]
6540    fn consume_known_packed_schema(
6541        &self,
6542        side: RelaySideId,
6543        bytes: &[u8],
6544    ) -> TelemetryResult<bool> {
6545        let Some((address, payload)) =
6546            wire_format::peek_plain_discovery_payload(bytes, crate::DataType::DiscoverySchema)?
6547        else {
6548            return Ok(false);
6549        };
6550        if !discovery::discovery_schema_payload_is_fully_known(payload)? {
6551            return Ok(false);
6552        }
6553        // Discovery schemas are always sent as full refreshes. No compact
6554        // dictionary entry or second owned payload is needed for this no-op.
6555        let mut st = self.state.lock();
6556        if let Some(route) = st.discovery_routes.get_mut(&side) {
6557            for (name, peer) in &mut route.announcers {
6558                if peer.advertised_address == Some(address) || sender_address_u32(name) == address {
6559                    peer.has_schema = true;
6560                    break;
6561                }
6562            }
6563        }
6564        Ok(true)
6565    }
6566
6567    // A two-sided, fully warmed packed bridge needs no new large metadata.
6568    // Re-check before dispatch: schemas/templates/routes can change while queued.
6569    #[cfg(feature = "discovery")]
6570    fn cached_transit_ready(&self, side: RelaySideId, bytes: &[u8]) -> TelemetryResult<bool> {
6571        if bytes.len() > 128 || bytes.first().is_none_or(|flags| flags & 0x11 != 0) {
6572            return Ok(false);
6573        }
6574        let frame = wire_format::peek_routing_frame_info(bytes)?;
6575        let ty = frame.envelope.ty;
6576        if is_internal_control_type(ty)
6577            || frame.envelope.is_reliable
6578            || frame.reliable.is_some()
6579            || frame.envelope.target_senders().len() != 0
6580            // A matching inline schema is immutable metadata on the existing
6581            // shared frame. It needs no owned decoder state. Unknown or changed
6582            // shapes still require full admission and payload decoding.
6583            || frame.envelope.wire_shape.is_some_and(|shape| {
6584                shape != crate::message_meta(ty).element
6585            })
6586            || !(1..=3).contains(&frame.envelope.endpoints().take(4).count())
6587        {
6588            return Ok(false);
6589        }
6590
6591        let (template, ..) = extract_side_header_template_view(bytes)?;
6592        let st = self.state.lock();
6593        let active: usize = st.sides.iter().filter(|entry| entry.is_some()).count();
6594        if active != 2 {
6595            return Ok(false);
6596        }
6597        for (id, entry) in st
6598            .sides
6599            .iter()
6600            .enumerate()
6601            .filter_map(|(id, entry)| entry.as_ref().map(|e| (id, e)))
6602        {
6603            if entry.opts.reliable_enabled
6604                || !matches!(entry.tx_handler, RelayTxHandlerFn::Packed(_))
6605                || !st
6606                    .side_runtime_stats
6607                    .get(&id)
6608                    .is_some_and(|stats| stats.data_types.contains_key(&ty.as_u32()))
6609            {
6610                return Ok(false);
6611            }
6612            if id == side {
6613                continue;
6614            }
6615            #[cfg(feature = "discovery")]
6616            if !st.discovery_routes.get(&id).is_some_and(|route| {
6617                self.clock.now_ms().saturating_sub(route.last_seen_ms) <= DISCOVERY_ROUTE_TTL_MS
6618                    && frame
6619                        .envelope
6620                        .endpoints()
6621                        .any(|ep| route.reachable.contains(&ep))
6622            }) {
6623                return Ok(false);
6624            }
6625            if entry.opts.header_template_enabled
6626                && entry.opts.max_frame_bytes != 0
6627                && entry.opts.max_side_transport_templates != 0
6628                && !self.side_has_multiple_announcers_locked(&st, id, self.clock.now_ms())
6629                && !st.side_transport.get(&id).is_some_and(|transport| {
6630                    transport.tx_template_ids.contains_key(&template.hash)
6631                        && transport.tx_templates.contains_key(&template.hash)
6632                })
6633            {
6634                // An optional compression entry may have been evicted while
6635                // routing/statistics remain warm. Canonical forwarding needs
6636                // no replacement entry, but must fit the transport and belong
6637                // to an already discovered ingress peer.
6638                #[cfg(any(feature = "std", feature = "fair-admission"))]
6639                if (entry.opts.max_frame_bytes == 0 || bytes.len() <= entry.opts.max_frame_bytes)
6640                    && st.discovery_routes.get(&side).is_some_and(|route| {
6641                        route.announcers.iter().any(|(name, peer)| {
6642                            peer.advertised_address == Some(frame.envelope.source_address)
6643                                || sender_address_u32(name) == frame.envelope.source_address
6644                        })
6645                    })
6646                {
6647                    continue;
6648                }
6649                return Ok(false);
6650            }
6651        }
6652        Ok(true)
6653    }
6654    #[cfg(not(feature = "discovery"))]
6655    fn cached_transit_ready(&self, _side: RelaySideId, _bytes: &[u8]) -> TelemetryResult<bool> {
6656        Ok(false)
6657    }
6658    #[cfg(any(feature = "std", feature = "fair-admission"))]
6659    fn reclaim_for_receive(&self, bytes: &[u8], victim: &mut Option<(u32, u8)>) -> bool {
6660        let Some((incoming, priority)) = crate::admission_fairness::incoming(bytes) else {
6661            return false;
6662        };
6663        self.reclaim_for_incoming(incoming, priority, victim)
6664    }
6665    #[cfg(any(feature = "std", feature = "fair-admission"))]
6666    fn reclaim_for_incoming(
6667        &self,
6668        incoming: u32,
6669        priority: u8,
6670        victim: &mut Option<(u32, u8)>,
6671    ) -> bool {
6672        let mut st = self.state.lock();
6673        let (owner, priority) = if let Some(selected) = *victim {
6674            selected
6675        } else {
6676            let Some(owner) = st.competing_bulk_owner(incoming, priority) else {
6677                return false;
6678            };
6679            *victim = Some((owner, priority));
6680            (owner, priority)
6681        };
6682        st.reclaim_bulk_from(owner, priority, None)
6683    }
6684    fn admit_received_frame(&self, side: RelaySideId, bytes: &[u8]) -> TelemetryResult<()> {
6685        #[cfg(any(feature = "std", feature = "fair-admission"))]
6686        let mut fair_victim = None;
6687        if crate::memory_admission::supports_work_kinds()
6688            && self.cached_transit_ready(side, bytes)?
6689        {
6690            let result = crate::memory_admission::check_cached_transit(bytes.len());
6691            #[cfg(any(feature = "std", feature = "fair-admission"))]
6692            let result = {
6693                let mut result = result;
6694                while matches!(result, Err(TelemetryError::Io("memory pressure"))) {
6695                    if !self.reclaim_for_receive(bytes, &mut fair_victim) {
6696                        break;
6697                    }
6698                    result = crate::memory_admission::check_cached_transit(bytes.len());
6699                }
6700                result
6701            };
6702            result
6703        } else {
6704            let mut protected = None;
6705            loop {
6706                match crate::memory_admission::check_receive(bytes) {
6707                    Err(TelemetryError::Io("memory pressure")) => {
6708                        let protected = *protected.get_or_insert_with(|| {
6709                            wire_format::peek_routing_frame_info(bytes)
6710                                .ok()
6711                                .is_some_and(|frame| {
6712                                    message_priority(frame.envelope.ty) >= 200
6713                                        || crate::small_frozen_control(
6714                                            frame.envelope.ty,
6715                                            frame.envelope.target_senders().len(),
6716                                            frame.envelope.wire_shape,
6717                                        )
6718                                })
6719                        });
6720                        if !protected {
6721                            #[cfg(any(feature = "std", feature = "fair-admission"))]
6722                            if self.reclaim_for_receive(bytes, &mut fair_victim) {
6723                                continue;
6724                            }
6725                            return Err(TelemetryError::Io("memory pressure"));
6726                        }
6727                        // Queue priority cannot help before the wire copy is
6728                        // admitted. Reclaim bulk payloads, preserving control
6729                        // traffic and every learned route/schema.
6730                        let mut st = self.state.lock();
6731                        let reclaimed = st.pop_lowest_packet_queue_item(200);
6732                        if !reclaimed {
6733                            return Err(TelemetryError::Io("memory pressure"));
6734                        }
6735                    }
6736                    result => return result,
6737                }
6738            }
6739        }
6740    }
6741
6742    fn decode_side_transport_frame(
6743        &self,
6744        side: RelaySideId,
6745        bytes: &[u8],
6746    ) -> TelemetryResult<Option<crate::SharedBytes>> {
6747        self.expire_side_chunks();
6748        let Some((kind, body)) = parse_side_transport_wrapper(bytes)? else {
6749            #[cfg(feature = "discovery")]
6750            self.refresh_ingress_peer_liveness(side, bytes);
6751            #[cfg(all(feature = "discovery", not(feature = "std")))]
6752            if self.consume_known_packed_schema(side, bytes)? {
6753                return Ok(None);
6754            }
6755            #[cfg(feature = "discovery")]
6756            if self.consume_known_packed_topology(side, bytes)? {
6757                return Ok(None);
6758            }
6759            self.admit_received_frame(side, bytes)?;
6760            return Ok(Some(crate::shared_bytes::convert(Arc::<[u8]>::from(bytes))));
6761        };
6762        match kind {
6763            SIDE_TRANSPORT_KIND_FULL => {
6764                let mut off = 0usize;
6765                let template_id = u32::try_from(read_uleb128_local(body, &mut off)?)
6766                    .map_err(|_| TelemetryError::Unpack("side template id too large"))?;
6767                #[cfg(feature = "discovery")]
6768                self.refresh_ingress_peer_liveness(side, &body[off..]);
6769                #[cfg(all(feature = "discovery", not(feature = "std")))]
6770                if self.consume_known_packed_schema(side, &body[off..])? {
6771                    return Ok(None);
6772                }
6773                self.admit_received_frame(side, &body[off..])?;
6774                let raw = Arc::<[u8]>::from(&body[off..]);
6775                if let Ok((template, _, _, _, timestamp, _, _, _)) =
6776                    extract_side_header_template(raw.as_ref())
6777                {
6778                    let mut st = self.state.lock();
6779                    let max_templates = st
6780                        .sides
6781                        .get(side)
6782                        .and_then(|side| side.as_ref())
6783                        .map(|side| side.opts.max_side_transport_templates)
6784                        .unwrap_or(DEFAULT_SIDE_TRANSPORT_TEMPLATE_LIMIT);
6785                    let evicted = st.side_transport.get_mut(&side).is_some_and(|side_state| {
6786                        let evicted =
6787                            side_state.insert_rx_template(template_id, template, max_templates);
6788                        if max_templates > 0 {
6789                            side_state.rx_last_timestamps.insert(template_id, timestamp);
6790                        }
6791                        evicted
6792                    });
6793                    if evicted {
6794                        st.side_runtime_stats
6795                            .entry(side)
6796                            .or_default()
6797                            .note_side_transport_template_eviction();
6798                    }
6799                }
6800                Ok(Some(crate::shared_bytes::convert(raw)))
6801            }
6802            SIDE_TRANSPORT_KIND_COMPACT
6803            | SIDE_TRANSPORT_KIND_COMPACT_DELTA
6804            | SIDE_TRANSPORT_KIND_COMPACT_SAME_TIMESTAMP => {
6805                if body.is_empty() {
6806                    return Err(TelemetryError::Unpack("short side compact frame"));
6807                }
6808                let mut off = 1usize;
6809                let template_id = u32::try_from(read_uleb128_local(body, &mut off)?)
6810                    .map_err(|_| TelemetryError::Unpack("side template id too large"))?;
6811                let (template, timestamp_base) = {
6812                    let st = self.state.lock();
6813                    let state = st.side_transport.get(&side);
6814                    let template = state
6815                        .and_then(|state| state.rx_templates_by_id.get(&template_id))
6816                        .cloned();
6817                    let timestamp_base = if matches!(
6818                        kind,
6819                        SIDE_TRANSPORT_KIND_COMPACT_DELTA
6820                            | SIDE_TRANSPORT_KIND_COMPACT_SAME_TIMESTAMP
6821                    ) {
6822                        state
6823                            .and_then(|state| state.rx_last_timestamps.get(&template_id))
6824                            .copied()
6825                    } else {
6826                        None
6827                    };
6828                    (template, timestamp_base)
6829                };
6830                let template =
6831                    template.ok_or(TelemetryError::Unpack("unknown side compact template"))?;
6832                // The wrapper CRC and known template identify this peer even
6833                // if payload allocation is refused. No payload copy is needed.
6834                #[cfg(feature = "discovery")]
6835                if let Some(address) = template.source_address() {
6836                    self.refresh_peer_liveness(side, address);
6837                }
6838                let reconstructed_bound = body
6839                    .len()
6840                    .saturating_add(template.prefix().len())
6841                    .saturating_add(template.between().len())
6842                    .saturating_add(32);
6843                // Wrapper CRC and the retained full template are validated
6844                // before compact reconstruction. Preserve the same control
6845                // priority as canonical ingress while reclaiming only bulk.
6846                let protected = template
6847                    .data_type()
6848                    .is_some_and(|ty| message_priority(ty) >= 200);
6849                let ack = template.is_ack();
6850                #[cfg(any(feature = "std", feature = "fair-admission"))]
6851                let mut fair_incoming = None;
6852                #[cfg(any(feature = "std", feature = "fair-admission"))]
6853                let mut fair_victim = None;
6854                loop {
6855                    match crate::memory_admission::check_compact_reconstruction(
6856                        reconstructed_bound,
6857                        ack,
6858                    ) {
6859                        Err(TelemetryError::Io("memory pressure")) if protected => {
6860                            let mut st = self.state.lock();
6861                            let reclaimed = st.pop_lowest_packet_queue_item(200);
6862                            if !reclaimed {
6863                                return Err(TelemetryError::Io("memory pressure"));
6864                            }
6865                        }
6866                        Err(TelemetryError::Io("memory pressure")) => {
6867                            // Compact reports need the same fairness before
6868                            // allocation as canonical ingress. Otherwise they
6869                            // never reach the canonical admission/reclaim path.
6870                            #[cfg(any(feature = "std", feature = "fair-admission"))]
6871                            if let Some((incoming, priority)) =
6872                                *fair_incoming.get_or_insert_with(|| {
6873                                    crate::admission_fairness::compact_incoming(&template)
6874                                })
6875                                && self.reclaim_for_incoming(incoming, priority, &mut fair_victim)
6876                            {
6877                                continue;
6878                            }
6879                            return Err(TelemetryError::Io("memory pressure"));
6880                        }
6881                        result => {
6882                            result?;
6883                            break;
6884                        }
6885                    }
6886                }
6887                let timestamp_mode = match kind {
6888                    SIDE_TRANSPORT_KIND_COMPACT_DELTA => SideCompactTimestampMode::Delta,
6889                    SIDE_TRANSPORT_KIND_COMPACT_SAME_TIMESTAMP => SideCompactTimestampMode::Omitted,
6890                    _ => SideCompactTimestampMode::Absolute,
6891                };
6892                let (frame, timestamp) = reconstruct_side_compact_frame(
6893                    &template,
6894                    body[0],
6895                    &body[off..],
6896                    timestamp_mode,
6897                    timestamp_base,
6898                )?;
6899                {
6900                    let mut st = self.state.lock();
6901                    if let Some(side_state) = st.side_transport.get_mut(&side) {
6902                        side_state.rx_last_timestamps.insert(template_id, timestamp);
6903                    }
6904                }
6905                // Byte reconstruction is not decoder/forwarding admission.
6906                // Check the canonical frame before queueing, and recheck at
6907                // dispatch if routes, schemas or available memory have changed.
6908                if crate::memory_admission::supports_work_kinds() {
6909                    self.admit_received_frame(side, frame.as_ref())?;
6910                }
6911                Ok(Some(frame))
6912            }
6913            SIDE_TRANSPORT_KIND_CHUNK => {
6914                crate::memory_admission::check_receive(bytes)?;
6915                if body.len() < 8 {
6916                    return Err(TelemetryError::Unpack("short side chunk frame"));
6917                }
6918                let transfer_id = u32::from_le_bytes([body[0], body[1], body[2], body[3]]);
6919                let index = u16::from_le_bytes([body[4], body[5]]);
6920                let total = u16::from_le_bytes([body[6], body[7]]);
6921                if total == 0 || index >= total {
6922                    return Err(TelemetryError::Unpack("invalid side chunk index"));
6923                }
6924                let assembled = {
6925                    let mut st = self.state.lock();
6926                    let now_ms = self.clock.now_ms();
6927                    st.make_side_chunk_room(side, transfer_id, index, total, body.len() - 8)?;
6928                    let side_state = st
6929                        .side_transport
6930                        .get_mut(&side)
6931                        .ok_or(TelemetryError::BadArg)?;
6932                    let entry = side_state.rx_chunks.entry(transfer_id).or_default();
6933                    entry.last_seen_ms = now_ms;
6934                    entry.total = total;
6935                    let added = entry.insert_with(index, || {
6936                        crate::shared_bytes::convert(Arc::<[u8]>::from(&body[8..]))
6937                    });
6938                    side_state.rx_chunk_bytes = side_state.rx_chunk_bytes.saturating_add(added);
6939                    if entry.received.len() == usize::from(total) {
6940                        let entry = side_state
6941                            .take_chunk(transfer_id, false)
6942                            .ok_or(TelemetryError::Unpack("side chunk missing"))?;
6943                        Some(entry.assemble().inspect_err(|_| {
6944                            side_state.rx_chunk_reclaims =
6945                                side_state.rx_chunk_reclaims.saturating_add(1);
6946                        })?)
6947                    } else {
6948                        None
6949                    }
6950                };
6951                match assembled {
6952                    Some(frame) => self.decode_side_transport_frame(side, frame.as_ref()),
6953                    None => Ok(None),
6954                }
6955            }
6956            _ => Err(TelemetryError::Unpack("unknown side transport frame")),
6957        }
6958    }
6959
6960    /// Helper: call a TX handler with the best representation we have.
6961    /// - Packet handler + Packet item: direct.
6962    /// - Packed handler + Packed item: direct.
6963    /// - Packet handler + Packed item: unpack for this call.
6964    /// - Packed handler + Packet item: pack for this call.
6965    fn call_tx_handler(
6966        &self,
6967        side: RelaySideId,
6968        handler: &RelayTxHandlerFn,
6969        data: &RelayItem,
6970    ) -> TelemetryResult<()> {
6971        let opts = {
6972            let st = self.state.lock();
6973            Self::side_ref(&st, side)?.opts.clone()
6974        };
6975        let Some(_side_tx_guard) = self.try_enter_side_tx() else {
6976            return Err(TelemetryError::Io("side tx busy"));
6977        };
6978        let started_ms = self.clock.now_ms();
6979        let ty = match data {
6980            RelayItem::Packet(pkt) => pkt.data_type(),
6981            RelayItem::Packed(bytes) => wire_format::peek_routing_envelope(bytes.as_ref())?.ty,
6982        };
6983        let result = match (handler, data) {
6984            // Fast paths
6985            (RelayTxHandlerFn::Packed(f), RelayItem::Packed(bytes)) => {
6986                let frames = self.encode_side_transport_frames(side, opts, bytes.clone())?;
6987                let mut sent_bytes = 0usize;
6988                self.send_side_frames_resumable(side, frames, |frame| {
6989                    f(frame)?;
6990                    sent_bytes = sent_bytes.saturating_add(frame.len());
6991
6992                    Ok(())
6993                })?;
6994                self.record_side_tx_sample(side, sent_bytes, started_ms, self.clock.now_ms());
6995                self.note_side_tx_success(side, ty, sent_bytes, 1);
6996                return Ok(());
6997            }
6998            (RelayTxHandlerFn::Packet(f), RelayItem::Packet(pkt)) => f(pkt),
6999
7000            // Conversion paths
7001            (RelayTxHandlerFn::Packed(f), RelayItem::Packet(pkt)) => {
7002                if ty == crate::DataType::DiscoverySchema
7003                    && opts.max_frame_bytes != 0
7004                    && pkt.payload().len() > opts.max_frame_bytes
7005                {
7006                    let key = crate::packet::hash_bytes_u64(
7007                        pkt.packet_id(),
7008                        &opts.max_frame_bytes.to_le_bytes(),
7009                    );
7010                    let result = self.run_side_chunk_transfer(side, key, |next| {
7011                        crate::side_transport::send_packet_chunks(
7012                            pkt,
7013                            opts.max_frame_bytes,
7014                            self.sender_arc().as_bytes(),
7015                            next,
7016                            |frame| f(frame),
7017                        )
7018                    });
7019                    let (raw_len, wire_len, chunks) = match result {
7020                        Ok(stats) => stats,
7021                        Err(err) => {
7022                            self.note_side_tx_failure(side, ty, 1);
7023                            return Err(err);
7024                        }
7025                    };
7026                    {
7027                        let mut st = self.state.lock();
7028                        let stats = st.side_runtime_stats.entry(side).or_default();
7029                        stats.note_side_transport_full(raw_len, wire_len);
7030                        if chunks > 1 {
7031                            stats.note_side_transport_chunks(chunks);
7032                        }
7033                    }
7034                    self.record_side_tx_sample(side, wire_len, started_ms, self.clock.now_ms());
7035                    self.note_side_tx_success(side, ty, wire_len, 1);
7036                    return Ok(());
7037                }
7038                let owned = wire_format::pack_packet(pkt);
7039                let frames = self.encode_side_transport_frames(
7040                    side,
7041                    opts.clone(),
7042                    crate::shared_bytes::convert(owned),
7043                )?;
7044                let mut sent_bytes = 0usize;
7045                self.send_side_frames_resumable(side, frames, |frame| {
7046                    f(frame)?;
7047                    sent_bytes = sent_bytes.saturating_add(frame.len());
7048
7049                    Ok(())
7050                })?;
7051                self.record_side_tx_sample(side, sent_bytes, started_ms, self.clock.now_ms());
7052                self.note_side_tx_success(side, ty, sent_bytes, 1);
7053                return Ok(());
7054            }
7055            (RelayTxHandlerFn::Packet(f), RelayItem::Packed(bytes)) => {
7056                if wire_format::peek_frame_info(bytes.as_ref())
7057                    .ok()
7058                    .is_some_and(|frame| frame.ack_only())
7059                {
7060                    return Ok(());
7061                }
7062                let pkt = wire_format::unpack_packet(bytes.as_ref())?;
7063                f(&pkt)
7064            }
7065        };
7066        if result.is_ok()
7067            && let Ok(bytes) = Self::relay_item_wire_len(data)
7068        {
7069            self.record_side_tx_sample(side, bytes, started_ms, self.clock.now_ms());
7070            self.note_side_tx_success(side, ty, bytes, 1);
7071        } else if result.is_err() {
7072            self.note_side_tx_failure(side, ty, 1);
7073        }
7074        result
7075    }
7076
7077    fn adjust_reliable_for_side(
7078        &self,
7079        opts: RelaySideOptions,
7080        data: RelayItem,
7081    ) -> TelemetryResult<Option<RelayItem>> {
7082        if opts.reliable_enabled {
7083            return Ok(Some(data));
7084        }
7085
7086        match data {
7087            RelayItem::Packed(bytes) => {
7088                let frame = match wire_format::peek_routing_frame_info(bytes.as_ref()) {
7089                    Ok(frame) => frame,
7090                    Err(_) => return Ok(Some(RelayItem::Packed(bytes))),
7091                };
7092                if is_reliable_type(frame.envelope.ty)
7093                    && let Some(hdr) = frame.reliable
7094                {
7095                    if (hdr.flags & wire_format::RELIABLE_FLAG_ACK_ONLY) != 0 {
7096                        return Ok(None);
7097                    }
7098                    if (hdr.flags & wire_format::RELIABLE_FLAG_UNSEQUENCED) == 0 {
7099                        let Some(rewritten) = wire_format::rewrite_reliable_header_owned(
7100                            bytes.as_ref(),
7101                            wire_format::RELIABLE_FLAG_UNSEQUENCED,
7102                            hdr.seq,
7103                            0,
7104                        )?
7105                        else {
7106                            return Ok(Some(RelayItem::Packed(bytes)));
7107                        };
7108                        return Ok(Some(RelayItem::Packed(crate::shared_bytes::convert(
7109                            rewritten,
7110                        ))));
7111                    }
7112                }
7113                Ok(Some(RelayItem::Packed(bytes)))
7114            }
7115            RelayItem::Packet(pkt) => {
7116                if matches!(
7117                    pkt.data_type(),
7118                    crate::DataType::ReliableAck
7119                        | crate::DataType::ReliablePartialAck
7120                        | crate::DataType::ReliablePacketRequest
7121                ) {
7122                    // Hop reliability can be disabled on a physical bridge,
7123                    // but the publisher's end-to-end ACK must still cross it.
7124                    if Self::end_to_end_ack_sender_hash(&pkt).is_some()
7125                        && Self::decode_end_to_end_reliable_ack(pkt.payload()).is_ok()
7126                    {
7127                        return Ok(Some(RelayItem::Packet(pkt)));
7128                    }
7129                    return Ok(None);
7130                }
7131                Ok(Some(RelayItem::Packet(pkt)))
7132            }
7133        }
7134    }
7135
7136    /// Drain the RX queue fully, expanding to TX items.
7137    #[inline]
7138    pub fn process_rx_queue(&self) -> TelemetryResult<()> {
7139        self.process_rx_queue_with_timeout(0)
7140    }
7141
7142    /// Drain the TX queue fully, invoking per-side TX handlers.
7143    ///
7144    /// If called from inside a side TX callback, this becomes a no-op so relay TX handlers cannot
7145    /// recurse into nested queue drains on the same stack.
7146    #[inline]
7147    pub fn process_tx_queue(&self) -> TelemetryResult<()> {
7148        self.process_tx_queue_with_timeout(0)
7149    }
7150
7151    /// Drain RX then TX queues fully (one pass).
7152    #[inline]
7153    pub fn process_all_queues(&self) -> TelemetryResult<()> {
7154        self.process_all_queues_with_timeout(0)
7155    }
7156
7157    /// Process the TX queue for up to `timeout_ms` milliseconds.
7158    ///
7159    /// `timeout_ms == 0` drains fully. If called from inside a side TX callback, this becomes a
7160    /// no-op so relay TX handlers cannot recurse into nested queue drains on the same stack.
7161    pub fn process_tx_queue_with_timeout(&self, timeout_ms: u32) -> TelemetryResult<()> {
7162        self.expire_side_chunks();
7163        if self.side_tx_active() {
7164            return Ok(());
7165        }
7166        #[cfg(feature = "discovery")]
7167        {
7168            let _ = crate::memory_admission::allow_drain(self.poll_discovery())?;
7169        }
7170        let start = self.clock.now_ms();
7171        loop {
7172            let received_control = crate::memory_admission::allow_drain(
7173                self.drain_queued_receipts_before_timeouts(start, timeout_ms),
7174            )?;
7175            let control_budget_used = received_control
7176                && timeout_ms != 0
7177                && self.clock.now_ms().wrapping_sub(start) >= u64::from(timeout_ms);
7178            if !control_budget_used {
7179                crate::memory_admission::allow_drain(self.process_reliable_timeouts())?;
7180            }
7181            if !control_budget_used && self.process_replay_queue_item()? {
7182                if timeout_ms != 0 && self.clock.now_ms().wrapping_sub(start) >= timeout_ms as u64 {
7183                    break;
7184                }
7185                continue;
7186            }
7187            let Some((src, dst, handler, opts, data, priority)) = self.pop_ready_tx_item() else {
7188                break;
7189            };
7190            match self.send_tx_item(src, dst, handler, opts, data.clone()) {
7191                Ok(sent) => {
7192                    if sent
7193                        && timeout_ms != 0
7194                        && self.clock.now_ms().wrapping_sub(start) >= timeout_ms as u64
7195                    {
7196                        break;
7197                    }
7198                }
7199                Err(e) if Self::is_transient_tx_error(&e) => {
7200                    let mut st = self.state.lock();
7201                    st.push_tx(RelayTxItem {
7202                        src,
7203                        dst,
7204                        data,
7205                        priority,
7206                        retry_after_ms: self.clock.now_ms().saturating_add(1),
7207                    })?;
7208                    continue;
7209                }
7210                Err(e) => return Err(e),
7211            }
7212        }
7213        Ok(())
7214    }
7215
7216    /// Process RX queue with timeout.
7217    pub fn process_rx_queue_with_timeout(&self, timeout_ms: u32) -> TelemetryResult<()> {
7218        self.expire_side_chunks();
7219        #[cfg(feature = "discovery")]
7220        {
7221            let _ = crate::memory_admission::allow_drain(self.poll_discovery())?;
7222        }
7223        let start = self.clock.now_ms();
7224        loop {
7225            let item_opt = {
7226                let mut st = self.state.lock();
7227                st.rx_queue.pop_front()
7228            };
7229            let Some(item) = item_opt else { break };
7230            self.process_rx_queue_item(item)?;
7231
7232            if timeout_ms != 0 && self.clock.now_ms().wrapping_sub(start) >= timeout_ms as u64 {
7233                break;
7234            }
7235        }
7236        Ok(())
7237    }
7238
7239    /// Process RX and TX queues interleaved for up to `timeout_ms` milliseconds.
7240    ///
7241    /// `timeout_ms == 0` drains fully. If called from inside a side TX callback, this becomes a
7242    /// no-op so relay TX handlers cannot recurse into nested queue drains on the same stack.
7243    pub fn process_all_queues_with_timeout(&self, timeout_ms: u32) -> TelemetryResult<()> {
7244        if self.side_tx_active() {
7245            return Ok(());
7246        }
7247        #[cfg(feature = "discovery")]
7248        let _ = crate::memory_admission::allow_drain(self.poll_discovery())?;
7249        self.drain_queues_with_timeout(timeout_ms)
7250    }
7251
7252    /// Drain RX/TX work without generating periodic discovery announcements.
7253    /// Reliable retries remain active. Call `periodic` or `poll_discovery`
7254    /// separately on a regular cadence. Nested side callbacks remain a no-op.
7255    pub fn drain_queues_with_timeout(&self, timeout_ms: u32) -> TelemetryResult<()> {
7256        self.expire_side_chunks();
7257        if self.side_tx_active() {
7258            return Ok(());
7259        }
7260        let drain_fully = timeout_ms == 0;
7261        let start = if drain_fully { 0 } else { self.clock.now_ms() };
7262
7263        loop {
7264            let mut did_any;
7265            let received_control = crate::memory_admission::allow_drain(
7266                self.drain_queued_receipts_before_timeouts(start, timeout_ms),
7267            )?;
7268            did_any = received_control;
7269            let control_budget_used = received_control
7270                && timeout_ms != 0
7271                && self.clock.now_ms().wrapping_sub(start) >= u64::from(timeout_ms);
7272            if !control_budget_used {
7273                crate::memory_admission::allow_drain(self.process_reliable_timeouts())?;
7274            }
7275
7276            // One TX/RX pair is atomic for fairness under a short budget.
7277            if !control_budget_used
7278                && let Some(item) = {
7279                    let mut st = self.state.lock();
7280                    st.rx_queue.pop_front()
7281                }
7282            {
7283                self.process_rx_queue_item(item)?;
7284                did_any = true;
7285            }
7286
7287            if !control_budget_used && self.process_replay_queue_item()? {
7288                did_any = true;
7289            }
7290
7291            // Then send out TX
7292            let sent_one =
7293                if let Some((src, dst, handler, opts, data, priority)) = self.pop_ready_tx_item() {
7294                    match self.send_tx_item(src, dst, handler, opts, data.clone()) {
7295                        Ok(sent) => sent,
7296                        Err(error) if Self::is_transient_tx_error(&error) => {
7297                            self.state.lock().push_tx(RelayTxItem {
7298                                src,
7299                                dst,
7300                                data,
7301                                priority,
7302                                retry_after_ms: self.clock.now_ms().saturating_add(1),
7303                            })?;
7304                            did_any = true;
7305                            false
7306                        }
7307                        Err(error) => return Err(error),
7308                    }
7309                } else {
7310                    false
7311                };
7312
7313            if sent_one {
7314                did_any = true;
7315            }
7316
7317            if !drain_fully && self.clock.now_ms().wrapping_sub(start) >= timeout_ms as u64 {
7318                break;
7319            }
7320
7321            if !did_any {
7322                break;
7323            }
7324        }
7325
7326        Ok(())
7327    }
7328
7329    /// Runs one application-loop maintenance cycle.
7330    ///
7331    /// This polls built-in discovery when that feature is compiled in, then drains queued RX/TX
7332    /// work for up to `timeout_ms` milliseconds.
7333    pub fn periodic(&self, timeout_ms: u32) -> TelemetryResult<()> {
7334        #[cfg(feature = "discovery")]
7335        {
7336            let _ = crate::memory_admission::allow_drain(self.poll_discovery())?;
7337        }
7338
7339        self.drain_queues_with_timeout(timeout_ms)
7340    }
7341}
7342
7343#[cfg(all(test, feature = "std", feature = "discovery"))]
7344mod discovery_reclaim_tests {
7345    use super::*;
7346    use crate::DataType;
7347    use core::sync::atomic::{AtomicUsize, Ordering};
7348    #[test]
7349    fn relay_discovery_reclaims_telemetry_without_discarding_controls() {
7350        crate::tests::ensure_common_test_schema();
7351        static REFUSALS: AtomicUsize = AtomicUsize::new(0);
7352        extern "C" fn scratch_after_reclaim(additional: usize, _: usize) -> bool {
7353            additional < 2048 || REFUSALS.fetch_add(1, Ordering::SeqCst) >= 2
7354        }
7355        struct Reset;
7356        impl Drop for Reset {
7357            fn drop(&mut self) {
7358                crate::memory_admission::set_probe(None);
7359            }
7360        }
7361        let relay = Relay::new(Box::new(|| 1));
7362        let side = relay.add_side_packet("CAN", |_| Ok(()));
7363        for priority in [1, 2, 200, 254, 255] {
7364            relay
7365                .state
7366                .lock()
7367                .push_tx(RelayTxItem {
7368                    src: None,
7369                    dst: side,
7370                    data: RelayItem::Packed(crate::shared_bytes::convert(Arc::<[u8]>::from(
7371                        [0; 32],
7372                    ))),
7373                    priority,
7374                    retry_after_ms: 0,
7375                })
7376                .unwrap();
7377        }
7378        REFUSALS.store(0, Ordering::SeqCst);
7379        let _reset = Reset;
7380        crate::memory_admission::set_probe(Some(scratch_after_reclaim));
7381        relay
7382            .queue_discovery_announce_on_side(false, true, None)
7383            .unwrap();
7384        let st = relay.state.lock();
7385        assert_eq!(st.tx_queue.lowest_priority(|item| item.priority), Some(200));
7386        for priority in [200, 254] {
7387            assert_eq!(
7388                st.tx_queue
7389                    .iter()
7390                    .filter(|item| item.priority == priority)
7391                    .count(),
7392                1
7393            );
7394        }
7395        assert!(
7396            st.tx_queue
7397                .iter()
7398                .any(|item| matches!(&item.data, RelayItem::Packet(pkt)
7399            if pkt.data_type() == DataType::DiscoveryLinkCapabilities))
7400        );
7401    }
7402}
7403
7404#[cfg(all(test, feature = "std", feature = "discovery"))]
7405mod queued_schema_identity_tests {
7406    use super::*;
7407
7408    fn catalog(timestamp: u64, byte: u8) -> Packet {
7409        Packet::new(
7410            crate::DataType::DiscoverySchema,
7411            &[crate::DataEndpoint::Discovery],
7412            "SCHEMA_PEER",
7413            timestamp,
7414            crate::shared_bytes::convert(Arc::<[u8]>::from(vec![byte; 1024])),
7415        )
7416        .unwrap()
7417    }
7418
7419    #[test]
7420    fn repeated_schema_preserves_partial_transfer_but_updates_and_other_sides_do_not_coalesce() {
7421        let relay = Relay::new(Box::new(|| 0u64));
7422        let side = relay.add_side_packed("A", |_| Ok(()));
7423        let other_side = relay.add_side_packed("B", |_| Ok(()));
7424        let original = catalog(1, 42);
7425        let duplicate = catalog(2, 42);
7426        assert_ne!(original.packet_id(), duplicate.packet_id());
7427        let mut st = relay.state.lock();
7428        st.tx_queue.clear();
7429        let queued = |dst, pkt| RelayTxItem {
7430            src: None,
7431            dst,
7432            data: RelayItem::Packet(Arc::new(pkt)),
7433            priority: 255,
7434            retry_after_ms: 123,
7435        };
7436        st.push_tx(queued(side, original.clone())).unwrap();
7437        st.push_tx(queued(side, duplicate.clone())).unwrap();
7438        assert_eq!(st.tx_queue.len(), 1);
7439        let pending = st.tx_queue.iter().next().unwrap();
7440        assert_eq!(pending.retry_after_ms, 123);
7441        assert!(matches!(&pending.data, RelayItem::Packet(pkt)
7442            if pkt.packet_id() == original.packet_id()));
7443        st.push_tx(queued(other_side, duplicate)).unwrap();
7444        assert_eq!(st.tx_queue.len(), 2);
7445        let changed = catalog(3, 43);
7446        st.push_tx(queued(side, changed.clone())).unwrap();
7447        assert_eq!(st.tx_queue.len(), 2);
7448        assert!(st.tx_queue.iter().any(|queued| queued.dst == side
7449            && matches!(&queued.data,
7450            RelayItem::Packet(pkt) if pkt.packet_id() == changed.packet_id())));
7451    }
7452
7453    #[test]
7454    fn repeated_announcements_complete_one_fragment_per_dispatch() {
7455        use core::sync::atomic::{AtomicUsize, Ordering};
7456        let allowance = Arc::new(AtomicUsize::new(0));
7457        let accepted = Arc::new(std::sync::Mutex::new(Vec::<Arc<[u8]>>::new()));
7458        let available = allowance.clone();
7459        let frames = accepted.clone();
7460        let node = Relay::new(Box::new(|| 1u64));
7461        let side = node.add_side_packed_with_options(
7462            "bounded CAN",
7463            move |bytes| {
7464                if available
7465                    .try_update(Ordering::Relaxed, Ordering::Relaxed, |count| {
7466                        count.checked_sub(1)
7467                    })
7468                    .is_err()
7469                {
7470                    return Err(TelemetryError::Io("side tx busy"));
7471                }
7472                frames.lock().unwrap().push(Arc::<[u8]>::from(bytes));
7473                Ok(())
7474            },
7475            RelaySideOptions::default().with_small_packet_transport(128),
7476        );
7477        let handler = node.state.lock().sides[side]
7478            .as_ref()
7479            .unwrap()
7480            .tx_handler
7481            .clone();
7482        let mut complete = false;
7483        for timestamp in 1..100 {
7484            // Incompressible bytes ensure this really uses several chunks.
7485            let mut state = 0x517cc1b7u32;
7486            let payload: Vec<u8> = (0..3600)
7487                .map(|_| {
7488                    state ^= state << 13;
7489                    state ^= state >> 17;
7490                    state ^= state << 5;
7491                    state as u8
7492                })
7493                .collect();
7494            let packet = Packet::new(
7495                crate::DataType::DiscoverySchema,
7496                &[crate::DataEndpoint::Discovery],
7497                "SCHEMA_PEER",
7498                timestamp,
7499                crate::shared_bytes::convert(Arc::<[u8]>::from(payload)),
7500            )
7501            .unwrap();
7502            node.state
7503                .lock()
7504                .push_tx(RelayTxItem {
7505                    src: None,
7506                    dst: side,
7507                    data: RelayItem::Packet(Arc::new(packet)),
7508                    priority: 255,
7509                    retry_after_ms: 0,
7510                })
7511                .unwrap();
7512            let data = node
7513                .state
7514                .lock()
7515                .tx_queue
7516                .iter()
7517                .next()
7518                .unwrap()
7519                .data
7520                .clone();
7521            allowance.store(1, Ordering::Relaxed);
7522            match node.call_tx_handler(side, &handler, &data) {
7523                Ok(()) => {
7524                    complete = true;
7525                    break;
7526                }
7527                Err(TelemetryError::Io(_)) => {}
7528                other => panic!("unexpected result: {other:?}"),
7529            }
7530        }
7531        assert!(
7532            complete,
7533            "new announcement timestamps must not replay the prefix forever"
7534        );
7535        let received = accepted.lock().unwrap();
7536        // One transfer ID and consecutive fragment indices prove prefix reuse
7537        // did not create incomplete transfers or duplicate accepted fragments.
7538        assert!(
7539            received.len() > 1,
7540            "fixture must exercise fragmentation, not a compressed single frame"
7541        );
7542        let first = &received[0];
7543        assert_eq!(&first[..4], &[83, 68, 84, 3]);
7544        assert_eq!(
7545            u16::from_le_bytes([first[10], first[11]]) as usize,
7546            received.len()
7547        );
7548        let transfer_id = &first[4..8];
7549        for (index, frame) in received.iter().enumerate() {
7550            assert_eq!(&frame[4..8], transfer_id);
7551            assert_eq!(u16::from_le_bytes([frame[8], frame[9]]) as usize, index);
7552        }
7553    }
7554    #[test]
7555    fn schema_receipt_resolves_assigned_numeric_address_without_adding_peers() {
7556        let relay = Relay::new(Box::new(|| 0u64));
7557        let side = relay.add_side_packed("A", |_| Ok(()));
7558        let assigned = 0x1234u32;
7559        assert_ne!(sender_address_u32("SCHEMA_PEER"), assigned);
7560        {
7561            let mut st = relay.state.lock();
7562            st.discovery_routes
7563                .entry(side)
7564                .or_default()
7565                .announcers
7566                .insert(
7567                    "SCHEMA_PEER".into(),
7568                    DiscoverySenderState {
7569                        advertised_address: Some(assigned),
7570                        ..DiscoverySenderState::default()
7571                    },
7572                );
7573        }
7574        relay.note_schema_received(side, "@addr:4660");
7575        relay.note_schema_received(side, "@addr:4661");
7576        let st = relay.state.lock();
7577        let peers = &st.discovery_routes.get(&side).unwrap().announcers;
7578        assert_eq!(peers.len(), 1);
7579        assert!(peers["SCHEMA_PEER"].has_schema);
7580    }
7581}
7582
7583#[cfg(all(test, feature = "discovery", feature = "std"))]
7584fn untemplated_test_node() -> Relay {
7585    Relay::new(Box::new(|| 0))
7586}
7587#[cfg(all(test, feature = "discovery", feature = "std"))]
7588fn untemplated_test_options() -> RelaySideOptions {
7589    RelaySideOptions::default().with_small_packet_transport(64)
7590}
7591#[cfg(all(test, feature = "discovery", feature = "std"))]
7592#[allow(clippy::duplicate_mod)]
7593#[path = "tests/untemplated_transport.rs"]
7594mod untemplated_transport_tests;
7595
7596#[cfg(all(test, feature = "std"))]
7597mod frozen_control_delivery_tests {
7598    use super::*;
7599    use crate::router::{NetworkVariablePermissions, Router, RouterConfig, RouterSideOptions};
7600    use core::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
7601    use std::sync::Mutex;
7602
7603    fn control() -> Packet {
7604        let ep = crate::config::register_endpoint("FROZEN_RELAY_CONTROL", false).unwrap();
7605        let ty = crate::config::register_data_type_with_description(
7606            "FROZEN_RELAY_CONTROL",
7607            "",
7608            crate::MessageElement::Static(
7609                1,
7610                crate::MessageDataType::UInt8,
7611                crate::MessageClass::Data,
7612            ),
7613            &[ep],
7614            crate::ReliableMode::None,
7615            16,
7616        )
7617        .unwrap();
7618        Packet::new_with_wire_contract(
7619            ty,
7620            &[ep],
7621            "GS",
7622            1,
7623            7,
7624            Arc::<[u8]>::from([1]),
7625            None,
7626            Arc::from([Relay::sender_hash("FC")]),
7627        )
7628        .unwrap()
7629    }
7630
7631    #[test]
7632    fn foreign_frozen_control_retains_priority_and_full_reserved_admission() {
7633        let packet = control();
7634        let bytes = wire_format::pack_packet(&packet);
7635        let relay = Relay::new(Box::new(|| 0u64));
7636        let side = relay.add_side_packed("radio", |_| Ok(()));
7637        relay.rx_packed_from_side(side, &bytes).unwrap();
7638        relay.clear_queues();
7639        let foreign = Router::new(RouterConfig::new([]).with_sender("FOREIGN"));
7640        let foreign_side = foreign.add_side_packed("radio", |_| Ok(()));
7641        foreign
7642            .rx_packed_queue_from_side(&bytes, foreign_side)
7643            .unwrap();
7644        foreign.clear_queues();
7645        assert_eq!(
7646            Relay::relay_item_priority(&RelayItem::Packet(Arc::new(packet))).unwrap(),
7647            254
7648        );
7649        assert_eq!(
7650            Relay::relay_item_priority(&RelayItem::Packed(crate::shared_bytes::convert(
7651                bytes.clone()
7652            )))
7653            .unwrap(),
7654            254
7655        );
7656        extern "C" fn protected_only(additional: usize, largest: usize, kind: u32) -> bool {
7657            kind == crate::memory_admission::WORK_SMALL_PROTECTED
7658                && (2048..=2432).contains(&additional)
7659                && largest <= 160
7660        }
7661        struct Reset;
7662        impl Drop for Reset {
7663            fn drop(&mut self) {
7664                crate::memory_admission::set_probe_v2(None);
7665            }
7666        }
7667        let _reset = Reset;
7668        crate::memory_admission::set_probe_v2(Some(protected_only));
7669        assert_eq!(wire_format::unpack_packet(&bytes).unwrap().payload(), &[1]);
7670        relay.rx_packed_from_side(side, &bytes).unwrap();
7671        foreign
7672            .rx_packed_queue_from_side(&bytes, foreign_side)
7673            .unwrap();
7674        let mut corrupt = bytes.to_vec();
7675        *corrupt.last_mut().unwrap() ^= 1;
7676        assert!(relay.rx_packed_from_side(side, &corrupt).is_err());
7677    }
7678
7679    #[test]
7680    fn best_effort_schema_frozen_contract_retries_through_standalone_relay() {
7681        let packet = control();
7682        let bytes = wire_format::pack_packet(&packet);
7683        let now = Arc::new(AtomicU64::new(0));
7684        let clock = now.clone();
7685        let relay = Relay::new(Box::new(move || clock.load(Ordering::Relaxed)));
7686        let upstream = Arc::new(Mutex::new(Vec::<Vec<u8>>::new()));
7687        let output = upstream.clone();
7688        let radio = relay.add_side_packed_with_options(
7689            "radio",
7690            move |bytes| {
7691                output.lock().unwrap().push(bytes.to_vec());
7692                Ok(())
7693            },
7694            RelaySideOptions {
7695                header_template_enabled: false,
7696                reliable_enabled: false,
7697                ..Default::default()
7698            },
7699        );
7700        let downstream = Arc::new(Mutex::new(Vec::<Vec<u8>>::new()));
7701        let output = downstream.clone();
7702        let can = relay.add_side_packed_with_options(
7703            "CAN",
7704            move |bytes| {
7705                output.lock().unwrap().push(bytes.to_vec());
7706                Ok(())
7707            },
7708            RelaySideOptions {
7709                header_template_enabled: false,
7710                reliable_enabled: false,
7711                ..Default::default()
7712            },
7713        );
7714        relay.set_route(Some(radio), can, true).unwrap();
7715        relay.set_route(Some(can), radio, true).unwrap();
7716        let child = Router::new(RouterConfig::new([]).with_sender("FC"));
7717        child
7718            .enable_network_variable(packet.data_type(), NetworkVariablePermissions::READ_ONLY)
7719            .unwrap();
7720        let effects = Arc::new(AtomicUsize::new(0));
7721        let count = effects.clone();
7722        child
7723            .on_network_variable_update(packet.data_type(), move |_| {
7724                count.fetch_add(1, Ordering::Relaxed);
7725                Ok(())
7726            })
7727            .unwrap();
7728        let receipts = Arc::new(Mutex::new(Vec::<Vec<u8>>::new()));
7729        let output = receipts.clone();
7730        let child_side = child.add_side_packed_with_options(
7731            "CAN",
7732            move |bytes| {
7733                output.lock().unwrap().push(bytes.to_vec());
7734                Ok(())
7735            },
7736            RouterSideOptions {
7737                header_template_enabled: false,
7738                reliable_enabled: false,
7739                ..Default::default()
7740            },
7741        );
7742        relay.clear_queues();
7743        child.clear_queues();
7744        for attempt in 0..3 {
7745            now.store(
7746                u64::from(crate::config::runtime_tuning_config().reliable_retransmit_ms) * attempt,
7747                Ordering::Relaxed,
7748            );
7749            relay.rx_packed_from_side(radio, &bytes).unwrap();
7750            relay.drain_queues_with_timeout(0).unwrap();
7751            let frames = core::mem::take(&mut *downstream.lock().unwrap());
7752            let values: Vec<_> = frames
7753                .into_iter()
7754                .filter(|bytes| wire_format::peek_envelope(bytes).unwrap().ty == packet.data_type())
7755                .collect();
7756            assert!(
7757                !values.is_empty(),
7758                "frozen Off-schema retry was swallowed by relay dedupe"
7759            );
7760            if attempt == 0 {
7761                continue;
7762            } // The first data transmission is lost.
7763            for bytes in values {
7764                child.rx_packed_queue_from_side(&bytes, child_side).unwrap();
7765            }
7766            child.drain_queues_with_timeout(0).unwrap();
7767            for bytes in core::mem::take(&mut *receipts.lock().unwrap()) {
7768                relay.rx_packed_from_side(can, &bytes).unwrap();
7769            }
7770            relay.drain_queues_with_timeout(0).unwrap();
7771            assert_eq!(
7772                effects.load(Ordering::Relaxed),
7773                1,
7774                "receipt retry repeated application effect"
7775            );
7776            assert!(
7777                core::mem::take(&mut *upstream.lock().unwrap())
7778                    .iter()
7779                    .any(|bytes| {
7780                        let ack = wire_format::unpack_packet(bytes).unwrap();
7781                        ack.data_type() == crate::DataType::ReliableAck
7782                            && ack.payload() == packet.packet_id().to_le_bytes()
7783                    }),
7784                "the endpoint receipt did not return through the relay"
7785            );
7786        }
7787    }
7788}
7789
7790#[cfg(all(test, feature = "std"))]
7791mod bounded_fair_queue_tests {
7792    use super::*;
7793    #[test]
7794    fn small_canonical_reports_survive_fast_senders_fixed_ring_bursts() {
7795        let ep = crate::config::register_endpoint("RELAY_SMALL_QUEUE_DEST", false).unwrap();
7796        let ty = crate::config::register_data_type_with_description(
7797            "RELAY_SMALL_QUEUE_REPORT",
7798            "",
7799            crate::MessageElement::Static(
7800                4,
7801                crate::MessageDataType::UInt8,
7802                crate::MessageClass::Data,
7803            ),
7804            &[ep],
7805            crate::ReliableMode::None,
7806            0,
7807        )
7808        .unwrap();
7809        for tx in [false, true] {
7810            for slow_last in [false, true] {
7811                let node = Relay::new(Box::new(|| 0));
7812                let make = |sender| {
7813                    RelayItem::Packed(crate::shared_bytes::convert(wire_format::pack_packet(
7814                        &Packet::new(ty, &[ep], sender, 1, Arc::<[u8]>::from([1; 4])).unwrap(),
7815                    )))
7816                };
7817                let rx = |data| RelayRxItem {
7818                    src: 0,
7819                    data,
7820                    priority: 0,
7821                };
7822                let tx_item = |data| RelayTxItem {
7823                    src: None,
7824                    dst: 1,
7825                    data,
7826                    priority: 0,
7827                    retry_after_ms: 0,
7828                };
7829                let mut st = node.state.lock();
7830                st.memory.max_queue_budget = usize::MAX;
7831                if tx {
7832                    st.tx_queue = BoundedDeque::new(8192, 3 * size_of::<RelayTxItem>(), 1.0);
7833                } else {
7834                    st.rx_queue = BoundedDeque::new(8192, 3 * size_of::<RelayRxItem>(), 1.0);
7835                }
7836                let order = if slow_last {
7837                    ["FAST", "FAST", "SLOW"]
7838                } else {
7839                    ["SLOW", "FAST", "FAST"]
7840                };
7841                for sender in order.into_iter().chain(core::iter::repeat_n("FAST", 32)) {
7842                    if tx {
7843                        st.push_tx(tx_item(make(sender))).unwrap();
7844                    } else {
7845                        st.push_rx(rx(make(sender))).unwrap();
7846                    }
7847                }
7848                let slow = wire_format::source_address_for_sender("SLOW");
7849                let retained = if tx {
7850                    st.tx_queue.iter().any(|item| {
7851                        RelayInner::fair_bulk_owner(&item.data, item.priority) == Some(slow)
7852                    })
7853                } else {
7854                    st.rx_queue.iter().any(|item| {
7855                        RelayInner::fair_bulk_owner(&item.data, item.priority) == Some(slow)
7856                    })
7857                };
7858                assert!(
7859                    retained,
7860                    "small report was displaced: tx={tx}, slow_last={slow_last}"
7861                );
7862            }
7863        }
7864    }
7865
7866    #[test]
7867    fn slow_stream_survives_same_board_fast_stream_bursts() {
7868        let ep = crate::config::register_endpoint("RELAY_STREAM_QUEUE_DEST", false).unwrap();
7869        let ty = crate::config::register_data_type_with_description(
7870            "RELAY_STREAM_QUEUE_FAST",
7871            "",
7872            crate::MessageElement::Static(
7873                64,
7874                crate::MessageDataType::UInt8,
7875                crate::MessageClass::Data,
7876            ),
7877            &[ep],
7878            crate::ReliableMode::None,
7879            0,
7880        )
7881        .unwrap();
7882        let slow_ty = crate::config::register_data_type_with_description(
7883            "RELAY_STREAM_QUEUE_SLOW",
7884            "",
7885            crate::MessageElement::Static(
7886                64,
7887                crate::MessageDataType::UInt8,
7888                crate::MessageClass::Data,
7889            ),
7890            &[ep],
7891            crate::ReliableMode::None,
7892            0,
7893        )
7894        .unwrap();
7895        for mode in 0..3 {
7896            for tx in [false, true] {
7897                for slow_last in [false, true] {
7898                    let node = Relay::new(Box::new(|| 0));
7899                    let make = |sender| {
7900                        let packet = Packet::new(
7901                            if sender == "SLOW" { slow_ty } else { ty },
7902                            &[ep],
7903                            "SAME_BOARD",
7904                            1,
7905                            Arc::<[u8]>::from([1; 64]),
7906                        )
7907                        .unwrap();
7908                        if mode == 1 {
7909                            RelayItem::Packet(packet.into())
7910                        } else {
7911                            RelayItem::Packed(crate::shared_bytes::convert(
7912                                wire_format::pack_packet(&packet),
7913                            ))
7914                        }
7915                    };
7916                    let rx = |data| RelayRxItem {
7917                        src: 0,
7918                        data,
7919                        priority: 0,
7920                    };
7921                    let tx_item = |data| RelayTxItem {
7922                        src: None,
7923                        dst: 1,
7924                        data,
7925                        priority: 0,
7926                        retry_after_ms: 0,
7927                    };
7928                    let mut st = node.state.lock();
7929                    st.memory.max_queue_budget = usize::MAX;
7930                    if mode == 0 {
7931                        if tx {
7932                            st.tx_queue =
7933                                BoundedDeque::new(8192, 3 * size_of::<RelayTxItem>(), 1.0);
7934                        } else {
7935                            st.rx_queue =
7936                                BoundedDeque::new(8192, 3 * size_of::<RelayRxItem>(), 1.0);
7937                        }
7938                    } else if mode == 1 {
7939                        let cost = if tx {
7940                            tx_item(make("SLOW")).byte_cost()
7941                                + 2 * tx_item(make("FAST")).byte_cost()
7942                        } else {
7943                            rx(make("SLOW")).byte_cost() + 2 * rx(make("FAST")).byte_cost()
7944                        };
7945                        if tx {
7946                            st.tx_queue = BoundedDeque::new(cost, cost, 2.0);
7947                        } else {
7948                            st.rx_queue = BoundedDeque::new(cost, cost, 2.0);
7949                        }
7950                    }
7951                    let order = if slow_last {
7952                        ["FAST", "FAST", "SLOW"]
7953                    } else {
7954                        ["SLOW", "FAST", "FAST"]
7955                    };
7956                    for (index, sender) in order
7957                        .into_iter()
7958                        .chain(core::iter::repeat_n("FAST", 32))
7959                        .enumerate()
7960                    {
7961                        if mode == 2 && index == 3 {
7962                            st.memory.max_queue_budget = st.shared_queue_bytes_used();
7963                        }
7964                        if tx {
7965                            st.push_tx(tx_item(make(sender))).unwrap();
7966                        } else {
7967                            st.push_rx(rx(make(sender))).unwrap();
7968                        }
7969                    }
7970                    let is_slow = |data: &RelayItem| match data {
7971                        RelayItem::Packet(p) => p.data_type() == slow_ty,
7972                        RelayItem::Packed(b) => {
7973                            wire_format::scheduling_type(b.as_ref()) == Some(slow_ty)
7974                        }
7975                    };
7976                    let retained = if tx {
7977                        st.tx_queue.iter().any(|item| {
7978                            let data = &item.data;
7979                            is_slow(data)
7980                        })
7981                    } else {
7982                        st.rx_queue.iter().any(|item| is_slow(&item.data))
7983                    };
7984                    assert!(
7985                        retained,
7986                        "slow stream displaced: mode={mode}, tx={tx}, slow_last={slow_last}"
7987                    );
7988                }
7989            }
7990        }
7991    }
7992
7993    #[test]
7994    fn fast_producer_preserves_slow_reports_at_each_queue_boundary() {
7995        let ep = crate::config::register_endpoint("RELAY_QUEUE_FAIR_DEST", false).unwrap();
7996        let ty = crate::config::register_data_type_with_description(
7997            "RELAY_QUEUE_FAIR_REPORT",
7998            "",
7999            crate::MessageElement::Static(
8000                64,
8001                crate::MessageDataType::UInt8,
8002                crate::MessageClass::Data,
8003            ),
8004            &[ep],
8005            crate::ReliableMode::None,
8006            0,
8007        )
8008        .unwrap();
8009        for mode in 0..3 {
8010            for tx in [false, true] {
8011                for slow_last in [false, true] {
8012                    for next_sender in ["FAST", "NEW_SLOW"] {
8013                        let router = Relay::new(Box::new(|| 0));
8014                        let make = |sender| {
8015                            RelayItem::Packet(Arc::new(
8016                                Packet::new(ty, &[ep], sender, 1, Arc::<[u8]>::from([1; 64]))
8017                                    .unwrap(),
8018                            ))
8019                        };
8020                        let rx = |data| RelayRxItem {
8021                            src: 0,
8022                            data,
8023                            priority: 0,
8024                        };
8025                        let tx_item = |data| RelayTxItem {
8026                            src: None,
8027                            dst: 1,
8028                            data,
8029                            priority: 0,
8030                            retry_after_ms: 0,
8031                        };
8032                        let order = if slow_last {
8033                            ["FAST", "FAST", "SLOW"]
8034                        } else {
8035                            ["SLOW", "FAST", "FAST"]
8036                        };
8037                        let mut st = router.state.lock();
8038                        st.memory.max_queue_budget = usize::MAX;
8039                        let cost = if tx {
8040                            order.iter().map(|s| tx_item(make(s)).byte_cost()).sum()
8041                        } else {
8042                            order.iter().map(|s| rx(make(s)).byte_cost()).sum()
8043                        };
8044                        if mode == 0 {
8045                            if tx {
8046                                st.tx_queue = BoundedDeque::new(cost, cost, 2.0);
8047                            } else {
8048                                st.rx_queue = BoundedDeque::new(cost, cost, 2.0);
8049                            }
8050                        } else if mode == 1 {
8051                            if tx {
8052                                st.tx_queue =
8053                                    BoundedDeque::new(cost * 10, 3 * size_of::<RelayTxItem>(), 1.0);
8054                            } else {
8055                                st.rx_queue =
8056                                    BoundedDeque::new(cost * 10, 3 * size_of::<RelayRxItem>(), 1.0);
8057                            }
8058                        }
8059                        for sender in order {
8060                            if tx {
8061                                st.push_tx(tx_item(make(sender))).unwrap();
8062                            } else {
8063                                st.push_rx(rx(make(sender))).unwrap();
8064                            }
8065                        }
8066                        if mode == 2 {
8067                            st.memory.max_queue_budget = st.shared_queue_bytes_used();
8068                        }
8069                        if tx {
8070                            st.push_tx(tx_item(make(next_sender))).unwrap_or_else(|error| panic!("admit error {error:?}: mode={mode} tx={tx} slow_last={slow_last} incoming={next_sender}"));
8071                        } else {
8072                            st.push_rx(rx(make(next_sender))).unwrap_or_else(|error| panic!("admit error {error:?}: mode={mode} tx={tx} slow_last={slow_last} incoming={next_sender}"));
8073                        }
8074                        let slow = wire_format::source_address_for_sender("SLOW");
8075                        let retained = if tx {
8076                            st.tx_queue.iter().any(|item| {
8077                                RelayInner::fair_bulk_owner(&item.data, item.priority) == Some(slow)
8078                            })
8079                        } else {
8080                            st.rx_queue.iter().any(|item| {
8081                                RelayInner::fair_bulk_owner(&item.data, item.priority) == Some(slow)
8082                            })
8083                        };
8084                        assert!(
8085                            retained,
8086                            "slow report evicted: mode={mode} tx={tx} slow_last={slow_last} incoming={next_sender}"
8087                        );
8088                    }
8089                }
8090            }
8091        }
8092    }
8093}
8094
8095#[cfg(all(test, feature = "discovery"))]
8096#[test]
8097fn cow_discovery_rejected_growth_keeps_original_graph_and_routes() {
8098    crate::tests::ensure_common_test_schema();
8099    let node = Relay::new_with_config(RelayConfig::default().with_sender("GB"), Box::new(|| 0));
8100    let side = node.add_side_packed("can", |_| Ok(()));
8101    let baseline = discovery::build_discovery_topology(
8102        "PEER",
8103        0,
8104        &[TopologyBoardNode {
8105            sender_id: "PEER".into(),
8106            reachable_endpoints: Vec::new(),
8107            reachable_timesync_sources: Vec::new(),
8108            connections: Vec::new(),
8109        }],
8110    )
8111    .unwrap();
8112    node.rx_from_side(side, baseline).unwrap();
8113    node.process_all_queues().unwrap();
8114    node.clear_queues();
8115    let before = node.export_topology();
8116    let original = {
8117        let mut st = node.state.lock();
8118        let graph = st.discovery_routes[&side].announcers["PEER"]
8119            .topology_boards
8120            .clone();
8121        st.memory.max_queue_budget = st.shared_queue_bytes_used();
8122        graph
8123    };
8124    let update = discovery::build_discovery_topology_delta(
8125        "PEER",
8126        1,
8127        &[TopologyBoardNode {
8128            sender_id: "PEER".into(),
8129            reachable_endpoints: Vec::new(),
8130            reachable_timesync_sources: vec!["NEW_CLOCK".into()],
8131            connections: vec!["NEW_NEIGHBOR".into()],
8132        }],
8133        &[],
8134    )
8135    .unwrap();
8136    assert!(
8137        node.rx_from_side(side, update)
8138            .and_then(|_| node.process_all_queues())
8139            .is_err(),
8140        "growth must fail when no queue budget can be reclaimed"
8141    );
8142    assert_eq!(node.export_topology().routes, before.routes);
8143    let st = node.state.lock();
8144    assert!(Arc::ptr_eq(
8145        &original,
8146        &st.discovery_routes[&side].announcers["PEER"].topology_boards
8147    ));
8148    assert!(original[0].connections.is_empty());
8149    assert!(original[0].reachable_timesync_sources.is_empty());
8150}