Skip to main content

pamoja_mqtt/
delivery.rs

1//! Following each published message until the broker acknowledges it.
2//!
3//! `rumqttc` hands out no packet identifier when a message is published, so the event
4//! loop's own record is read instead. It takes publish requests in order and reports each
5//! as it goes out, with the identifier it chose, and later reports the broker's `PUBACK`
6//! or `PUBCOMP` under that identifier. Every publish joins a queue as it is requested; the
7//! queue's head is matched to the next outgoing report, and the acknowledgment settles it.
8
9use std::collections::{HashMap, VecDeque};
10use std::sync::{Arc, Mutex, PoisonError};
11
12use pamoja_core::{Error, Result};
13use rumqttc::QoS;
14use tokio::sync::oneshot;
15
16/// The broker's acknowledgment of one published message, still to come.
17///
18/// A message published at QoS 1 is settled by the broker's `PUBACK`, and one at QoS 2 by
19/// its `PUBCOMP`, the last step of the exchange that delivers it exactly once (OASIS MQTT
20/// 3.1.1, sections 4.3.2 and 4.3.3). MQTT acknowledges nothing at QoS 0, so such a message
21/// is settled once the connection has taken it.
22#[derive(Debug)]
23#[must_use = "a delivery says nothing until it is awaited"]
24pub struct Delivery {
25    settled: oneshot::Receiver<Result<()>>,
26}
27
28impl Delivery {
29    /// Waits for the broker to acknowledge the message.
30    ///
31    /// # Returns
32    ///
33    /// `Ok(())` once the broker holds the message.
34    ///
35    /// # Errors
36    ///
37    /// Returns [`Error::Transport`] if the connection ended before the acknowledgment
38    /// arrived, or [`Error::Closed`] if the transport was disconnected first. Either way
39    /// the message may or may not have reached the broker.
40    pub async fn confirmed(self) -> Result<()> {
41        self.settled.await.unwrap_or(Err(Error::Closed))
42    }
43}
44
45struct Waiting {
46    qos: QoS,
47    settle: oneshot::Sender<Result<()>>,
48}
49
50/// The publishes the event loop has yet to report, shared by the transport that requests
51/// them and the task that runs the loop.
52#[derive(Clone, Default)]
53pub(crate) struct Requested(Arc<Mutex<VecDeque<Waiting>>>);
54
55impl Requested {
56    /// Queues a publish about to be requested.
57    pub(crate) fn push(&self, qos: QoS) -> Delivery {
58        let (settle, settled) = oneshot::channel();
59        self.lock().push_back(Waiting { qos, settle });
60        Delivery { settled }
61    }
62
63    /// Takes back the publish queued last, when its request never reached the loop.
64    pub(crate) fn withdraw(&self) {
65        self.lock().pop_back();
66    }
67
68    fn lock(&self) -> std::sync::MutexGuard<'_, VecDeque<Waiting>> {
69        self.0.lock().unwrap_or_else(PoisonError::into_inner)
70    }
71}
72
73/// What the event loop's task keeps: publishes that went out and wait for their
74/// acknowledgment, by packet identifier.
75#[derive(Default)]
76pub(crate) struct InFlight {
77    by_id: HashMap<u16, oneshot::Sender<Result<()>>>,
78}
79
80impl InFlight {
81    /// Matches the loop's report of an outgoing publish to the request at the queue's head.
82    pub(crate) fn sent(&mut self, requested: &Requested, pkid: u16) {
83        let Some(waiting) = requested.lock().pop_front() else {
84            return;
85        };
86        if waiting.qos == QoS::AtMostOnce {
87            let _ = waiting.settle.send(Ok(()));
88        } else {
89            self.by_id.insert(pkid, waiting.settle);
90        }
91    }
92
93    /// Settles the publish the broker acknowledged.
94    pub(crate) fn acknowledged(&mut self, pkid: u16) {
95        if let Some(settle) = self.by_id.remove(&pkid) {
96            let _ = settle.send(Ok(()));
97        }
98    }
99
100    /// Fails every publish still waiting, when the connection ends.
101    pub(crate) fn fail(&mut self, requested: &Requested, reason: &str) {
102        let waiting = requested
103            .lock()
104            .drain(..)
105            .map(|waiting| waiting.settle)
106            .collect::<Vec<_>>();
107        for settle in self.by_id.drain().map(|(_, settle)| settle).chain(waiting) {
108            let _ = settle.send(Err(Error::Transport(format!(
109                "the connection ended before the broker acknowledged the message: {reason}"
110            ))));
111        }
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[tokio::test]
120    async fn each_report_settles_the_request_it_belongs_to() {
121        let requested = Requested::default();
122        let mut in_flight = InFlight::default();
123        let quick = requested.push(QoS::AtMostOnce);
124        let once = requested.push(QoS::AtLeastOnce);
125        let exact = requested.push(QoS::ExactlyOnce);
126
127        in_flight.sent(&requested, 0);
128        in_flight.sent(&requested, 7);
129        in_flight.sent(&requested, 8);
130        quick
131            .confirmed()
132            .await
133            .expect("QoS 0 settles once it goes out");
134
135        in_flight.acknowledged(8);
136        in_flight.acknowledged(7);
137        exact
138            .confirmed()
139            .await
140            .expect("settled by its own identifier");
141        once.confirmed()
142            .await
143            .expect("whatever order the answers come in");
144    }
145
146    #[tokio::test]
147    async fn an_ended_connection_fails_what_is_still_waiting() {
148        let requested = Requested::default();
149        let mut in_flight = InFlight::default();
150        let out = requested.push(QoS::AtLeastOnce);
151        let queued = requested.push(QoS::AtLeastOnce);
152        in_flight.sent(&requested, 1);
153        in_flight.fail(&requested, "the broker went away");
154        for delivery in [out, queued] {
155            match delivery.confirmed().await {
156                Err(Error::Transport(reason)) => assert!(reason.contains("went away"), "{reason}"),
157                other => panic!("a delivery cut off is refused, got {other:?}"),
158            }
159        }
160    }
161
162    #[tokio::test]
163    async fn a_withdrawn_request_is_closed() {
164        let requested = Requested::default();
165        let delivery = requested.push(QoS::AtLeastOnce);
166        requested.withdraw();
167        assert!(matches!(delivery.confirmed().await, Err(Error::Closed)));
168    }
169}