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