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;