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;