Skip to main content

rns_core/transport/
announce_queue.rs

1//! Per-interface announce bandwidth queuing.
2//!
3//! Announces with hops > 0 (propagation, not locally-originated) are gated
4//! by a per-interface bandwidth cap (default 2%). When bandwidth is exhausted,
5//! announces are queued and released when bandwidth becomes available.
6//!
7//! Python reference: Transport.py:1085-1165, Interface.py:246-286
8
9use alloc::collections::BTreeMap;
10use alloc::vec::Vec;
11
12use super::types::{AirtimeProfile, InterfaceId, PacketBytes, TransportAction};
13use crate::constants;
14
15/// A queued announce entry waiting for bandwidth availability.
16#[derive(Debug, Clone)]
17pub struct AnnounceQueueEntry {
18    /// Destination hash of the announce.
19    pub destination_hash: [u8; 16],
20    /// Time the announce was queued.
21    pub time: f64,
22    /// Hops from the announce.
23    pub hops: u8,
24    /// Time the announce was originally emitted (from random blob).
25    pub emitted: f64,
26    /// Raw announce bytes (ready to send).
27    pub raw: PacketBytes,
28}
29
30/// Per-interface announce queue with bandwidth tracking.
31#[derive(Debug, Clone)]
32pub struct InterfaceAnnounceQueue {
33    /// Queued announce entries.
34    pub entries: Vec<AnnounceQueueEntry>,
35    /// Earliest time another announce is allowed on this interface.
36    pub announce_allowed_at: f64,
37}
38
39impl InterfaceAnnounceQueue {
40    pub fn new() -> Self {
41        InterfaceAnnounceQueue {
42            entries: Vec::new(),
43            announce_allowed_at: 0.0,
44        }
45    }
46
47    /// Insert an announce into the queue.
48    /// If an entry for the same destination already exists, update it if the new one
49    /// has fewer hops or is newer.
50    pub fn insert(&mut self, entry: AnnounceQueueEntry) {
51        // Check for existing entry with same destination
52        if let Some(pos) = self
53            .entries
54            .iter()
55            .position(|e| e.destination_hash == entry.destination_hash)
56        {
57            let existing = &self.entries[pos];
58            // Update if new entry has fewer hops, or same hops and newer
59            if entry.hops < existing.hops
60                || (entry.hops == existing.hops && entry.emitted > existing.emitted)
61            {
62                self.entries[pos] = entry;
63            }
64            // Otherwise discard the new entry
65        } else {
66            // Enforce max queue size
67            if self.entries.len() >= constants::MAX_QUEUED_ANNOUNCES {
68                // Drop oldest entry
69                self.entries.remove(0);
70            }
71            self.entries.push(entry);
72        }
73    }
74
75    /// Remove stale entries (older than QUEUED_ANNOUNCE_LIFE).
76    pub fn remove_stale(&mut self, now: f64) {
77        self.entries
78            .retain(|e| now - e.time < constants::QUEUED_ANNOUNCE_LIFE);
79    }
80
81    /// Select the next announce to send: minimum hops, then oldest (FIFO).
82    /// Returns the index of the selected entry, or None if empty.
83    pub fn select_next(&self) -> Option<usize> {
84        if self.entries.is_empty() {
85            return None;
86        }
87        let mut best_idx = 0;
88        let mut best_hops = self.entries[0].hops;
89        let mut best_time = self.entries[0].time;
90
91        for (i, entry) in self.entries.iter().enumerate().skip(1) {
92            if entry.hops < best_hops || (entry.hops == best_hops && entry.time < best_time) {
93                best_idx = i;
94                best_hops = entry.hops;
95                best_time = entry.time;
96            }
97        }
98        Some(best_idx)
99    }
100
101    /// Check if an announce is allowed now based on bandwidth.
102    pub fn is_allowed(&self, now: f64) -> bool {
103        now >= self.announce_allowed_at
104    }
105
106    /// Calculate the next allowed time after sending an announce.
107    /// `raw_len`: size of the announce in bytes
108    /// `bitrate`: interface bitrate in bits/second
109    /// `announce_cap`: fraction of bitrate reserved for announces
110    pub fn calculate_next_allowed(
111        now: f64,
112        raw_len: usize,
113        bitrate: u64,
114        airtime_profile: Option<AirtimeProfile>,
115        announce_cap: f64,
116    ) -> f64 {
117        if announce_cap <= 0.0 {
118            return now; // no cap
119        }
120
121        let time_to_send = airtime_profile
122            .map(|profile| profile.transmit_time_secs(raw_len))
123            .unwrap_or_else(|| {
124                if bitrate == 0 {
125                    0.0
126                } else {
127                    let bits = (raw_len * 8) as f64;
128                    bits / (bitrate as f64)
129                }
130            });
131        if time_to_send <= 0.0 {
132            return now;
133        }
134        let delay = time_to_send / announce_cap;
135        now + delay
136    }
137}
138
139impl Default for InterfaceAnnounceQueue {
140    fn default() -> Self {
141        Self::new()
142    }
143}
144
145/// Manage announce queues for all interfaces.
146#[derive(Debug, Clone)]
147pub struct AnnounceQueues {
148    queues: BTreeMap<InterfaceId, InterfaceAnnounceQueue>,
149    max_interfaces: usize,
150    interface_cap_drops: u64,
151}
152
153impl AnnounceQueues {
154    pub fn new(max_interfaces: usize) -> Self {
155        AnnounceQueues {
156            queues: BTreeMap::new(),
157            max_interfaces,
158            interface_cap_drops: 0,
159        }
160    }
161
162    /// Try to send an announce on an interface. If bandwidth is available,
163    /// returns the action immediately. Otherwise, queues it.
164    ///
165    /// Returns Some(action) if the announce should be sent now, None if queued.
166    #[allow(clippy::too_many_arguments)]
167    pub fn gate_announce(
168        &mut self,
169        interface: InterfaceId,
170        raw: PacketBytes,
171        dest_hash: [u8; 16],
172        hops: u8,
173        emitted: f64,
174        now: f64,
175        bitrate: Option<u64>,
176        airtime_profile: Option<AirtimeProfile>,
177        announce_cap: f64,
178    ) -> Option<TransportAction> {
179        // If no timing model is available, no cap applies — send immediately
180        let bitrate = match bitrate {
181            Some(br) if br > 0 => br,
182            _ if airtime_profile.is_none() => {
183                return Some(TransportAction::SendOnInterface { interface, raw });
184            }
185            _ => 0,
186        };
187
188        if !self.queues.contains_key(&interface) && self.queues.len() >= self.max_interfaces {
189            self.interface_cap_drops = self.interface_cap_drops.saturating_add(1);
190            return None;
191        }
192
193        let queue = self.queues.entry(interface).or_default();
194
195        if queue.is_allowed(now) {
196            // Bandwidth available — send now and update allowed_at
197            queue.announce_allowed_at = InterfaceAnnounceQueue::calculate_next_allowed(
198                now,
199                raw.len(),
200                bitrate,
201                airtime_profile,
202                announce_cap,
203            );
204            Some(TransportAction::SendOnInterface { interface, raw })
205        } else {
206            // Queue the announce
207            queue.insert(AnnounceQueueEntry {
208                destination_hash: dest_hash,
209                time: now,
210                hops,
211                emitted,
212                raw,
213            });
214            None
215        }
216    }
217
218    /// Process all announce queues: dequeue and send when bandwidth is available.
219    /// Called from tick().
220    pub fn process_queues(
221        &mut self,
222        now: f64,
223        interfaces: &BTreeMap<InterfaceId, super::types::InterfaceInfo>,
224    ) -> Vec<TransportAction> {
225        let mut actions = Vec::new();
226        let mut empty_queues = Vec::new();
227
228        for (iface_id, queue) in self.queues.iter_mut() {
229            // Remove stale entries
230            queue.remove_stale(now);
231
232            // Process as many announces as bandwidth allows
233            while queue.is_allowed(now) {
234                if let Some(idx) = queue.select_next() {
235                    let entry = queue.entries.remove(idx);
236
237                    // Look up bitrate for this interface
238                    let (bitrate, airtime_profile, announce_cap) =
239                        if let Some(info) = interfaces.get(iface_id) {
240                            (
241                                info.bitrate.unwrap_or(0),
242                                info.airtime_profile,
243                                info.announce_cap,
244                            )
245                        } else {
246                            (0, None, constants::ANNOUNCE_CAP)
247                        };
248
249                    if bitrate > 0 || airtime_profile.is_some() {
250                        queue.announce_allowed_at = InterfaceAnnounceQueue::calculate_next_allowed(
251                            now,
252                            entry.raw.len(),
253                            bitrate,
254                            airtime_profile,
255                            announce_cap,
256                        );
257                    } else {
258                        queue.announce_allowed_at = now;
259                    }
260
261                    actions.push(TransportAction::SendOnInterface {
262                        interface: *iface_id,
263                        raw: entry.raw,
264                    });
265                } else {
266                    break;
267                }
268            }
269
270            if queue.entries.is_empty() {
271                empty_queues.push(*iface_id);
272            }
273        }
274
275        for iface_id in empty_queues {
276            self.queues.remove(&iface_id);
277        }
278
279        actions
280    }
281
282    /// Return true when recursive path requests should wait for this interface.
283    pub fn blocks_recursive_path_request(&self, interface: InterfaceId, now: f64) -> bool {
284        self.queues
285            .get(&interface)
286            .is_some_and(|queue| !queue.entries.is_empty() || !queue.is_allowed(now))
287    }
288
289    /// Reserve announce-cap airtime after sending a recursive path request.
290    pub fn reserve_recursive_path_request(
291        &mut self,
292        interface: InterfaceId,
293        raw_len: usize,
294        now: f64,
295        bitrate: Option<u64>,
296        airtime_profile: Option<AirtimeProfile>,
297        announce_cap: f64,
298    ) {
299        let bitrate = match bitrate {
300            Some(br) if br > 0 => br,
301            _ if airtime_profile.is_none() => return,
302            _ => 0,
303        };
304
305        if !self.queues.contains_key(&interface) && self.queues.len() >= self.max_interfaces {
306            self.interface_cap_drops = self.interface_cap_drops.saturating_add(1);
307            return;
308        }
309
310        let queue = self.queues.entry(interface).or_default();
311        queue.announce_allowed_at = InterfaceAnnounceQueue::calculate_next_allowed(
312            now,
313            raw_len,
314            bitrate,
315            airtime_profile,
316            announce_cap,
317        );
318    }
319
320    /// Remove all announce queue state for an interface.
321    pub fn remove_interface(&mut self, interface: InterfaceId) -> bool {
322        self.queues.remove(&interface).is_some()
323    }
324
325    /// Number of interface queues currently tracked.
326    pub fn queue_count(&self) -> usize {
327        self.queues.len()
328    }
329
330    /// Number of interface queues that currently hold buffered announces.
331    pub fn nonempty_queue_count(&self) -> usize {
332        self.queues
333            .values()
334            .filter(|queue| !queue.entries.is_empty())
335            .count()
336    }
337
338    /// Total number of buffered announce entries across all interfaces.
339    pub fn total_queued_announces(&self) -> usize {
340        self.queues.values().map(|queue| queue.entries.len()).sum()
341    }
342
343    /// Total retained raw-byte payload across all buffered announces.
344    pub fn total_queued_bytes(&self) -> usize {
345        self.queues
346            .values()
347            .flat_map(|queue| queue.entries.iter())
348            .map(|entry| entry.raw.len())
349            .sum()
350    }
351
352    /// Number of announces dropped because the interface queue cap was reached.
353    pub fn interface_cap_drop_count(&self) -> u64 {
354        self.interface_cap_drops
355    }
356
357    /// Get the queue for a specific interface (for testing).
358    #[cfg(test)]
359    pub fn queue_for(&self, id: &InterfaceId) -> Option<&InterfaceAnnounceQueue> {
360        self.queues.get(id)
361    }
362}
363
364impl Default for AnnounceQueues {
365    fn default() -> Self {
366        Self::new(1024)
367    }
368}
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373    use alloc::string::String;
374
375    fn make_entry(dest: u8, hops: u8, time: f64) -> AnnounceQueueEntry {
376        AnnounceQueueEntry {
377            destination_hash: [dest; 16],
378            time,
379            hops,
380            emitted: time,
381            raw: vec![0x01, 0x02, 0x03].into(),
382        }
383    }
384
385    fn make_interface_info(id: u64, bitrate: Option<u64>) -> super::super::types::InterfaceInfo {
386        super::super::types::InterfaceInfo {
387            stamp_costs: Default::default(),
388            id: InterfaceId(id),
389            name: String::from("test"),
390            mode: crate::constants::MODE_FULL,
391            gravity: 0,
392            recursive_prs: false,
393            announces_from_internal: true,
394            announces_to_internal: None,
395            out_capable: true,
396            in_capable: true,
397            bitrate,
398            airtime_profile: None,
399            announce_rate_target: None,
400            announce_rate_grace: 0,
401            announce_rate_penalty: 0.0,
402            announce_cap: constants::ANNOUNCE_CAP,
403            is_local_client: false,
404            wants_tunnel: false,
405            tunnel_id: None,
406            mtu: constants::MTU as u32,
407            ingress_control: crate::transport::types::IngressControlConfig::disabled(),
408            ia_freq: 0.0,
409            ip_freq: 0.0,
410            op_freq: 0.0,
411            op_samples: 0,
412            started: 0.0,
413        }
414    }
415
416    // --- InterfaceAnnounceQueue tests ---
417
418    #[test]
419    fn test_queue_entry_creation() {
420        let entry = make_entry(0xAA, 3, 1000.0);
421        assert_eq!(entry.hops, 3);
422        assert_eq!(entry.destination_hash, [0xAA; 16]);
423    }
424
425    #[test]
426    fn test_queue_insert_and_select() {
427        let mut queue = InterfaceAnnounceQueue::new();
428        queue.insert(make_entry(0x01, 3, 100.0));
429        queue.insert(make_entry(0x02, 1, 200.0));
430        queue.insert(make_entry(0x03, 2, 150.0));
431
432        // Should select min hops first (0x02 with hops=1)
433        let idx = queue.select_next().unwrap();
434        assert_eq!(queue.entries[idx].destination_hash, [0x02; 16]);
435    }
436
437    #[test]
438    fn test_queue_select_fifo_on_same_hops() {
439        let mut queue = InterfaceAnnounceQueue::new();
440        queue.insert(make_entry(0x01, 2, 200.0)); // newer
441        queue.insert(make_entry(0x02, 2, 100.0)); // older
442
443        // Same hops — should pick oldest (0x02 at time 100)
444        let idx = queue.select_next().unwrap();
445        assert_eq!(queue.entries[idx].destination_hash, [0x02; 16]);
446    }
447
448    #[test]
449    fn test_queue_dedup_update() {
450        let mut queue = InterfaceAnnounceQueue::new();
451        queue.insert(make_entry(0x01, 3, 100.0));
452        assert_eq!(queue.entries.len(), 1);
453
454        // Insert same dest with fewer hops — should update
455        queue.insert(make_entry(0x01, 1, 200.0));
456        assert_eq!(queue.entries.len(), 1);
457        assert_eq!(queue.entries[0].hops, 1);
458
459        // Insert same dest with more hops — should NOT update
460        queue.insert(make_entry(0x01, 5, 300.0));
461        assert_eq!(queue.entries.len(), 1);
462        assert_eq!(queue.entries[0].hops, 1);
463    }
464
465    #[test]
466    fn newer_duplicate_updates_matched_entry_not_trailing_entry() {
467        let mut queue = InterfaceAnnounceQueue::new();
468        queue.insert(make_entry(0x01, 3, 100.0));
469        queue.insert(make_entry(0x02, 4, 150.0));
470
471        let mut replacement = make_entry(0x01, 3, 200.0);
472        replacement.raw = vec![0x09, 0x08].into();
473        queue.insert(replacement);
474
475        assert_eq!(queue.entries.len(), 2);
476        assert_eq!(queue.entries[0].destination_hash, [0x01; 16]);
477        assert_eq!(queue.entries[0].time, 200.0);
478        assert_eq!(queue.entries[0].hops, 3);
479        assert_eq!(queue.entries[0].emitted, 200.0);
480        assert_eq!(&*queue.entries[0].raw, &[0x09, 0x08]);
481
482        assert_eq!(queue.entries[1].destination_hash, [0x02; 16]);
483        assert_eq!(queue.entries[1].time, 150.0);
484        assert_eq!(queue.entries[1].hops, 4);
485        assert_eq!(queue.entries[1].emitted, 150.0);
486        assert_eq!(&*queue.entries[1].raw, &[0x01, 0x02, 0x03]);
487    }
488
489    #[test]
490    fn duplicate_lookup_updates_only_first_preexisting_match() {
491        let mut queue = InterfaceAnnounceQueue::new();
492        queue.entries.push(make_entry(0x01, 3, 100.0));
493        queue.entries.push(make_entry(0x01, 3, 150.0));
494
495        let mut replacement = make_entry(0x01, 3, 200.0);
496        replacement.raw = vec![0x09].into();
497        queue.insert(replacement);
498
499        assert_eq!(queue.entries.len(), 2);
500        assert_eq!(queue.entries[0].time, 200.0);
501        assert_eq!(queue.entries[0].emitted, 200.0);
502        assert_eq!(&*queue.entries[0].raw, &[0x09]);
503        assert_eq!(queue.entries[1].time, 150.0);
504        assert_eq!(queue.entries[1].emitted, 150.0);
505        assert_eq!(&*queue.entries[1].raw, &[0x01, 0x02, 0x03]);
506    }
507
508    #[test]
509    fn test_queue_stale_removal() {
510        let mut queue = InterfaceAnnounceQueue::new();
511        queue.insert(make_entry(0x01, 1, 100.0));
512        queue.insert(make_entry(0x02, 2, 200.0));
513
514        // One second beyond the lifetime, only the newer entry remains.
515        queue.remove_stale(100.0 + constants::QUEUED_ANNOUNCE_LIFE + 1.0);
516        assert_eq!(queue.entries.len(), 1);
517        assert_eq!(queue.entries[0].destination_hash, [0x02; 16]);
518    }
519
520    #[test]
521    fn test_queue_max_size() {
522        let mut queue = InterfaceAnnounceQueue::new();
523        for i in 0..constants::MAX_QUEUED_ANNOUNCES {
524            queue.insert(AnnounceQueueEntry {
525                destination_hash: {
526                    let mut d = [0u8; 16];
527                    d[0] = (i >> 8) as u8;
528                    d[1] = i as u8;
529                    d
530                },
531                time: i as f64,
532                hops: 1,
533                emitted: i as f64,
534                raw: vec![0x01].into(),
535            });
536        }
537        assert_eq!(queue.entries.len(), constants::MAX_QUEUED_ANNOUNCES);
538
539        // Add one more — oldest should be dropped
540        queue.insert(make_entry(0xFF, 1, 99999.0));
541        assert_eq!(queue.entries.len(), constants::MAX_QUEUED_ANNOUNCES);
542    }
543
544    #[test]
545    fn test_queue_empty_select() {
546        let queue = InterfaceAnnounceQueue::new();
547        assert!(queue.select_next().is_none());
548    }
549
550    #[test]
551    fn test_bandwidth_allowed() {
552        let mut queue = InterfaceAnnounceQueue::new();
553        assert!(queue.is_allowed(0.0));
554        assert!(queue.is_allowed(100.0));
555
556        queue.announce_allowed_at = 200.0;
557        assert!(!queue.is_allowed(100.0));
558        assert!(!queue.is_allowed(199.9));
559        assert!(queue.is_allowed(200.0));
560        assert!(queue.is_allowed(300.0));
561    }
562
563    #[test]
564    fn test_calculate_next_allowed() {
565        // 100 bytes = 800 bits, bitrate = 1000 bps, cap = 0.02
566        // time_to_send = 800/1000 = 0.8s
567        // delay = 0.8 / 0.02 = 40.0s
568        let next = InterfaceAnnounceQueue::calculate_next_allowed(1000.0, 100, 1000, None, 0.02);
569        assert!((next - 1040.0).abs() < 0.001);
570    }
571
572    #[test]
573    fn test_calculate_next_allowed_zero_bitrate() {
574        let next = InterfaceAnnounceQueue::calculate_next_allowed(1000.0, 100, 0, None, 0.02);
575        assert_eq!(next, 1000.0); // no cap
576    }
577
578    #[test]
579    fn test_calculate_next_allowed_uses_lora_airtime() {
580        let profile = AirtimeProfile::Lora {
581            bandwidth: 125_000,
582            spreading_factor: 8,
583            coding_rate: 5,
584            preamble_symbols: 8,
585            explicit_header: true,
586            crc: true,
587        };
588
589        let next =
590            InterfaceAnnounceQueue::calculate_next_allowed(1000.0, 100, 0, Some(profile), 0.02);
591
592        // 100-byte explicit-header LoRa packet at BW125/SF8/CR4/5:
593        // (8 + ceil((800 - 32 + 28 + 16) / 32) * 5 + 12.25) symbols
594        // * 2.048 ms/symbol = 307.712 ms airtime.
595        assert!((next - 1015.3856).abs() < 0.0001);
596    }
597
598    // --- AnnounceQueues tests ---
599
600    #[test]
601    fn test_gate_announce_no_bitrate_immediate() {
602        let mut queues = AnnounceQueues::new(1024);
603        let result = queues.gate_announce(
604            InterfaceId(1),
605            vec![0x01, 0x02, 0x03].into(),
606            [0xAA; 16],
607            2,
608            1000.0,
609            1000.0,
610            None, // no bitrate
611            None,
612            0.02,
613        );
614        assert!(result.is_some());
615        assert!(matches!(
616            result.unwrap(),
617            TransportAction::SendOnInterface { .. }
618        ));
619    }
620
621    #[test]
622    fn test_gate_announce_uses_airtime_profile_without_bitrate() {
623        let mut queues = AnnounceQueues::new(1024);
624        let profile = AirtimeProfile::Lora {
625            bandwidth: 125_000,
626            spreading_factor: 8,
627            coding_rate: 5,
628            preamble_symbols: 8,
629            explicit_header: true,
630            crc: true,
631        };
632
633        let first = queues.gate_announce(
634            InterfaceId(1),
635            vec![0x01; 100].into(),
636            [0xAA; 16],
637            2,
638            1000.0,
639            1000.0,
640            None,
641            Some(profile),
642            0.02,
643        );
644        assert!(first.is_some());
645
646        let queue = queues.queue_for(&InterfaceId(1)).unwrap();
647        assert!((queue.announce_allowed_at - 1015.3856).abs() < 0.0001);
648
649        let second = queues.gate_announce(
650            InterfaceId(1),
651            vec![0x02; 100].into(),
652            [0xBB; 16],
653            2,
654            1000.0,
655            1000.0,
656            None,
657            Some(profile),
658            0.02,
659        );
660        assert!(second.is_none());
661        assert_eq!(queues.queue_for(&InterfaceId(1)).unwrap().entries.len(), 1);
662    }
663
664    #[test]
665    fn test_gate_announce_bandwidth_available() {
666        let mut queues = AnnounceQueues::new(1024);
667        let result = queues.gate_announce(
668            InterfaceId(1),
669            vec![0x01; 100].into(),
670            [0xBB; 16],
671            2,
672            1000.0,
673            1000.0,
674            Some(10000), // 10 kbps
675            None,
676            0.02,
677        );
678        // First announce should go through
679        assert!(result.is_some());
680
681        // Check that allowed_at was updated
682        let queue = queues.queue_for(&InterfaceId(1)).unwrap();
683        assert!(queue.announce_allowed_at > 1000.0);
684    }
685
686    #[test]
687    fn test_gate_announce_bandwidth_exhausted_queues() {
688        let mut queues = AnnounceQueues::new(1024);
689
690        // First announce goes through
691        let r1 = queues.gate_announce(
692            InterfaceId(1),
693            vec![0x01; 100].into(),
694            [0xAA; 16],
695            2,
696            1000.0,
697            1000.0,
698            Some(1000), // 1 kbps — very slow
699            None,
700            0.02,
701        );
702        assert!(r1.is_some());
703
704        // Second announce at same time should be queued
705        let r2 = queues.gate_announce(
706            InterfaceId(1),
707            vec![0x02; 100].into(),
708            [0xBB; 16],
709            3,
710            1000.0,
711            1000.0,
712            Some(1000),
713            None,
714            0.02,
715        );
716        assert!(r2.is_none()); // queued
717
718        let queue = queues.queue_for(&InterfaceId(1)).unwrap();
719        assert_eq!(queue.entries.len(), 1);
720    }
721
722    #[test]
723    fn test_process_queues_dequeues_when_allowed() {
724        let mut queues = AnnounceQueues::new(1024);
725
726        // Queue an announce by exhausting bandwidth first
727        let _ = queues.gate_announce(
728            InterfaceId(1),
729            vec![0x01; 10].into(),
730            [0xAA; 16],
731            2,
732            0.0,
733            0.0,
734            Some(1000),
735            None,
736            0.02,
737        );
738        let _ = queues.gate_announce(
739            InterfaceId(1),
740            vec![0x02; 10].into(),
741            [0xBB; 16],
742            3,
743            0.0,
744            0.0,
745            Some(1000),
746            None,
747            0.02,
748        );
749
750        // Queue should have one entry
751        assert_eq!(queues.queue_for(&InterfaceId(1)).unwrap().entries.len(), 1);
752
753        let mut interfaces = BTreeMap::new();
754        interfaces.insert(InterfaceId(1), make_interface_info(1, Some(1000)));
755
756        // Process at a future time when bandwidth is available
757        let allowed_at = queues
758            .queue_for(&InterfaceId(1))
759            .unwrap()
760            .announce_allowed_at;
761        let actions = queues.process_queues(allowed_at + 1.0, &interfaces);
762
763        assert_eq!(actions.len(), 1);
764        assert!(matches!(
765            &actions[0],
766            TransportAction::SendOnInterface { interface, .. } if *interface == InterfaceId(1)
767        ));
768
769        // Queue should be pruned now that it is empty
770        assert!(queues.queue_for(&InterfaceId(1)).is_none());
771    }
772
773    #[test]
774    fn test_local_announce_bypasses_cap() {
775        // hops == 0 means locally-originated, should not be queued
776        // The caller (TransportEngine) is responsible for only calling gate_announce
777        // for hops > 0. We verify the gate_announce works for hops=0 too.
778        let mut queues = AnnounceQueues::new(1024);
779
780        // Exhaust bandwidth
781        let _ = queues.gate_announce(
782            InterfaceId(1),
783            vec![0x01; 100].into(),
784            [0xAA; 16],
785            2,
786            0.0,
787            0.0,
788            Some(1000),
789            None,
790            0.02,
791        );
792
793        // hops=0 should still be queued by gate_announce since hops filtering
794        // is the caller's responsibility. gate_announce is agnostic.
795        let r = queues.gate_announce(
796            InterfaceId(1),
797            vec![0x02; 100].into(),
798            [0xBB; 16],
799            0,
800            0.0,
801            0.0,
802            Some(1000),
803            None,
804            0.02,
805        );
806        assert!(r.is_none()); // queued — caller must bypass for hops==0
807    }
808
809    #[test]
810    fn test_remove_interface_queue() {
811        let mut queues = AnnounceQueues::new(1024);
812        let _ = queues.gate_announce(
813            InterfaceId(1),
814            vec![0x01; 100].into(),
815            [0xAA; 16],
816            2,
817            0.0,
818            0.0,
819            Some(1000),
820            None,
821            0.02,
822        );
823        let _ = queues.gate_announce(
824            InterfaceId(1),
825            vec![0x02; 100].into(),
826            [0xBB; 16],
827            3,
828            0.0,
829            0.0,
830            Some(1000),
831            None,
832            0.02,
833        );
834
835        assert!(queues.queue_for(&InterfaceId(1)).is_some());
836        assert!(queues.remove_interface(InterfaceId(1)));
837        assert!(queues.queue_for(&InterfaceId(1)).is_none());
838        assert!(!queues.remove_interface(InterfaceId(1)));
839    }
840
841    #[test]
842    fn test_process_queues_prunes_empty_queue() {
843        let mut queues = AnnounceQueues::new(1024);
844
845        let _ = queues.gate_announce(
846            InterfaceId(1),
847            vec![0x01; 10].into(),
848            [0xAA; 16],
849            2,
850            0.0,
851            0.0,
852            Some(1000),
853            None,
854            0.02,
855        );
856        let _ = queues.gate_announce(
857            InterfaceId(1),
858            vec![0x02; 10].into(),
859            [0xBB; 16],
860            3,
861            0.0,
862            0.0,
863            Some(1000),
864            None,
865            0.02,
866        );
867
868        let mut interfaces = BTreeMap::new();
869        interfaces.insert(InterfaceId(1), make_interface_info(1, Some(1000)));
870        let allowed_at = queues
871            .queue_for(&InterfaceId(1))
872            .unwrap()
873            .announce_allowed_at;
874
875        let actions = queues.process_queues(allowed_at + 1.0, &interfaces);
876        assert_eq!(actions.len(), 1);
877        assert!(queues.queue_for(&InterfaceId(1)).is_none());
878        assert_eq!(queues.queue_count(), 0);
879    }
880
881    #[test]
882    fn test_process_queues_keeps_nonempty_queue() {
883        let mut queues = AnnounceQueues::new(1024);
884        let _ = queues.gate_announce(
885            InterfaceId(1),
886            vec![0x01; 100].into(),
887            [0xAA; 16],
888            2,
889            0.0,
890            0.0,
891            Some(1000),
892            None,
893            0.02,
894        );
895        let _ = queues.gate_announce(
896            InterfaceId(1),
897            vec![0x02; 100].into(),
898            [0xBB; 16],
899            3,
900            0.0,
901            0.0,
902            Some(1000),
903            None,
904            0.02,
905        );
906        let _ = queues.gate_announce(
907            InterfaceId(1),
908            vec![0x03; 100].into(),
909            [0xCC; 16],
910            4,
911            0.0,
912            0.0,
913            Some(1000),
914            None,
915            0.02,
916        );
917
918        let mut interfaces = BTreeMap::new();
919        interfaces.insert(InterfaceId(1), make_interface_info(1, Some(1000)));
920        let allowed_at = queues
921            .queue_for(&InterfaceId(1))
922            .unwrap()
923            .announce_allowed_at;
924
925        let actions = queues.process_queues(allowed_at + 1.0, &interfaces);
926        assert_eq!(actions.len(), 1);
927        assert!(queues.queue_for(&InterfaceId(1)).is_some());
928        assert_eq!(queues.queue_for(&InterfaceId(1)).unwrap().entries.len(), 1);
929    }
930
931    #[test]
932    fn test_gate_announce_refuses_new_interface_when_at_capacity() {
933        let mut queues = AnnounceQueues::new(1);
934
935        let _ = queues.gate_announce(
936            InterfaceId(1),
937            vec![0x01; 100].into(),
938            [0xAA; 16],
939            2,
940            0.0,
941            0.0,
942            Some(1000),
943            None,
944            0.02,
945        );
946        let second = queues.gate_announce(
947            InterfaceId(1),
948            vec![0x02; 100].into(),
949            [0xBB; 16],
950            3,
951            0.0,
952            0.0,
953            Some(1000),
954            None,
955            0.02,
956        );
957        assert!(second.is_none());
958        assert_eq!(queues.queue_count(), 1);
959
960        let rejected = queues.gate_announce(
961            InterfaceId(2),
962            vec![0x03; 100].into(),
963            [0xCC; 16],
964            4,
965            0.0,
966            0.0,
967            Some(1000),
968            None,
969            0.02,
970        );
971        assert!(rejected.is_none());
972        assert_eq!(queues.queue_count(), 1);
973        assert!(queues.queue_for(&InterfaceId(2)).is_none());
974        assert_eq!(queues.interface_cap_drop_count(), 1);
975    }
976
977    #[test]
978    fn test_gate_announce_allows_existing_queue_when_at_capacity() {
979        let mut queues = AnnounceQueues::new(1);
980
981        let _ = queues.gate_announce(
982            InterfaceId(1),
983            vec![0x01; 100].into(),
984            [0xAA; 16],
985            2,
986            0.0,
987            0.0,
988            Some(1000),
989            None,
990            0.02,
991        );
992        let queued = queues.gate_announce(
993            InterfaceId(1),
994            vec![0x02; 100].into(),
995            [0xBB; 16],
996            3,
997            0.0,
998            0.0,
999            Some(1000),
1000            None,
1001            0.02,
1002        );
1003        assert!(queued.is_none());
1004        assert_eq!(queues.queue_count(), 1);
1005        assert_eq!(queues.queue_for(&InterfaceId(1)).unwrap().entries.len(), 1);
1006        assert_eq!(queues.interface_cap_drop_count(), 0);
1007    }
1008}