use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicU16, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tokio::io::{AsyncWriteExt, WriteHalf};
use tokio::sync::{Mutex as AsyncMutex, oneshot};
use tokio::task::JoinHandle;
use crate::error::{MqttError, MqttResult};
use crate::protocol::MqttVersion;
use crate::protocol::packet::Packet;
use crate::support::{EventQueue, LockExt, guard_callback};
use super::tls::Transport;
use super::types::{MqttMessage, MqttMessageListener};
#[derive(Default)]
pub(super) struct Delivery {
pub listener: Mutex<Option<Arc<dyn MqttMessageListener>>>,
pub queue: EventQueue<MqttMessage>,
pub last_disconnect: Mutex<Option<String>>,
}
pub(super) type ListenerSlot = Arc<Delivery>;
pub(super) struct Inner {
pub version: MqttVersion,
pub writer: AsyncMutex<Option<WriteHalf<Box<dyn Transport>>>>,
next_packet_id: AtomicU16,
pub pending: Mutex<HashMap<u16, oneshot::Sender<Packet>>>,
pub incoming_qos2: Mutex<HashMap<u16, MqttMessage>>,
listener: ListenerSlot,
pub connected: AtomicBool,
pub read_task: AsyncMutex<Option<JoinHandle<()>>>,
pub keepalive_task: AsyncMutex<Option<JoinHandle<()>>>,
pub operation_timeout: Duration,
last_rx: Mutex<Instant>,
busy: AtomicBool,
}
impl Inner {
pub fn new(
version: MqttVersion,
writer: WriteHalf<Box<dyn Transport>>,
listener: ListenerSlot,
operation_timeout: Duration,
) -> Self {
Inner {
version,
writer: AsyncMutex::new(Some(writer)),
next_packet_id: AtomicU16::new(1),
pending: Mutex::new(HashMap::new()),
incoming_qos2: Mutex::new(HashMap::new()),
listener,
connected: AtomicBool::new(true),
read_task: AsyncMutex::new(None),
keepalive_task: AsyncMutex::new(None),
operation_timeout,
last_rx: Mutex::new(Instant::now()),
busy: AtomicBool::new(false),
}
}
pub fn is_connected(&self) -> bool {
self.connected.load(Ordering::Relaxed)
}
pub fn alloc_packet_id(&self) -> u16 {
let pending = self.pending.lock_safe();
let mut id = 0;
for _ in 0..=u16::MAX {
id = self.next_packet_id.fetch_add(1, Ordering::Relaxed);
if id != 0 && !pending.contains_key(&id) {
break;
}
}
id
}
pub fn take_listener(&self) -> Option<Arc<dyn MqttMessageListener>> {
self.listener.listener.lock_safe().clone()
}
pub fn touch(&self) {
*self.last_rx.lock_safe() = Instant::now();
}
pub fn set_busy(&self, busy: bool) {
self.busy.store(busy, Ordering::Relaxed);
}
pub fn is_busy(&self) -> bool {
self.busy.load(Ordering::Relaxed)
}
pub fn idle_for(&self) -> Duration {
self.last_rx.lock_safe().elapsed()
}
pub fn shutdown_now(&self) {
self.connected.store(false, Ordering::Relaxed);
self.pending.lock_safe().clear();
if let Ok(mut writer) = self.writer.try_lock() {
writer.take();
}
for task in [&self.read_task, &self.keepalive_task] {
if let Ok(mut task) = task.try_lock()
&& let Some(handle) = task.take()
{
handle.abort();
}
}
}
pub async fn shutdown(&self) {
self.connected.store(false, Ordering::Relaxed);
self.pending.lock_safe().clear();
if let Some(mut w) = self.writer.lock().await.take() {
let _ = w.shutdown().await;
}
for task in [&self.read_task, &self.keepalive_task] {
if let Some(handle) = task.lock().await.take() {
handle.abort();
}
}
}
}
pub(super) fn register_pending(inner: &Arc<Inner>, id: u16) -> oneshot::Receiver<Packet> {
let (tx, rx) = oneshot::channel();
inner.pending.lock_safe().insert(id, tx);
rx
}
pub(super) fn forget_pending(inner: &Arc<Inner>, id: u16) {
inner.pending.lock_safe().remove(&id);
}
pub(super) fn resolve_pending(inner: &Arc<Inner>, id: u16, packet: Packet) {
if let Some(tx) = inner.pending.lock_safe().remove(&id) {
let _ = tx.send(packet);
}
}
pub(super) async fn wait_for(
inner: &Arc<Inner>,
id: u16,
rx: oneshot::Receiver<Packet>,
) -> MqttResult<Packet> {
match tokio::time::timeout(inner.operation_timeout, rx).await {
Ok(Ok(packet)) => Ok(packet),
Ok(Err(_)) => Err(MqttError::Session(
"connection closed while waiting for ack".into(),
)),
Err(_) => {
forget_pending(inner, id);
Err(MqttError::Timeout)
}
}
}
pub(super) fn unexpected(packet: &Packet) -> MqttError {
MqttError::MalformedPacket(format!("unexpected packet while awaiting ack: {packet:?}"))
}
pub(super) fn deliver(inner: &Arc<Inner>, message: MqttMessage) {
if inner.listener.queue.is_enabled() {
inner.listener.queue.push(message.clone());
}
if let Some(listener) = inner.take_listener() {
guard_callback("on_message", || listener.on_message(message));
}
}
pub(super) fn finish_disconnected(inner: &Arc<Inner>, reason: String) {
let was_connected = inner.connected.swap(false, Ordering::Relaxed);
inner.pending.lock_safe().clear();
if !was_connected {
return;
}
*inner.listener.last_disconnect.lock_safe() = Some(reason.clone());
if let Some(listener) = inner.take_listener() {
guard_callback("on_disconnected", || listener.on_disconnected(reason));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::QoS;
use std::sync::atomic::AtomicUsize;
fn inner(timeout: Duration) -> (Arc<Inner>, ListenerSlot) {
let (a, _b) = tokio::io::duplex(64);
let transport: Box<dyn Transport> = Box::new(a);
let (_r, w) = tokio::io::split(transport);
let slot: ListenerSlot = Arc::default();
(
Arc::new(Inner::new(MqttVersion::V5, w, slot.clone(), timeout)),
slot,
)
}
#[derive(Default)]
struct Recorder {
messages: AtomicUsize,
disconnects: Mutex<Vec<String>>,
}
impl MqttMessageListener for Recorder {
fn on_message(&self, _: MqttMessage) {
self.messages.fetch_add(1, Ordering::Relaxed);
}
fn on_disconnected(&self, reason: String) {
self.disconnects.lock_safe().push(reason);
}
}
fn message() -> MqttMessage {
MqttMessage {
topic: "t".into(),
payload: vec![1],
qos: QoS::AtMostOnce,
retain: false,
}
}
#[tokio::test]
async fn packet_ids_are_nonzero_and_skip_in_flight() {
let (inner, _) = inner(Duration::from_secs(1));
let _rx = register_pending(&inner, 2);
let ids: Vec<u16> = (0..4).map(|_| inner.alloc_packet_id()).collect();
assert!(ids.iter().all(|&i| i != 0));
assert!(!ids.contains(&2), "in-flight id must not be reused");
}
#[tokio::test]
async fn packet_ids_wrap_past_zero() {
let (inner, _) = inner(Duration::from_secs(1));
inner.next_packet_id.store(u16::MAX, Ordering::Relaxed);
assert_eq!(inner.alloc_packet_id(), u16::MAX);
assert_eq!(inner.alloc_packet_id(), 1, "0 is skipped on wrap");
}
#[tokio::test]
async fn resolve_completes_the_waiter() {
let (inner, _) = inner(Duration::from_secs(1));
let rx = register_pending(&inner, 7);
resolve_pending(&inner, 7, Packet::PingResp);
assert!(matches!(
wait_for(&inner, 7, rx).await,
Ok(Packet::PingResp)
));
resolve_pending(&inner, 7, Packet::PingResp); }
#[tokio::test(start_paused = true)]
async fn wait_times_out_and_forgets_the_id() {
let (inner, _) = inner(Duration::from_millis(50));
let rx = register_pending(&inner, 9);
assert!(matches!(
wait_for(&inner, 9, rx).await,
Err(MqttError::Timeout)
));
assert!(inner.pending.lock_safe().is_empty());
}
#[tokio::test]
async fn disconnect_fails_waiters_and_notifies_exactly_once() {
let (inner, slot) = inner(Duration::from_secs(5));
let recorder = Arc::new(Recorder::default());
*slot.listener.lock_safe() = Some(recorder.clone());
let rx = register_pending(&inner, 3);
finish_disconnected(&inner, "lost".into());
finish_disconnected(&inner, "again".into());
assert!(matches!(
wait_for(&inner, 3, rx).await,
Err(MqttError::Session(_))
));
assert!(!inner.is_connected());
assert_eq!(*recorder.disconnects.lock_safe(), ["lost"]);
assert_eq!(slot.last_disconnect.lock_safe().as_deref(), Some("lost"));
}
#[tokio::test]
async fn deliver_reaches_listener_and_pull_queue() {
let (inner, slot) = inner(Duration::from_secs(1));
let recorder = Arc::new(Recorder::default());
*slot.listener.lock_safe() = Some(recorder.clone());
slot.queue.enable(4);
deliver(&inner, message());
assert_eq!(recorder.messages.load(Ordering::Relaxed), 1);
assert!(slot.queue.is_enabled());
}
#[tokio::test]
async fn shutdown_marks_disconnected_and_drops_writer() {
let (inner, _) = inner(Duration::from_secs(1));
inner.shutdown().await;
assert!(!inner.is_connected());
assert!(inner.writer.lock().await.is_none());
}
#[tokio::test]
async fn idle_time_resets_on_touch() {
let (inner, _) = inner(Duration::from_secs(1));
tokio::time::sleep(Duration::from_millis(20)).await;
assert!(inner.idle_for() >= Duration::from_millis(20));
inner.touch();
assert!(inner.idle_for() < Duration::from_millis(20));
}
}