Skip to main content

pamoja_ladder/
lib.rs

1//! Cost-aware transport ladder for the pamoja SDK.
2//!
3//! A field node usually has more than one way to reach the wider network, and
4//! those links differ wildly in cost, range, and availability: a local mesh hop is
5//! nearly free, long-range radio is cheap but slow, cellular is metered, and
6//! satellite is expensive. [`TransportLadder`] models that hierarchy. It holds a
7//! set of [`Transport`] rungs ordered cheapest-first and,
8//! on each send, uses the first rung that accepts the message. When no rung is
9//! reachable, the message is buffered in a durable [`Store`]
10//! and replayed later, so connectivity degrades gracefully instead of failing.
11//!
12//! This is the offline-first behavior the target deployments need on day one: an
13//! irrigation node or a fridge alarm keeps recording while every link is down and
14//! loses nothing once one returns.
15//!
16//! # Ordering and the buffer
17//!
18//! Delivery is in order. Once anything is buffered, later sends are buffered too
19//! rather than jumping ahead of the backlog over a recovered link;
20//! [`flush`](TransportLadder::flush) drains the backlog oldest-first, removing each
21//! record only after a rung accepts it. The pattern is to call
22//! [`flush`](TransportLadder::flush) when a link event suggests connectivity may
23//! have returned, and [`send`](TransportLadder::send) for new data.
24//!
25//! # The ladder as a link
26//!
27//! A ladder is itself a [`Transport`] and a [`Receive`], so anything written against
28//! one link, a profile node, a rule engine, a hand-written loop, runs over the
29//! ladder unchanged and gains its buffering. A subscription is placed on every rung
30//! that listens, and a receive hands up whichever rung delivers first, so a command
31//! reaches the node over whatever link happens to be up. A rung added with
32//! [`rung`](TransportLadder::rung) both sends and listens; a send-only link such as
33//! a LoRa uplink is added with [`uplink`](TransportLadder::uplink) and is never
34//! listened on.
35//!
36//! # Examples
37//!
38//! ```
39//! use pamoja_ladder::{Delivery, TransportLadder};
40//! use pamoja_loopback::{LoopbackBroker, LoopbackTransport};
41//! use pamoja_sync::MemoryStore;
42//!
43//! # async fn run() -> pamoja_core::Result<()> {
44//! let broker = LoopbackBroker::new();
45//! let mut ladder =
46//!     TransportLadder::new(MemoryStore::new()).rung(LoopbackTransport::new(broker.clone()));
47//! ladder.connect().await?;
48//!
49//! match ladder.send("sensors/1/temperature", b"21.5").await? {
50//!     Delivery::Sent => println!("delivered over a live link"),
51//!     Delivery::Buffered => println!("no link, buffered for later"),
52//! }
53//! # Ok(())
54//! # }
55//! ```
56
57use core::future::Future;
58use core::pin::Pin;
59use core::task::{Context, Poll};
60
61use pamoja_core::{Error, Message, Receive, Result, Store, Transport};
62
63/// The outcome of a [`TransportLadder::send`].
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
65pub enum Delivery {
66    /// The message was delivered immediately over one of the ladder's rungs.
67    Sent,
68    /// No rung accepted the message, so it was buffered for a later
69    /// [`flush`](TransportLadder::flush).
70    Buffered,
71}
72
73/// The boxed future a rung's receive returns.
74type Inbound<'a> = Pin<Box<dyn Future<Output = Result<Option<Message>>> + Send + 'a>>;
75
76/// Object-safe erasure of a rung so a ladder can hold heterogeneous rungs.
77///
78/// The core traits use `async fn`, which is not dyn-compatible; this wrapper boxes
79/// the returned futures so transports of different concrete types can live
80/// together in one ordered list.
81///
82/// The boxed futures are `Send` so a ladder can be driven from a multi-threaded
83/// runtime, which is where one usually lives: a gateway ticks it from a task
84/// rather than blocking a thread on it.
85trait DynTransport: Send {
86    /// Connects the underlying transport.
87    fn connect(&mut self) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>>;
88
89    /// Sends a payload to a topic over the underlying transport.
90    fn send<'a>(
91        &'a mut self,
92        topic: &'a str,
93        payload: &'a [u8],
94    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
95
96    /// Subscribes the underlying transport to a topic.
97    fn subscribe<'a>(
98        &'a mut self,
99        topic: &'a str,
100    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
101
102    /// Awaits the next message the underlying transport delivers.
103    fn recv(&mut self) -> Inbound<'_>;
104}
105
106/// Newtype that carries one two-way transport behind the object-safe
107/// [`DynTransport`]. Erasing through a dedicated wrapper, rather than a blanket
108/// impl over every `T: Transport`, keeps these boxed-future methods off the
109/// transports themselves so their own `connect`/`send` stay unambiguous.
110struct Erased<T>(T);
111
112impl<T: Transport + Receive + Send> DynTransport for Erased<T> {
113    fn connect(&mut self) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
114        Box::pin(Transport::connect(&mut self.0))
115    }
116
117    fn send<'a>(
118        &'a mut self,
119        topic: &'a str,
120        payload: &'a [u8],
121    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
122        Box::pin(Transport::send(&mut self.0, topic, payload))
123    }
124
125    fn subscribe<'a>(
126        &'a mut self,
127        topic: &'a str,
128    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
129        Box::pin(Transport::subscribe(&mut self.0, topic))
130    }
131
132    fn recv(&mut self) -> Inbound<'_> {
133        Box::pin(Receive::recv(&mut self.0))
134    }
135}
136
137/// Newtype that carries a send-only transport behind [`DynTransport`]. The ladder
138/// never subscribes or listens on one, so its receive reports an ended link.
139struct Uplink<T>(T);
140
141impl<T: Transport + Send> DynTransport for Uplink<T> {
142    fn connect(&mut self) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
143        Box::pin(Transport::connect(&mut self.0))
144    }
145
146    fn send<'a>(
147        &'a mut self,
148        topic: &'a str,
149        payload: &'a [u8],
150    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
151        Box::pin(Transport::send(&mut self.0, topic, payload))
152    }
153
154    fn subscribe<'a>(
155        &'a mut self,
156        topic: &'a str,
157    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
158        Box::pin(Transport::subscribe(&mut self.0, topic))
159    }
160
161    fn recv(&mut self) -> Inbound<'_> {
162        Box::pin(async { Ok(None) })
163    }
164}
165
166/// One link in the ladder and what the ladder knows about its state.
167struct Rung {
168    transport: Box<dyn DynTransport>,
169    /// Whether the link delivers, so the ladder subscribes and listens on it.
170    listens: bool,
171    /// Whether the link is connected as far as the ladder has observed: set by a
172    /// successful connect, cleared when the link reports itself closed or ended.
173    connected: bool,
174    /// The filters live on this link since it last connected.
175    subscribed: Vec<String>,
176}
177
178impl Rung {
179    /// Forgets the connection and every subscription placed on it, so the next
180    /// [`connect`](TransportLadder::connect) starts the link over.
181    fn drop_link(&mut self) {
182        self.connected = false;
183        self.subscribed.clear();
184    }
185
186    /// Places `filter` on the link if it is not there already.
187    ///
188    /// A link answering [`Error::Closed`] is forgotten, so the filter is placed
189    /// again when it reconnects.
190    async fn place(&mut self, filter: &str) -> Result<()> {
191        if self.subscribed.iter().any(|placed| placed == filter) {
192            return Ok(());
193        }
194        match self.transport.subscribe(filter).await {
195            Ok(()) => {
196                self.subscribed.push(filter.to_owned());
197                Ok(())
198            }
199            Err(Error::Closed) => {
200                self.drop_link();
201                Err(Error::Closed)
202            }
203            Err(error) => Err(error),
204        }
205    }
206}
207
208/// Completes with the first rung to deliver, polling them in rotation.
209///
210/// Polling starts at a different rung each call so one busy link cannot starve the
211/// others, and every future but the one that completed is dropped, which is why
212/// [`Receive::recv`] has to be cancel-safe.
213struct FirstReady<'a> {
214    inbound: Vec<(usize, Inbound<'a>)>,
215    start: usize,
216}
217
218impl Future for FirstReady<'_> {
219    type Output = (usize, Result<Option<Message>>);
220
221    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
222        let this = &mut *self;
223        let count = this.inbound.len();
224        for offset in 0..count {
225            let (index, future) = &mut this.inbound[(this.start + offset) % count];
226            if let Poll::Ready(outcome) = future.as_mut().poll(cx) {
227                return Poll::Ready((*index, outcome));
228            }
229        }
230        Poll::Pending
231    }
232}
233
234/// An ordered set of transports backed by an offline buffer.
235///
236/// Rungs are tried in the order they are added, so the cheapest, most-preferred
237/// link is added first. A send that no rung accepts is buffered in the
238/// [`Store`] and replayed by [`flush`](Self::flush). The ladder is itself a
239/// [`Transport`] and a [`Receive`]: a subscription goes onto every rung that
240/// listens, and a receive takes whichever rung delivers first.
241pub struct TransportLadder<S> {
242    rungs: Vec<Rung>,
243    filters: Vec<String>,
244    buffer: S,
245    turn: usize,
246}
247
248impl<S: Store> TransportLadder<S> {
249    /// Creates an empty ladder that buffers into `buffer`.
250    ///
251    /// # Arguments
252    ///
253    /// * `buffer` - the durable queue that holds messages while no rung is
254    ///   reachable.
255    ///
256    /// # Returns
257    ///
258    /// A ladder with no rungs; add them with [`rung`](Self::rung) and
259    /// [`uplink`](Self::uplink).
260    pub fn new(buffer: S) -> Self {
261        Self {
262            rungs: Vec::new(),
263            filters: Vec::new(),
264            buffer,
265            turn: 0,
266        }
267    }
268
269    /// Adds a rung that sends and listens, lowest-cost first.
270    ///
271    /// # Arguments
272    ///
273    /// * `transport` - a link to try. Rungs added earlier are preferred, so add
274    ///   the cheapest link first and the costliest fallback last. The ladder
275    ///   subscribes on it and listens to it as well as sending over it.
276    ///
277    /// # Returns
278    ///
279    /// The ladder, for chaining.
280    pub fn rung(mut self, transport: impl Transport + Receive + Send + 'static) -> Self {
281        self.rungs.push(Rung {
282            transport: Box::new(Erased(transport)),
283            listens: true,
284            connected: false,
285            subscribed: Vec::new(),
286        });
287        self
288    }
289
290    /// Adds a rung that only sends, lowest-cost first.
291    ///
292    /// This is the shape of a LoRa uplink, a satellite messenger, or any link that
293    /// carries readings out but never a command back. The ladder sends over it in
294    /// turn with the other rungs and never subscribes or listens on it.
295    ///
296    /// # Arguments
297    ///
298    /// * `transport` - a send-only link to try, in the same cheapest-first order as
299    ///   [`rung`](Self::rung).
300    ///
301    /// # Returns
302    ///
303    /// The ladder, for chaining.
304    pub fn uplink(mut self, transport: impl Transport + Send + 'static) -> Self {
305        self.rungs.push(Rung {
306            transport: Box::new(Uplink(transport)),
307            listens: false,
308            connected: false,
309            subscribed: Vec::new(),
310        });
311        self
312    }
313
314    /// Connects every rung that is not connected, best-effort.
315    ///
316    /// A rung that fails to connect is left unreachable rather than failing the
317    /// whole ladder; sends simply fall through to the next rung or the buffer, and
318    /// the next call tries it again. A rung that connects receives every filter
319    /// the ladder has been asked to [`subscribe`](Self::subscribe) to, so a
320    /// subscription placed while a link was down takes effect when it returns.
321    ///
322    /// # Returns
323    ///
324    /// `Ok(())` once every rung has been given the chance to connect.
325    ///
326    /// # Errors
327    ///
328    /// This call is best-effort and currently always returns `Ok(())`.
329    pub async fn connect(&mut self) -> Result<()> {
330        for rung in self.rungs.iter_mut().filter(|rung| !rung.connected) {
331            if rung.transport.connect().await.is_err() {
332                continue;
333            }
334            rung.connected = true;
335            if !rung.listens {
336                continue;
337            }
338            for filter in &self.filters {
339                if matches!(rung.place(filter).await, Err(Error::Closed)) {
340                    break;
341                }
342            }
343        }
344        Ok(())
345    }
346
347    /// Sends a payload, falling back down the rungs and then to the buffer.
348    ///
349    /// If the buffer is empty, each connected rung is tried in order and the first
350    /// to accept the message delivers it. If every rung fails, or the buffer
351    /// already holds a backlog, the message is buffered to preserve order, and only
352    /// [`flush`](Self::flush) drains a backlog. A rung that answers
353    /// [`Error::Closed`] is treated as down until the next
354    /// [`connect`](Self::connect).
355    ///
356    /// # Arguments
357    ///
358    /// * `topic` - the destination topic.
359    /// * `payload` - the bytes to send.
360    ///
361    /// # Returns
362    ///
363    /// [`Delivery::Sent`] if a rung delivered the message, or [`Delivery::Buffered`]
364    /// if it was queued for a later [`flush`](Self::flush).
365    ///
366    /// # Errors
367    ///
368    /// Returns [`Error::Io`] if the message must be buffered
369    /// but the store cannot be written.
370    pub async fn send(&mut self, topic: &str, payload: &[u8]) -> Result<Delivery> {
371        if self.buffer.len().await? == 0 && Self::deliver(&mut self.rungs, topic, payload).await {
372            return Ok(Delivery::Sent);
373        }
374        self.buffer.append(&frame(topic, payload)).await?;
375        Ok(Delivery::Buffered)
376    }
377
378    /// Sends text down the ladder: a reading or a command written out.
379    ///
380    /// This is [`send`](Self::send) with the text's UTF-8 bytes, so a reading that is
381    /// already a number in words needs no encoding step; the far side reads it back with
382    /// [`Message::text`](pamoja_core::Message::text) or
383    /// [`Message::number`](pamoja_core::Message::number).
384    ///
385    /// # Arguments
386    ///
387    /// * `topic` - the destination topic.
388    /// * `text` - the text to send.
389    ///
390    /// # Returns
391    ///
392    /// [`Delivery::Sent`] if a rung delivered the message, or [`Delivery::Buffered`]
393    /// if it was queued for a later [`flush`](Self::flush).
394    ///
395    /// # Errors
396    ///
397    /// Whatever [`send`](Self::send) returns.
398    pub async fn send_text(&mut self, topic: &str, text: &str) -> Result<Delivery> {
399        self.send(topic, text.as_bytes()).await
400    }
401
402    /// Drains the buffer across the rungs, oldest record first.
403    ///
404    /// Each record is sent before it is removed, so the first record no rung can
405    /// deliver halts the drain and leaves it, and everything after it, buffered in
406    /// order for a later retry.
407    ///
408    /// # Returns
409    ///
410    /// The number of records forwarded before the buffer emptied or a rung refused
411    /// one.
412    ///
413    /// # Errors
414    ///
415    /// Returns [`Error::Io`] if the store cannot be read or
416    /// written, or [`Error::Codec`] if a buffered record
417    /// cannot be decoded.
418    pub async fn flush(&mut self) -> Result<usize> {
419        let mut forwarded = 0;
420        while let Some(record) = self.buffer.peek().await? {
421            let (topic, payload) = unframe(&record)?;
422            if !Self::deliver(&mut self.rungs, &topic, &payload).await {
423                break;
424            }
425            self.buffer.pop().await?;
426            forwarded += 1;
427        }
428        Ok(forwarded)
429    }
430
431    /// Returns how many messages are currently buffered.
432    ///
433    /// Takes the ladder mutably, like the rest of its surface. Reading through a
434    /// shared borrow would hold one across the await, which would in turn oblige
435    /// every rung to be `Sync` rather than only `Send`, and that is a heavier
436    /// requirement than a transport should have to meet.
437    ///
438    /// # Returns
439    ///
440    /// The number of records waiting for a [`flush`](Self::flush).
441    ///
442    /// # Errors
443    ///
444    /// Returns [`Error::Io`] if the store length cannot be
445    /// read.
446    pub async fn buffered(&mut self) -> Result<usize> {
447        self.buffer.len().await
448    }
449
450    /// Subscribes to a topic on every rung that listens.
451    ///
452    /// The filter is kept for the life of the ladder: a rung that is down when it
453    /// is placed, or that later drops and reconnects, receives it on its next
454    /// [`connect`](Self::connect). Subscribing while no rung is up therefore
455    /// succeeds, in the same way a send with no rung up is buffered rather than
456    /// refused.
457    ///
458    /// # Arguments
459    ///
460    /// * `topic` - the topic filter, in the syntax the rungs' links understand.
461    ///
462    /// # Returns
463    ///
464    /// `Ok(())` once the filter is live on at least one rung, or is held for the
465    /// rungs to pick up as they connect.
466    ///
467    /// # Errors
468    ///
469    /// Returns the rung's own [`Error::Transport`] if every connected rung refused
470    /// the filter. A rung that answers [`Error::Closed`] is treated as down rather
471    /// than as refusing.
472    pub async fn subscribe(&mut self, topic: &str) -> Result<()> {
473        if !self.filters.iter().any(|filter| filter == topic) {
474            self.filters.push(topic.to_owned());
475        }
476        let mut live = false;
477        let mut refusal = None;
478        for rung in self
479            .rungs
480            .iter_mut()
481            .filter(|rung| rung.listens && rung.connected)
482        {
483            match rung.place(topic).await {
484                Ok(()) => live = true,
485                Err(Error::Closed) => {}
486                Err(error) => refusal = Some(error),
487            }
488        }
489        match refusal {
490            Some(error) if !live => Err(error),
491            _ => Ok(()),
492        }
493    }
494
495    /// Awaits the next message from any rung that listens.
496    ///
497    /// Every connected, listening rung is polled together and the first to deliver
498    /// wins, starting from a different rung each call so a busy link cannot starve
499    /// a quiet one. A rung whose link ends, or that answers [`Error::Closed`], is
500    /// treated as down until the next [`connect`](Self::connect), and the wait
501    /// continues on the rungs that remain.
502    ///
503    /// The wait is cancel-safe when every rung's receive is, as the receives of the
504    /// links pamoja ships are: a wait given up, by a timeout or a `select!`, takes no
505    /// message. That is how a node that both listens and reports shares one ladder,
506    /// waiting with a limit and sending between waits.
507    ///
508    /// # Returns
509    ///
510    /// `Some(message)` for the next message any rung delivers. `None` is never
511    /// returned: a ladder's links come back with [`connect`](Self::connect), so it
512    /// reports a closed state rather than an ended one.
513    ///
514    /// # Errors
515    ///
516    /// Returns [`Error::Closed`] if no connected rung listens: none was added with
517    /// [`rung`](Self::rung), [`connect`](Self::connect) has not been called, or
518    /// every listening link has since ended. Returns a rung's own
519    /// [`Error::Transport`] if its link fails while waiting.
520    pub async fn recv(&mut self) -> Result<Option<Message>> {
521        loop {
522            let inbound: Vec<(usize, Inbound<'_>)> = self
523                .rungs
524                .iter_mut()
525                .enumerate()
526                .filter(|(_, rung)| rung.listens && rung.connected)
527                .map(|(index, rung)| (index, rung.transport.recv()))
528                .collect();
529            if inbound.is_empty() {
530                return Err(Error::Closed);
531            }
532            let start = self.turn % inbound.len();
533            self.turn = self.turn.wrapping_add(1);
534            let (index, outcome) = FirstReady { inbound, start }.await;
535            match outcome {
536                Ok(Some(message)) => return Ok(Some(message)),
537                Ok(None) | Err(Error::Closed) => self.rungs[index].drop_link(),
538                Err(error) => return Err(error),
539            }
540        }
541    }
542
543    /// Tries each connected rung in order, returning whether any accepted the
544    /// message. A rung that answers [`Error::Closed`] is dropped until the next
545    /// connect.
546    async fn deliver(rungs: &mut [Rung], topic: &str, payload: &[u8]) -> bool {
547        for rung in rungs.iter_mut().filter(|rung| rung.connected) {
548            match rung.transport.send(topic, payload).await {
549                Ok(()) => return true,
550                Err(Error::Closed) => rung.drop_link(),
551                Err(_) => {}
552            }
553        }
554        false
555    }
556}
557
558impl<S: Store + Send> Transport for TransportLadder<S> {
559    async fn connect(&mut self) -> Result<()> {
560        TransportLadder::connect(self).await
561    }
562
563    /// Sends through the ladder; a buffered message counts as accepted for
564    /// delivery, which is what the buffer is for.
565    async fn send(&mut self, topic: &str, payload: &[u8]) -> Result<()> {
566        TransportLadder::send(self, topic, payload)
567            .await
568            .map(|_| ())
569    }
570
571    async fn subscribe(&mut self, topic: &str) -> Result<()> {
572        TransportLadder::subscribe(self, topic).await
573    }
574}
575
576impl<S: Store + Send> Receive for TransportLadder<S> {
577    async fn recv(&mut self) -> Result<Option<Message>> {
578        TransportLadder::recv(self).await
579    }
580}
581
582/// Frames a topic and payload into one record for the buffer.
583///
584/// The layout is a four-byte big-endian topic length, the topic bytes, then the
585/// payload, so [`unframe`] can split them back apart.
586fn frame(topic: &str, payload: &[u8]) -> Vec<u8> {
587    let mut record = Vec::with_capacity(4 + topic.len() + payload.len());
588    record.extend_from_slice(&(topic.len() as u32).to_be_bytes());
589    record.extend_from_slice(topic.as_bytes());
590    record.extend_from_slice(payload);
591    record
592}
593
594/// Splits a buffered record back into its topic and payload.
595fn unframe(record: &[u8]) -> Result<(String, Vec<u8>)> {
596    let header: [u8; 4] = record
597        .get(..4)
598        .ok_or_else(|| Error::Codec("ladder record is missing its length header".to_owned()))?
599        .try_into()
600        .expect("a four-byte slice");
601    let topic_len = u32::from_be_bytes(header) as usize;
602    let topic_bytes = record
603        .get(4..4 + topic_len)
604        .ok_or_else(|| Error::Codec("ladder record topic is truncated".to_owned()))?;
605    let topic =
606        String::from_utf8(topic_bytes.to_vec()).map_err(|err| Error::Codec(err.to_string()))?;
607    let payload = record[4 + topic_len..].to_vec();
608    Ok((topic, payload))
609}
610
611#[cfg(test)]
612mod tests {
613    use super::*;
614
615    use std::collections::VecDeque;
616    use std::sync::{Arc, Mutex};
617    use std::time::Duration;
618
619    use pamoja_codec::CborCodec;
620    use pamoja_core::{Actuator, Sensor};
621    use pamoja_loopback::{Faulty, LoopbackBroker, LoopbackTransport};
622    use pamoja_profile::{ControlSpec, Node, PowerSchedule, Profile};
623    use pamoja_sync::MemoryStore;
624
625    /// Accepts anything that can move between threads.
626    fn assert_send<T: Send>(_value: T) {}
627
628    /// Accepts anything that is a full link.
629    fn assert_link<T: Transport + Receive>(_value: &T) {}
630
631    /// What a scripted link has been asked to do, shared with the test.
632    #[derive(Default)]
633    struct Log {
634        connects: usize,
635        subscriptions: Vec<String>,
636        sent: Vec<(String, Vec<u8>)>,
637        closed: bool,
638    }
639
640    /// A link whose inbound side plays a script: each entry is a delivered message
641    /// or the end of the link, and a link past its script waits forever.
642    struct Scripted {
643        inbound: VecDeque<Option<Message>>,
644        log: Arc<Mutex<Log>>,
645    }
646
647    impl Scripted {
648        fn new(inbound: impl IntoIterator<Item = Option<Message>>) -> (Self, Arc<Mutex<Log>>) {
649            let log = Arc::new(Mutex::new(Log::default()));
650            let link = Self {
651                inbound: inbound.into_iter().collect(),
652                log: Arc::clone(&log),
653            };
654            (link, log)
655        }
656    }
657
658    impl Transport for Scripted {
659        async fn connect(&mut self) -> Result<()> {
660            self.log.lock().expect("log").connects += 1;
661            Ok(())
662        }
663
664        async fn send(&mut self, topic: &str, payload: &[u8]) -> Result<()> {
665            let mut log = self.log.lock().expect("log");
666            if log.closed {
667                return Err(Error::Closed);
668            }
669            log.sent.push((topic.to_owned(), payload.to_vec()));
670            Ok(())
671        }
672
673        async fn subscribe(&mut self, topic: &str) -> Result<()> {
674            self.log
675                .lock()
676                .expect("log")
677                .subscriptions
678                .push(topic.to_owned());
679            Ok(())
680        }
681    }
682
683    impl Receive for Scripted {
684        async fn recv(&mut self) -> Result<Option<Message>> {
685            match self.inbound.pop_front() {
686                Some(next) => Ok(next),
687                None => std::future::pending().await,
688            }
689        }
690    }
691
692    #[test]
693    fn a_ladder_can_be_driven_from_a_spawned_task() {
694        // A gateway ticks its ladder from a task on a threaded runtime, so the
695        // futures have to be Send. This does not run them; it fails to compile
696        // if a rung ever stops promising it.
697        let mut ladder = TransportLadder::new(MemoryStore::new())
698            .rung(LoopbackTransport::new(LoopbackBroker::new()));
699        assert_send(ladder.connect());
700        assert_send(ladder.send("sensors/1", b"21.5"));
701        assert_send(ladder.flush());
702        assert_send(ladder.buffered());
703        assert_send(ladder.subscribe("commands/#"));
704        assert_send(ladder.recv());
705        assert_link(&ladder);
706    }
707
708    /// Subscribes a gateway to everything on a broker so the test can observe it.
709    async fn gateway(broker: &LoopbackBroker) -> LoopbackTransport {
710        let mut gateway = LoopbackTransport::new(broker.clone());
711        gateway.connect().await.expect("connect gateway");
712        gateway.subscribe("#").await.expect("subscribe gateway");
713        gateway
714    }
715
716    #[test]
717    fn frame_round_trips_topic_and_payload() {
718        let record = frame("sensors/1/temperature", b"21.5");
719        let (topic, payload) = unframe(&record).expect("unframe");
720        assert_eq!(topic, "sensors/1/temperature");
721        assert_eq!(payload, b"21.5");
722    }
723
724    #[test]
725    fn unframe_rejects_a_truncated_record() {
726        assert!(matches!(unframe(&[0, 0]), Err(Error::Codec(_))));
727        // Claims a four-byte topic but carries only one.
728        assert!(matches!(unframe(&[0, 0, 0, 4, b'a']), Err(Error::Codec(_))));
729    }
730
731    #[tokio::test]
732    async fn send_delivers_over_the_first_working_rung() {
733        let broker = LoopbackBroker::new();
734        let mut observer = gateway(&broker).await;
735
736        let mut ladder =
737            TransportLadder::new(MemoryStore::new()).rung(LoopbackTransport::new(broker.clone()));
738        ladder.connect().await.expect("connect");
739
740        let delivery = ladder
741            .send("sensors/1/temperature", b"21.5")
742            .await
743            .expect("send");
744        assert_eq!(delivery, Delivery::Sent);
745        assert_eq!(ladder.buffered().await.expect("buffered"), 0);
746
747        let message = observer.recv().await.expect("recv").expect("a message");
748        assert_eq!(message.topic, "sensors/1/temperature");
749        assert_eq!(message.payload, b"21.5");
750    }
751
752    #[tokio::test]
753    async fn send_falls_over_to_a_cheaper_rung_that_is_down() {
754        // The preferred rung publishes to its own broker but is broken; the
755        // fallback rung publishes to a second broker and works.
756        let preferred_broker = LoopbackBroker::new();
757        let fallback_broker = LoopbackBroker::new();
758        let mut preferred_observer = gateway(&preferred_broker).await;
759        let mut fallback_observer = gateway(&fallback_broker).await;
760
761        let preferred = Faulty::new(LoopbackTransport::new(preferred_broker.clone()), 1);
762        let fallback = LoopbackTransport::new(fallback_broker.clone());
763        let mut ladder = TransportLadder::new(MemoryStore::new())
764            .rung(preferred)
765            .rung(fallback);
766        ladder.connect().await.expect("connect");
767
768        let delivery = ladder.send("t", b"x").await.expect("send");
769        assert_eq!(delivery, Delivery::Sent);
770
771        let message = fallback_observer
772            .recv()
773            .await
774            .expect("recv")
775            .expect("a message");
776        assert_eq!(message.payload, b"x");
777        // The broken rung delivered nothing: its observer never sees a message.
778        let starved =
779            tokio::time::timeout(Duration::from_millis(50), preferred_observer.recv()).await;
780        assert!(
781            starved.is_err(),
782            "the broken rung must not deliver anything"
783        );
784    }
785
786    #[tokio::test]
787    async fn buffers_when_every_rung_is_down_then_flushes_in_order() {
788        let broker = LoopbackBroker::new();
789        let mut observer = gateway(&broker).await;
790
791        // One simulated outage on the only rung, then it recovers.
792        let rung = Faulty::new(LoopbackTransport::new(broker.clone()), 1);
793        let mut ladder = TransportLadder::new(MemoryStore::new()).rung(rung);
794        ladder.connect().await.expect("connect");
795
796        // First send hits the outage and buffers; the next two preserve order by
797        // buffering behind it rather than racing ahead.
798        assert_eq!(
799            ladder.send("out", b"a").await.expect("send"),
800            Delivery::Buffered
801        );
802        assert_eq!(
803            ladder.send("out", b"b").await.expect("send"),
804            Delivery::Buffered
805        );
806        assert_eq!(
807            ladder.send("out", b"c").await.expect("send"),
808            Delivery::Buffered
809        );
810        assert_eq!(ladder.buffered().await.expect("buffered"), 3);
811
812        // The link is back: drain everything in the order it was accepted.
813        let forwarded = ladder.flush().await.expect("flush");
814        assert_eq!(forwarded, 3);
815        assert_eq!(ladder.buffered().await.expect("buffered"), 0);
816
817        for expected in [b"a", b"b", b"c"] {
818            let message = observer.recv().await.expect("recv").expect("a message");
819            assert_eq!(message.topic, "out");
820            assert_eq!(message.payload, expected);
821        }
822    }
823
824    #[tokio::test]
825    async fn flush_stops_at_the_first_record_no_rung_accepts() {
826        let broker = LoopbackBroker::new();
827
828        // Three outages: the first buffers, the next two buffer behind it, and the
829        // flush attempt spends the remaining outages without draining anything.
830        let rung = Faulty::new(LoopbackTransport::new(broker.clone()), 3);
831        let mut ladder = TransportLadder::new(MemoryStore::new()).rung(rung);
832        ladder.connect().await.expect("connect");
833
834        ladder.send("out", b"a").await.expect("send");
835        ladder.send("out", b"b").await.expect("send");
836        ladder.send("out", b"c").await.expect("send");
837
838        // The link is still down on the first drained record, so nothing forwards
839        // and the backlog stays intact and ordered.
840        assert_eq!(ladder.flush().await.expect("flush"), 0);
841        assert_eq!(ladder.buffered().await.expect("buffered"), 3);
842    }
843
844    #[tokio::test]
845    async fn a_subscription_goes_on_every_rung_and_a_receive_takes_whichever_delivers() {
846        // Two rungs on two brokers, as a mesh hop and a backhaul would be.
847        let mesh = LoopbackBroker::new();
848        let backhaul = LoopbackBroker::new();
849        let mut ladder = TransportLadder::new(MemoryStore::new())
850            .rung(LoopbackTransport::new(mesh.clone()))
851            .rung(LoopbackTransport::new(backhaul.clone()));
852        ladder.connect().await.expect("connect");
853        ladder.subscribe("commands/#").await.expect("subscribe");
854
855        // A command published on either broker reaches the ladder.
856        let mut over_backhaul = LoopbackTransport::new(backhaul);
857        over_backhaul.connect().await.expect("connect");
858        over_backhaul
859            .send("commands/valve", b"open")
860            .await
861            .expect("send");
862        let command = ladder.recv().await.expect("recv").expect("a command");
863        assert_eq!(command.topic, "commands/valve");
864        assert_eq!(command.payload, b"open");
865
866        let mut over_mesh = LoopbackTransport::new(mesh);
867        over_mesh.connect().await.expect("connect");
868        over_mesh
869            .send("commands/valve", b"close")
870            .await
871            .expect("send");
872        let command = ladder.recv().await.expect("recv").expect("a command");
873        assert_eq!(command.payload, b"close");
874    }
875
876    #[tokio::test]
877    async fn a_filter_placed_while_no_rung_is_up_takes_effect_on_connect() {
878        let broker = LoopbackBroker::new();
879        let mut ladder =
880            TransportLadder::new(MemoryStore::new()).rung(LoopbackTransport::new(broker.clone()));
881
882        // Nothing is connected yet, so the filter is held rather than refused, and
883        // there is nothing to receive from.
884        ladder.subscribe("commands/#").await.expect("held");
885        assert!(matches!(ladder.recv().await, Err(Error::Closed)));
886
887        ladder.connect().await.expect("connect");
888        let mut publisher = LoopbackTransport::new(broker);
889        publisher.connect().await.expect("connect");
890        publisher.send("commands/1", b"go").await.expect("send");
891        let command = ladder.recv().await.expect("recv").expect("a command");
892        assert_eq!(command.payload, b"go");
893    }
894
895    #[tokio::test]
896    async fn an_uplink_sends_but_is_never_listened_on() {
897        let radio = LoopbackBroker::new();
898        let mut ground = gateway(&radio).await;
899        let mut ladder =
900            TransportLadder::new(MemoryStore::new()).uplink(LoopbackTransport::new(radio.clone()));
901        ladder.connect().await.expect("connect");
902
903        // A reading goes out over the uplink like any rung.
904        assert_eq!(
905            ladder.send("sensors/1", b"21.5").await.expect("send"),
906            Delivery::Sent
907        );
908        let reading = ground.recv().await.expect("recv").expect("a message");
909        assert_eq!(reading.payload, b"21.5");
910
911        // The uplink is not subscribed and not listened on: a message on its broker
912        // never reaches the ladder, which has no listening rung at all.
913        ladder.subscribe("commands/#").await.expect("held");
914        ground.send("commands/1", b"go").await.expect("send");
915        assert!(matches!(ladder.recv().await, Err(Error::Closed)));
916    }
917
918    #[tokio::test]
919    async fn receiving_rotates_between_rungs_and_drops_one_whose_link_ends() {
920        let (first, first_log) = Scripted::new([
921            Some(Message::new("c", b"first-1")),
922            None,
923            Some(Message::new("c", b"never")),
924        ]);
925        let (second, second_log) = Scripted::new([
926            Some(Message::new("c", b"second-1")),
927            Some(Message::new("c", b"second-2")),
928        ]);
929        let mut ladder = TransportLadder::new(MemoryStore::new())
930            .rung(first)
931            .rung(second);
932        ladder.connect().await.expect("connect");
933        ladder.subscribe("c").await.expect("subscribe");
934        assert_eq!(first_log.lock().expect("log").subscriptions, ["c"]);
935        assert_eq!(second_log.lock().expect("log").subscriptions, ["c"]);
936
937        // Both rungs are ready on every call; the starting rung rotates, so each
938        // gets a turn rather than the first always winning.
939        let one = ladder.recv().await.expect("recv").expect("a message");
940        let two = ladder.recv().await.expect("recv").expect("a message");
941        assert_eq!(one.payload, b"first-1");
942        assert_eq!(two.payload, b"second-1");
943
944        // The first link ends. The ladder drops it and keeps waiting on the second,
945        // whose next message arrives instead of the ended link's leftover.
946        let three = ladder.recv().await.expect("recv").expect("a message");
947        assert_eq!(three.payload, b"second-2");
948
949        // Connecting again brings the ended link back and places the filter on it
950        // afresh; the link that stayed up is not connected or subscribed twice.
951        ladder.connect().await.expect("connect");
952        assert_eq!(first_log.lock().expect("log").connects, 2);
953        assert_eq!(first_log.lock().expect("log").subscriptions, ["c", "c"]);
954        assert_eq!(second_log.lock().expect("log").connects, 1);
955        assert_eq!(second_log.lock().expect("log").subscriptions, ["c"]);
956        let four = ladder.recv().await.expect("recv").expect("a message");
957        assert_eq!(four.payload, b"never");
958    }
959
960    #[tokio::test]
961    async fn a_rung_that_reports_closed_on_send_is_down_until_the_next_connect() {
962        let (link, log) = Scripted::new([]);
963        let mut ladder = TransportLadder::new(MemoryStore::new()).rung(link);
964        ladder.connect().await.expect("connect");
965        assert_eq!(ladder.send("t", b"1").await.expect("send"), Delivery::Sent);
966
967        // The link closes underneath the ladder: the send buffers and the rung is
968        // not tried again until it is connected again.
969        log.lock().expect("log").closed = true;
970        assert_eq!(
971            ladder.send("t", b"2").await.expect("send"),
972            Delivery::Buffered
973        );
974        log.lock().expect("log").closed = false;
975        assert_eq!(ladder.flush().await.expect("flush"), 0);
976        assert_eq!(log.lock().expect("log").connects, 1);
977
978        ladder.connect().await.expect("connect");
979        assert_eq!(log.lock().expect("log").connects, 2);
980        assert_eq!(ladder.flush().await.expect("flush"), 1);
981        assert_eq!(log.lock().expect("log").sent.len(), 2);
982    }
983
984    /// A reading that is the same every time.
985    struct Probe(f32);
986
987    impl Sensor for Probe {
988        type Reading = f32;
989
990        async fn read(&mut self) -> Result<f32> {
991            Ok(self.0)
992        }
993    }
994
995    /// An output that records what it was told.
996    struct Valve(Vec<bool>);
997
998    impl Actuator for Valve {
999        type Command = bool;
1000
1001        async fn apply(&mut self, open: bool) -> Result<()> {
1002            self.0.push(open);
1003            Ok(())
1004        }
1005    }
1006
1007    #[tokio::test]
1008    async fn a_profile_node_publishes_through_a_ladder() {
1009        let broker = LoopbackBroker::new();
1010        let mut observer = gateway(&broker).await;
1011        let mut ladder =
1012            TransportLadder::new(MemoryStore::new()).rung(LoopbackTransport::new(broker));
1013        ladder.connect().await.expect("connect");
1014
1015        let profile = Profile {
1016            name: "raised-bed-drip".to_owned(),
1017            description: None,
1018            reads: None,
1019            topic: "garden/bed-1/moisture".to_owned(),
1020            control: ControlSpec::Setpoint {
1021                setpoint: 37.5,
1022                hysteresis: 7.5,
1023                cooling: false,
1024                safe_band: 30.0,
1025            },
1026            power: PowerSchedule::new(300, 1800, 3600),
1027            presentation: None,
1028        };
1029        let mut node = Node::new(profile, Probe(20.0), Valve(Vec::new()), ladder, CborCodec)
1030            .expect("a built-in kind");
1031        let reaction = node.tick().await.expect("a tick").reaction;
1032        assert_eq!(reaction.actuator, Some(true));
1033
1034        let reading = observer.recv().await.expect("recv").expect("a reading");
1035        assert_eq!(reading.topic, "garden/bed-1/moisture");
1036        assert!(!reading.payload.is_empty());
1037    }
1038}