Skip to main content

bacnet_network/
layer_admission.rs

1use super::ingress::{ApduReceiver, Ingress};
2use super::*;
3use std::collections::HashMap;
4use std::sync::Mutex;
5use std::task::Poll;
6use tokio::sync::mpsc;
7
8const QUEUE_CAPACITY: usize = 256;
9const APDU_SOURCE_CAPACITY: usize = 16;
10
11// NetworkLayer has one unnamed transport; router MACs are scoped to the
12// ingress port's network number, never the routed NPDU SNET/SADR.
13type AdmissionSource = (Option<u16>, MacAddr);
14
15fn apdu_source(apdu: &ReceivedApdu) -> AdmissionSource {
16    (apdu.ingress_network, apdu.source_mac.clone())
17}
18
19/// A consistent, count-only snapshot of one network-layer receive queue.
20///
21/// Counters belong to one receiver, not to a source MAC. A router's local queue
22/// is shared by all its ports. No payloads, source identities or per-source
23/// totals are exposed. The three drop totals saturate at `u64::MAX` and attribute
24/// each admission drop once, in **Closed > fairness > Full** order.
25/// Separate APDU/control snapshots are not an atomic snapshot of both queues.
26/// See the [receive-queue contract](crate::layer#receive-queue-admission) and
27/// [`QueueAdmissionCounters::snapshot`] for receiver kinds and lifetime.
28#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
29pub struct QueueAdmissionSnapshot {
30    /// Items currently queued (at most 256), excluding items already returned by
31    /// [`AdmissionReceiver::recv`] or [`AdmissionReceiver::try_recv`]. Close and
32    /// stop preserve depth until draining; dropping the receiver sets it to zero.
33    pub current_depth: usize,
34    /// Greatest queued depth since this receiver was created (at most 256).
35    /// Draining, closing, stopping or dropping the receiver does not reset it.
36    pub high_water: usize,
37    /// Arriving items dropped because the 256-item queue was full, after Closed
38    /// and any per-source fairness check; saturates at `u64::MAX`.
39    /// A fairness drop does not also increment this total.
40    pub full_drops: u64,
41    /// Arriving APDUs dropped by the tracked queue's quota of 16 per source:
42    /// complete source MAC bytes for NetworkLayer, (ingress port network number,
43    /// source MAC bytes) for routers, never routed NPDU SNET/SADR. See the
44    /// [source-key definitions](crate::layer#source-keys-and-drop-precedence).
45    /// Saturates at `u64::MAX`. Closed takes precedence; otherwise evaluated
46    /// before global Full, even when the queue has room. Always zero for control
47    /// queues, which have no per-source quota. This is a queue-wide total, not
48    /// a per-source counter.
49    pub fairness_drops: u64,
50    /// Arriving items dropped because the receiver was closed or dropped;
51    /// saturates at `u64::MAX`. Takes precedence over fairness and Full.
52    ///
53    /// In NetworkLayer, the first such APDU ends dispatch; the first such control
54    /// disables that stream. Later discarded controls are not admission attempts.
55    /// Routers keep forwarding and count each closed local-admission attempt.
56    /// Dropping already queued items with the receiver is not an admission drop.
57    pub closed_drops: u64,
58}
59
60/// Cloneable snapshot handle obtained from an [`AdmissionReceiver`].
61///
62/// This handle keeps only accounting alive, not payloads, the channel or dispatch task.
63/// It remains readable after the receiver, layer, or router is dropped. Dropping
64/// the receiver sets depth to zero; closing it preserves depth until drained.
65/// High-water/drop totals are retained, and later Closed admission attempts may
66/// still increase the Closed total. All clones observe the same queue; snapshots
67/// expose counts only, not source identities or per-source totals. See the
68/// [receive-queue contract](crate::layer#receive-queue-admission).
69#[derive(Debug, Clone, Default)]
70pub struct QueueAdmissionCounters(Arc<Mutex<QueueAdmissionState>>);
71
72#[derive(Debug, Default)]
73struct QueueAdmissionState {
74    snapshot: QueueAdmissionSnapshot,
75    // Used only by tracked APDU queues. Insert after successful
76    // admission, remove on the last dequeue: live entries <= depth <= 256.
77    queued_by_source: HashMap<AdmissionSource, usize>,
78}
79
80impl QueueAdmissionState {
81    fn dequeued(&mut self, source: Option<AdmissionSource>) {
82        self.snapshot.current_depth -= 1;
83        if let Some(ref source) = source {
84            let count = self
85                .queued_by_source
86                .get_mut(source)
87                .expect("queued APDU source must be accounted");
88            *count -= 1;
89            if *count == 0 {
90                self.queued_by_source.remove(source);
91            }
92            self.assert_source_depth();
93        }
94    }
95
96    fn assert_source_depth(&self) {
97        debug_assert!(self.queued_by_source.len() <= self.snapshot.current_depth);
98        debug_assert!(self.snapshot.current_depth <= QUEUE_CAPACITY);
99        debug_assert_eq!(
100            self.queued_by_source.values().sum::<usize>(),
101            self.snapshot.current_depth
102        );
103        debug_assert!(self
104            .queued_by_source
105            .values()
106            .all(|count| (1..=APDU_SOURCE_CAPACITY).contains(count)));
107    }
108}
109
110impl QueueAdmissionCounters {
111    /// Read this queue's depth, high-water mark, and admission-drop totals.
112    ///
113    /// Returns a consistent copy of all [`QueueAdmissionSnapshot`] fields under
114    /// one accounting lock, without resetting them. Separate calls for APDU and
115    /// control queues are not jointly atomic. Counts do not expose payloads,
116    /// source identities or per-source totals; drop precedence and saturation
117    /// follow the [receive-queue contract](crate::layer#receive-queue-admission).
118    /// The handle remains readable after the receiver, layer, or router is
119    /// dropped; a snapshot need not be final while dispatch can still attempt
120    /// admission. A default handle has zero counts and is not attached to a queue.
121    pub fn snapshot(&self) -> QueueAdmissionSnapshot {
122        self.0
123            .lock()
124            .expect("queue admission counters poisoned")
125            .snapshot
126    }
127}
128
129/// Opt-in, one-consumer receiver with exact queue-depth accounting.
130///
131/// Created by [`NetworkLayer::start_with_admission`] or
132/// [`NetworkLayer::enable_network_control_receiver_with_admission`], or by
133/// [`RouterOptions::track_admission`](crate::router::RouterOptions::track_admission) or
134/// [`RouterOptions::network_control_receiver_with_admission`](crate::router::RouterOptions::network_control_receiver_with_admission). The queue
135/// holds 256 items; tracked APDUs additionally have a quota of 16 queued items per
136/// source MAC (NetworkLayer) or (ingress port network number, source MAC) (router).
137/// Controls have no per-source quota. **Closed > fairness > Full** determines
138/// drop attribution; an admission drop releases the arriving payload, metadata
139/// and reply sender without reply bytes or a wire rejection, never evicting an
140/// older item. See the [receive-queue contract](crate::layer#receive-queue-admission)
141/// for exact keys, the raw/tracked matrix, queue independence and lifecycle.
142///
143/// [`Self::close`] retains queued items for draining; dropping this receiver
144/// discards them and sets depth to zero without counting admission drops.
145/// High-water/drop totals survive through an owned [`Self::counters`] handle.
146///
147/// This wrapper deliberately does not expose the underlying receiver: all
148/// dequeues must update the snapshot. Use [`NetworkLayer::start`] for the
149/// raw [`ApduReceiver`], a router started without
150/// [`RouterOptions::track_admission`](crate::router::RouterOptions::track_admission)
151/// for a plain `mpsc::Receiver`, or
152/// [`NetworkLayer::enable_network_control_receiver`] for raw controls.
153#[derive(Debug)]
154pub struct AdmissionReceiver<T> {
155    rx: mpsc::Receiver<T>,
156    counters: QueueAdmissionCounters,
157    // Only tracked APDU constructors install source accounting. The same
158    // extractor is used on admission and every dequeue; no public trait bound.
159    source_key: Option<fn(&T) -> AdmissionSource>,
160}
161
162impl<T> AdmissionReceiver<T> {
163    // Crate-internal construction keeps the generic sender and accounting in
164    // this module while letting the router hand out a raw receiver as well.
165    pub(crate) fn channel(
166        track_depth: bool,
167    ) -> (
168        AdmissionSender<T>,
169        mpsc::Receiver<T>,
170        QueueAdmissionCounters,
171    ) {
172        let (tx, rx) = AdmissionSender::channel(track_depth);
173        let counters = tx.counters.clone();
174        (tx, rx, counters)
175    }
176
177    pub(crate) fn from_parts(rx: mpsc::Receiver<T>, counters: QueueAdmissionCounters) -> Self {
178        Self {
179            rx,
180            counters,
181            source_key: None,
182        }
183    }
184
185    /// Obtain an independently owned snapshot handle for this queue.
186    ///
187    /// The clone shares accounting without retaining the receiver, payloads or
188    /// dispatch task, and remains readable after receiver/layer/router drop.
189    /// See [`QueueAdmissionCounters::snapshot`] and the
190    /// [receive-queue contract](crate::layer#receive-queue-admission).
191    pub fn counters(&self) -> QueueAdmissionCounters {
192        self.counters.clone()
193    }
194
195    /// Receive the next item, or `None` once closed and drained.
196    ///
197    /// Returning an item releases one depth slot and, for tracked APDUs, one
198    /// source-key slot before the consumer can retain or process it. `None`
199    /// changes no counts. The key and limits follow the
200    /// [receive-queue contract](crate::layer#receive-queue-admission).
201    /// Cancellation safe: dropping a pending receive does not remove an item
202    /// or alter its accounting. No counter lock is held while waiting.
203    pub async fn recv(&mut self) -> Option<T> {
204        std::future::poll_fn(|cx| {
205            let mut state = self
206                .counters
207                .0
208                .lock()
209                .expect("queue admission counters poisoned");
210            let result = self.rx.poll_recv(cx);
211            if let Poll::Ready(Some(item)) = &result {
212                state.dequeued(self.source_key.map(|source_key| source_key(item)));
213            }
214            result
215        })
216        .await
217    }
218
219    /// Receive immediately, releasing depth and source-key slots as in [`Self::recv`].
220    ///
221    /// A closed queue can still return queued items. Returns `Empty` when no item
222    /// is currently available but admission remains open, or `Disconnected` once
223    /// closed and drained. Errors do not change counts. See the
224    /// [receive-queue contract](crate::layer#receive-queue-admission).
225    pub fn try_recv(&mut self) -> Result<T, mpsc::error::TryRecvError> {
226        let mut state = self
227            .counters
228            .0
229            .lock()
230            .expect("queue admission counters poisoned");
231        let result = self.rx.try_recv();
232        if let Ok(item) = &result {
233            state.dequeued(self.source_key.map(|source_key| source_key(item)));
234        }
235        result
236    }
237
238    /// Stop admission while retaining already queued items for draining.
239    ///
240    /// Depth and source-key slots remain until [`Self::recv`] or [`Self::try_recv`]
241    /// dequeues an item, or the receiver is dropped. Close itself does not count
242    /// as a drop or reset high-water/drop totals. Later attempts count as Closed,
243    /// taking precedence over fairness and Full.
244    ///
245    /// In NetworkLayer, the next APDU admission attempt ends dispatch, or the next
246    /// control admission attempt disables only that stream. Routers count every
247    /// closed local admission attempt and keep forwarding/handling controls.
248    /// These match closing the corresponding raw receiver. See the
249    /// [receive-queue contract](crate::layer#close-drop-stop-and-observation).
250    pub fn close(&mut self) {
251        let _snapshot = self
252            .counters
253            .0
254            .lock()
255            .expect("queue admission counters poisoned");
256        self.rx.close();
257    }
258}
259
260impl AdmissionReceiver<ReceivedApdu> {
261    pub(crate) fn from_apdu_parts(
262        rx: mpsc::Receiver<ReceivedApdu>,
263        counters: QueueAdmissionCounters,
264    ) -> Self {
265        Self {
266            rx,
267            counters,
268            source_key: Some(apdu_source),
269        }
270    }
271}
272
273impl<T> Drop for AdmissionReceiver<T> {
274    fn drop(&mut self) {
275        let mut state = self
276            .counters
277            .0
278            .lock()
279            .expect("queue admission counters poisoned");
280        self.rx.close();
281        state.snapshot.current_depth = 0;
282        state.queued_by_source = HashMap::new();
283        // The receiver's queued items (including reply senders) drop after
284        // this guard is released. Closing first prevents concurrent admission.
285    }
286}
287
288/// Shared non-waiting admission for raw and tracked 256-item queues.
289///
290/// Both modes count Full/Closed internally; only tracked receivers expose
291/// snapshots and release depth/source slots on dequeue. Tracked APDU sends use
292/// the 16-per-key quota and Closed > fairness > Full precedence. Generic/control
293/// sends and raw APDUs have no quota. See the public
294/// [receive-queue contract](crate::layer#receive-queue-admission).
295pub(crate) struct AdmissionSender<T> {
296    tx: mpsc::Sender<T>,
297    counters: QueueAdmissionCounters,
298    // Legacy receivers dequeue outside this module, so only opt-in receivers
299    // expose depth/high-water accounting. Admission drops are counted in both.
300    track_depth: bool,
301}
302
303impl<T> Clone for AdmissionSender<T> {
304    fn clone(&self) -> Self {
305        Self {
306            tx: self.tx.clone(),
307            counters: self.counters.clone(),
308            track_depth: self.track_depth,
309        }
310    }
311}
312
313impl<T> AdmissionSender<T> {
314    fn channel(track_depth: bool) -> (Self, mpsc::Receiver<T>) {
315        let (tx, rx) = mpsc::channel(QUEUE_CAPACITY);
316        (
317            Self {
318                tx,
319                counters: QueueAdmissionCounters::default(),
320                track_depth,
321            },
322            rx,
323        )
324    }
325
326    pub(crate) fn try_send(&self, item: T) -> Result<(), mpsc::error::TrySendError<T>> {
327        self.try_send_from(item, None)
328    }
329
330    fn try_send_from(
331        &self,
332        item: T,
333        source: Option<AdmissionSource>,
334    ) -> Result<(), mpsc::error::TrySendError<T>> {
335        if !self.track_depth {
336            // A raw receiver dequeues without accounting and has no quota, so
337            // only a refusal is counted, and only a refusal takes the lock: an
338            // admitted item costs the channel alone (#1713).
339            let result = self.tx.try_send(item);
340            if let Err(error) = &result {
341                let mut state = self
342                    .counters
343                    .0
344                    .lock()
345                    .expect("queue admission counters poisoned");
346                let drops = match error {
347                    mpsc::error::TrySendError::Full(_) => &mut state.snapshot.full_drops,
348                    mpsc::error::TrySendError::Closed(_) => &mut state.snapshot.closed_drops,
349                };
350                *drops = drops.saturating_add(1);
351            }
352            return result;
353        }
354        // Serialize the admission and dequeue accounting, not asynchronous
355        // waiting. Each queue has its own short critical section, and no lock
356        // survives a poll/try operation. Closed keeps its lifecycle semantics;
357        // otherwise a source over quota takes precedence over global Full.
358        let mut state = self
359            .counters
360            .0
361            .lock()
362            .expect("queue admission counters poisoned");
363        if !self.tx.is_closed()
364            && source.as_ref().is_some_and(|source| {
365                state.queued_by_source.get(source).copied().unwrap_or(0) >= APDU_SOURCE_CAPACITY
366            })
367        {
368            state.snapshot.fairness_drops = state.snapshot.fairness_drops.saturating_add(1);
369            // Reuse the caller's drop-arriving path, but not its Full counter.
370            return Err(mpsc::error::TrySendError::Full(item));
371        }
372        let result = self.tx.try_send(item);
373        match &result {
374            Ok(()) if self.track_depth => {
375                state.snapshot.current_depth += 1;
376                state.snapshot.high_water =
377                    state.snapshot.high_water.max(state.snapshot.current_depth);
378                if let Some(source) = source {
379                    *state.queued_by_source.entry(source).or_default() += 1;
380                    state.assert_source_depth();
381                }
382            }
383            Ok(()) => {}
384            Err(mpsc::error::TrySendError::Full(_)) => {
385                state.snapshot.full_drops = state.snapshot.full_drops.saturating_add(1);
386            }
387            Err(mpsc::error::TrySendError::Closed(_)) => {
388                state.snapshot.closed_drops = state.snapshot.closed_drops.saturating_add(1);
389            }
390        }
391        result
392    }
393}
394
395impl AdmissionSender<ReceivedApdu> {
396    pub(crate) fn try_send_apdu(
397        &self,
398        apdu: ReceivedApdu,
399    ) -> Result<(), mpsc::error::TrySendError<()>> {
400        // Raw receivers cannot release source counts: legacy ingress neither
401        // clones keys nor enforces a quota.
402        let source = self.track_depth.then(|| apdu_source(&apdu));
403        // Both dispatchers drop arriving APDUs on failure. Release the owned
404        // item after the accounting lock is released; only Closed affects the
405        // NetworkLayer dispatch lifecycle, not the returned payload.
406        self.try_send_from(apdu, source)
407            .map_err(|error| match error {
408                mpsc::error::TrySendError::Full(_) => mpsc::error::TrySendError::Full(()),
409                mpsc::error::TrySendError::Closed(_) => mpsc::error::TrySendError::Closed(()),
410            })
411    }
412}
413
414impl<T: TransportPort + 'static> NetworkLayer<T> {
415    /// Enable the one-consumer decoded network-control stream with a raw receiver.
416    ///
417    /// Call before either [`Self::start`] or [`Self::start_with_admission`], at
418    /// most once across the two control-enable alternatives. Returns an encoding
419    /// error if the layer has started or a control receiver is already enabled.
420    /// With [`Self::start`], controls arrive only while its [`ApduReceiver`] is
421    /// polled; with [`Self::start_with_admission`], whenever they arrive.
422    /// The separate 256-item control queue has no per-source quota: Closed takes
423    /// precedence over Full, with Full/Closed counted internally but no snapshot
424    /// or depth/high-water tracking. Failed admission drops the control and its
425    /// metadata without waiting for a consumer or generating reply bytes/wire
426    /// rejections; APDU admission remains independent.
427    ///
428    /// Closing retains queued controls; dropping discards them. The first later
429    /// control admission attempt disables only this stream, then controls resume
430    /// discard behavior. Use [`Self::enable_network_control_receiver_with_admission`]
431    /// for counters, and see the [receive-queue contract](crate::layer#receive-queue-admission)
432    /// for the raw/tracked matrix, sequencing and stop/drain semantics.
433    pub fn enable_network_control_receiver(
434        &mut self,
435    ) -> Result<mpsc::Receiver<ReceivedNetworkControl>, Error> {
436        self.enable_network_control(false).map(|(rx, _)| rx)
437    }
438
439    /// Enable the control stream with queue-admission snapshots.
440    ///
441    /// This has the same before-start, one-consumer contract and errors as
442    /// [`Self::enable_network_control_receiver`]; the two methods are alternatives.
443    /// The separate 256-item queue tracks depth/high-water and Full/Closed drops
444    /// through [`AdmissionReceiver::counters`]. It has no per-source quota, so
445    /// fairness drops are always zero and precedence is Closed > Full.
446    /// Drop-arriving, ownership release, no-reply/no-wire-rejection and
447    /// close/drop/stop behavior follow the shared
448    /// [receive-queue contract](crate::layer#receive-queue-admission).
449    ///
450    /// Controls discarded without opting into either stream are not admission
451    /// drops. [`Self::network_control_ingress_sequence`] advances before every
452    /// attempted control admission, including Full and the first Closed drop,
453    /// but not for later controls once that stream is disabled.
454    pub fn enable_network_control_receiver_with_admission(
455        &mut self,
456    ) -> Result<AdmissionReceiver<ReceivedNetworkControl>, Error> {
457        let (rx, counters) = self.enable_network_control(true)?;
458        Ok(AdmissionReceiver::from_parts(rx, counters))
459    }
460
461    fn enable_network_control(
462        &mut self,
463        track_depth: bool,
464    ) -> Result<
465        (
466            mpsc::Receiver<ReceivedNetworkControl>,
467            QueueAdmissionCounters,
468        ),
469        Error,
470    > {
471        if self.started {
472            return Err(Error::Encoding(
473                "network-control receiver must be enabled before NetworkLayer::start".into(),
474            ));
475        }
476        if self.network_control_tx.is_some() {
477            return Err(Error::Encoding(
478                "network-control receiver is already enabled".into(),
479            ));
480        }
481        let (tx, rx) = AdmissionSender::channel(track_depth);
482        let counters = tx.counters.clone();
483        self.network_control_tx = Some(tx);
484        Ok((rx, counters))
485    }
486
487    /// Start the network layer. Returns the receiver for incoming APDUs.
488    ///
489    /// This starts the underlying transport. The [`ApduReceiver`] decodes the
490    /// transport's NPDUs in the task that polls it, with no dispatch task or
491    /// second queue between them (#1713): the transport's queue is the only
492    /// buffer and bounds memory under overload, and the transport counts what
493    /// it drops. Network messages reach an opted-in control receiver only
494    /// while the APDU receiver is polled. Receive is cancel safe.
495    ///
496    /// Close retains the transport's queued NPDUs for draining; drop discards
497    /// them. [`Self::stop`] after start leaves them drainable too. Use
498    /// [`Self::start_with_admission`] for a tracked queue with snapshots and
499    /// per-source fairness, whose controls flow independently; see the
500    /// [receive-queue contract](crate::layer#receive-queue-admission).
501    pub async fn start(&mut self) -> Result<ApduReceiver, Error> {
502        if self.started {
503            return Err(Error::Encoding("NetworkLayer is already started".into()));
504        }
505        let npdu_rx = self.transport.start().await?;
506        self.started = true;
507        // Taken once the transport knows its own addresses.
508        self.local_broadcasts = self.transport.local_broadcast_destinations();
509        let npdu_rx = Arc::new(Mutex::new(npdu_rx));
510        self.receiver_queue = Arc::downgrade(&npdu_rx);
511        Ok(ApduReceiver::new(npdu_rx, self.ingress()))
512    }
513
514    /// The receive state for this layer's start: its totals and controls.
515    fn ingress(&mut self) -> Ingress {
516        Ingress::new(
517            Arc::clone(&self.node_totals),
518            self.network_control_tx.take(),
519            Arc::clone(&self.network_control_ingress_sequence),
520        )
521    }
522
523    /// Start with an APDU receiver that also exposes admission snapshots.
524    ///
525    /// This is an alternative to [`Self::start`]. It runs a dispatch task that
526    /// decodes into its own queue, so controls flow whether or not this
527    /// receiver is polled. [`Self::stop`] closes admission but leaves
528    /// queued items available to drain. Dropping the receiver discards those
529    /// items and releases their reply channels without sending reply bytes.
530    ///
531    /// Unlike the raw receiver, this tracks depth/high-water and drop totals via
532    /// [`AdmissionReceiver::counters`] and caps each complete source MAC byte
533    /// value at 16 queued, unconsumed APDUs in the 256-item queue. The single
534    /// transport uses no port key and ignores routed NPDU SNET/SADR for quotas.
535    /// Dequeuing releases one source slot even if the consumer retains the APDU.
536    /// **Closed > fairness > Full** selects one drop reason, releasing the
537    /// arriving payload, metadata and reply sender without sending reply bytes
538    /// or a wire rejection. Control queues have separate capacity and no quota.
539    /// For the router's port-scoped key, raw/tracked matrix, and full
540    /// close/drop/stop and counter-lifetime rules, see the
541    /// [receive-queue contract](crate::layer#receive-queue-admission).
542    ///
543    /// ```no_run
544    /// # async fn example() -> Result<(), bacnet_types::error::Error> {
545    /// use bacnet_network::layer::NetworkLayer;
546    /// use bacnet_transport::loopback::LoopbackTransport;
547    /// let (transport, _peer) = LoopbackTransport::pair(vec![1], vec![2]);
548    /// let mut network = NetworkLayer::new(transport);
549    /// let mut controls = network.enable_network_control_receiver_with_admission()?;
550    /// let mut apdus = network.start_with_admission().await?;
551    /// let apdu_counters = apdus.counters();
552    /// let control_counters = controls.counters();
553    /// tokio::select! {
554    ///     _ = apdus.recv() => {},
555    ///     _ = controls.recv() => {},
556    /// }
557    /// let _snapshots = (apdu_counters.snapshot(), control_counters.snapshot());
558    /// network.stop().await?;
559    /// # Ok(()) }
560    /// ```
561    pub async fn start_with_admission(&mut self) -> Result<AdmissionReceiver<ReceivedApdu>, Error> {
562        let (rx, counters) = self.start_dispatch().await?;
563        Ok(AdmissionReceiver::from_apdu_parts(rx, counters))
564    }
565
566    async fn start_dispatch(
567        &mut self,
568    ) -> Result<(mpsc::Receiver<ReceivedApdu>, QueueAdmissionCounters), Error> {
569        if self.started {
570            return Err(Error::Encoding("NetworkLayer is already started".into()));
571        }
572        let mut npdu_rx = self.transport.start().await?;
573        self.started = true;
574        // Taken once the transport knows its own addresses.
575        self.local_broadcasts = self.transport.local_broadcast_destinations();
576        let mut ingress = self.ingress();
577        let (apdu_tx, apdu_rx) = AdmissionSender::channel(true);
578        let counters = apdu_tx.counters.clone();
579
580        let dispatch_task = tokio::spawn(async move {
581            while let Some(received) = npdu_rx.recv().await {
582                let Some(apdu) = ingress.take(received) else {
583                    continue;
584                };
585                match apdu_tx.try_send_apdu(apdu) {
586                    Ok(()) | Err(mpsc::error::TrySendError::Full(_)) => {}
587                    Err(mpsc::error::TrySendError::Closed(_)) => break,
588                }
589            }
590        });
591
592        self.dispatch_task = Some(dispatch_task);
593        Ok((apdu_rx, counters))
594    }
595}
596
597#[cfg(test)]
598#[path = "layer_fairness_tests.rs"]
599mod fairness_tests;