use std::collections::VecDeque;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use rumqttc::v5::mqttbytes::v5::{ConnectReturnCode, Packet, SubscribeReasonCode};
use rumqttc::v5::mqttbytes::{QoS, matches};
use rumqttc::v5::{AsyncClient, ConnectionError, Event, EventLoop, StateError};
use tokio::sync::{mpsc, oneshot};
use crate::error::MqttError;
use crate::message::MqttMessage;
pub(crate) struct SubEntry {
pub(crate) id: u64,
pub(crate) wire_filter: String,
pub(crate) match_filter: String,
pub(crate) qos: QoS,
pub(crate) tx: mpsc::UnboundedSender<Result<MqttMessage, MqttError>>,
}
struct PendingSub {
entry_id: u64,
filter: String,
pkid: Option<u16>,
done: oneshot::Sender<Result<(), MqttError>>,
}
pub(crate) struct Shared {
pub(crate) subs: Mutex<Vec<SubEntry>>,
pending: Mutex<VecDeque<PendingSub>>,
pub(crate) closed: AtomicBool,
next_id: AtomicU64,
round_robin: AtomicU64,
}
impl Shared {
pub(crate) fn new() -> Self {
Self {
subs: Mutex::new(Vec::new()),
pending: Mutex::new(VecDeque::new()),
closed: AtomicBool::new(false),
next_id: AtomicU64::new(0),
round_robin: AtomicU64::new(0),
}
}
pub(crate) fn ensure_open(&self) -> Result<(), MqttError> {
if self.closed.load(Ordering::Acquire) {
return Err(MqttError::NotConnected);
}
Ok(())
}
pub(crate) fn register(
&self,
wire_filter: String,
match_filter: String,
qos: QoS,
tx: mpsc::UnboundedSender<Result<MqttMessage, MqttError>>,
done: oneshot::Sender<Result<(), MqttError>>,
) -> u64 {
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
self.subs
.lock()
.expect("mqtt registry mutex poisoned")
.push(SubEntry {
id,
wire_filter: wire_filter.clone(),
match_filter,
qos,
tx,
});
self.pending
.lock()
.expect("mqtt pending mutex poisoned")
.push_back(PendingSub {
entry_id: id,
filter: wire_filter,
pkid: None,
done,
});
id
}
pub(crate) fn remove(&self, id: u64) -> Option<String> {
let mut subs = self.subs.lock().expect("mqtt registry mutex poisoned");
subs.iter()
.position(|entry| entry.id == id)
.map(|index| subs.swap_remove(index).wire_filter)
}
fn broadcast_error(&self, reason: &str) {
{
let subs = self.subs.lock().expect("mqtt registry mutex poisoned");
for entry in subs.iter() {
let _ = entry.tx.send(Err(MqttError::Receive(reason.to_owned())));
}
}
let mut pending = self.pending.lock().expect("mqtt pending mutex poisoned");
for sub in pending.drain(..) {
let _ = sub.done.send(Err(MqttError::Subscribe {
filter: sub.filter,
reason: reason.to_owned(),
}));
}
}
}
fn fatal_reason(err: &ConnectionError) -> Option<String> {
match err {
ConnectionError::ConnectionRefused(code) => match code {
ConnectReturnCode::ServerUnavailable
| ConnectReturnCode::ServerBusy
| ConnectReturnCode::ConnectionRateExceeded
| ConnectReturnCode::QuotaExceeded => None,
other => Some(format!("broker refused the connection: {other:?}")),
},
ConnectionError::MqttState(StateError::ServerDisconnect {
reason_code,
reason_string,
}) => Some(format!(
"broker disconnected the session: {reason_code:?} {reason_string:?}"
)),
ConnectionError::NotConnAck(_) => Some("the peer is not an MQTT broker".to_owned()),
_ => None,
}
}
pub(crate) struct Conn {
pub(crate) client: AsyncClient,
pub(crate) eventloop: EventLoop,
pub(crate) shared: Arc<Shared>,
pub(crate) first_connack: Option<oneshot::Sender<Result<(), MqttError>>>,
}
pub(crate) async fn run(mut conn: Conn) {
let mut backoff = Duration::from_millis(100);
loop {
if conn.shared.closed.load(Ordering::Acquire) {
break;
}
match conn.eventloop.poll().await {
Ok(Event::Incoming(packet)) => {
backoff = Duration::from_millis(100);
handle_incoming(&mut conn, packet);
}
Ok(Event::Outgoing(rumqttc::Outgoing::Subscribe(pkid))) => {
let mut pending = conn
.shared
.pending
.lock()
.expect("mqtt pending mutex poisoned");
if let Some(sub) = pending.iter_mut().find(|sub| sub.pkid.is_none()) {
sub.pkid = Some(pkid);
}
}
Ok(Event::Outgoing(_)) => {}
Err(err) => {
if conn.shared.closed.load(Ordering::Acquire) {
break;
}
if let Some(reason) = fatal_reason(&err) {
if let Some(done) = conn.first_connack.take() {
let _ = done.send(Err(MqttError::Connect(Box::from(reason.clone()))));
}
conn.shared.broadcast_error(&reason);
break;
}
tracing::debug!(error = %err, "mqtt connection error; backing off");
tokio::time::sleep(backoff).await;
backoff = (backoff * 2).min(Duration::from_secs(5));
}
}
}
}
#[allow(clippy::significant_drop_tightening)]
fn handle_incoming(conn: &mut Conn, packet: Packet) {
match packet {
Packet::ConnAck(connack) => {
if let Some(done) = conn.first_connack.take() {
let _ = done.send(Ok(()));
}
if !connack.session_present {
let subs = conn
.shared
.subs
.lock()
.expect("mqtt registry mutex poisoned");
for entry in subs.iter() {
if let Err(err) = conn
.client
.try_subscribe(entry.wire_filter.clone(), entry.qos)
{
tracing::warn!(filter = %entry.wire_filter, error = %err, "mqtt resubscribe failed");
}
}
}
}
Packet::SubAck(suback) => {
let pending_sub = {
let mut pending = conn
.shared
.pending
.lock()
.expect("mqtt pending mutex poisoned");
pending
.iter()
.position(|sub| sub.pkid == Some(suback.pkid))
.map(|index| pending.remove(index).expect("index just found"))
};
if let Some(sub) = pending_sub {
let outcome = match suback.return_codes.first() {
Some(SubscribeReasonCode::Success(_)) => Ok(()),
other => {
conn.shared.remove(sub.entry_id);
Err(MqttError::Subscribe {
filter: sub.filter,
reason: format!("broker rejected the subscription: {other:?}"),
})
}
};
let _ = sub.done.send(outcome);
}
}
Packet::Publish(publish) => {
let Ok(topic) = std::str::from_utf8(&publish.topic) else {
tracing::warn!("mqtt publish with non-utf8 topic dropped");
return;
};
let topic = topic.to_owned();
let mut dead = Vec::new();
{
let subs = conn
.shared
.subs
.lock()
.expect("mqtt registry mutex poisoned");
let mut groups: Vec<(&str, Vec<&SubEntry>)> = Vec::new();
for entry in subs.iter() {
if matches(&topic, &entry.match_filter) {
match groups
.iter_mut()
.find(|(wire, _)| *wire == entry.wire_filter)
{
Some((_, entries)) => entries.push(entry),
None => groups.push((&entry.wire_filter, vec![entry])),
}
}
}
let rotation =
usize::try_from(conn.shared.round_robin.fetch_add(1, Ordering::Relaxed))
.unwrap_or(0);
let mut acker = Some(conn.client.clone());
for (_, entries) in &groups {
let entry = entries[rotation % entries.len()];
let message = MqttMessage::new(topic.clone(), &publish, acker.take());
if entry.tx.send(Ok(message)).is_err() {
dead.push(entry.id);
}
}
}
for id in dead {
if let Some(filter) = conn.shared.remove(id) {
let _ = conn.client.try_unsubscribe(filter);
}
}
}
_ => {}
}
}