use std::collections::{BTreeMap, HashMap};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use bytes::Bytes;
use rumqttc::v5::mqttbytes::v5::{Publish, PublishProperties};
use rumqttc::v5::mqttbytes::{QoS, matches};
use ruststream::testing::Coordinator;
use ruststream::{OutgoingMessage, RawMessage};
use crate::broker::Link;
use crate::conn::{Shared, demultiplex};
use crate::error::MqttError;
use crate::filter::Qos;
use crate::message::{MqttMessage, headers_of, to_wire_properties};
use crate::publisher::check_topic;
use crate::registry::SubscribeRequest;
pub(crate) struct InFlight {
coordinator: Coordinator,
counted: bool,
}
impl InFlight {
fn new(coordinator: &Coordinator) -> Self {
coordinator.enqueued();
Self {
coordinator: coordinator.clone(),
counted: true,
}
}
pub(crate) fn suspend(&mut self) {
if std::mem::take(&mut self.counted) {
self.coordinator.consumed();
}
}
pub(crate) fn resume(&mut self) {
if !std::mem::replace(&mut self.counted, true) {
self.coordinator.enqueued();
}
}
}
impl Drop for InFlight {
fn drop(&mut self) {
if self.counted {
self.coordinator.consumed();
}
}
}
struct ServerSubscription {
wire_filter: String,
filter: String,
shared: bool,
qos: QoS,
identifier: Option<usize>,
}
impl ServerSubscription {
fn new(wire_filter: String, qos: QoS, identifier: Option<usize>) -> Self {
let group_filter = wire_filter
.strip_prefix("$share/")
.and_then(|rest| rest.split_once('/'))
.map(|(_, filter)| filter.to_owned());
Self {
shared: group_filter.is_some(),
filter: group_filter.unwrap_or_else(|| wire_filter.clone()),
wire_filter,
qos,
identifier,
}
}
}
#[derive(Default)]
struct Server {
subscriptions: Vec<ServerSubscription>,
retained: BTreeMap<String, Publish>,
log: HashMap<String, Vec<RawMessage>>,
}
pub(crate) struct Bus {
server: Mutex<Server>,
coordinator: OnceLock<Coordinator>,
closed: AtomicBool,
max_packet_size: u32,
}
impl std::fmt::Debug for Bus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Bus")
.field("max_packet_size", &self.max_packet_size)
.finish_non_exhaustive()
}
}
fn server_matches(topic: &str, filter: &str) -> bool {
topic.strip_prefix('$').map_or_else(
|| matches(topic, filter),
|topic| {
filter
.strip_prefix('$')
.is_some_and(|filter| matches(topic, filter))
},
)
}
fn named(packet: &mut Publish, identifier: Option<usize>) {
if let Some(identifier) = identifier {
packet
.properties
.get_or_insert_with(PublishProperties::default)
.subscription_identifiers = vec![identifier];
}
}
fn lesser(left: QoS, right: QoS) -> QoS {
if left <= right { left } else { right }
}
impl Bus {
pub(crate) fn new(max_packet_size: u32) -> Arc<Self> {
Arc::new(Self {
server: Mutex::new(Server::default()),
coordinator: OnceLock::new(),
closed: AtomicBool::new(false),
max_packet_size,
})
}
pub(crate) fn install(&self, coordinator: Coordinator) {
let _ = self.coordinator.set(coordinator);
}
pub(crate) fn is_open(&self) -> bool {
!self.closed.load(Ordering::Acquire)
}
pub(crate) fn close(&self) {
self.closed.store(true, Ordering::Release);
}
fn server(&self) -> std::sync::MutexGuard<'_, Server> {
self.server
.lock()
.expect("mqtt in-process server mutex poisoned")
}
pub(crate) fn subscribe(self: &Arc<Self>, shared: &Shared, request: &SubscribeRequest) {
let subscription =
ServerSubscription::new(request.filter.clone(), request.qos, request.identifier);
let identifier = subscription.identifier;
let dead = {
let mut server = self.server();
let retained = if subscription.shared || request.refresh {
Vec::new()
} else {
server
.retained
.iter()
.filter(|(topic, _)| server_matches(topic, &subscription.filter))
.map(|(_, packet)| {
let mut packet = packet.clone();
packet.qos = lesser(packet.qos, subscription.qos);
named(&mut packet, subscription.identifier);
packet
})
.collect()
};
match server
.subscriptions
.iter_mut()
.find(|held| held.wire_filter == subscription.wire_filter)
{
Some(held) => *held = subscription,
None => server.subscriptions.push(subscription),
}
let dead: Vec<u64> = retained
.iter()
.flat_map(|packet| self.hand_over(shared, packet, identifier))
.collect();
drop(server);
dead
};
self.forget(shared, dead);
}
pub(crate) fn release(&self, shared: &Shared, member: u64) {
let mut server = self.server();
shared.release_with(member, |wire_filter| {
server
.subscriptions
.retain(|held| held.wire_filter != wire_filter);
});
}
pub(crate) fn publish(
self: &Arc<Self>,
shared: &Shared,
topic: &str,
qos: Qos,
retain: bool,
payload: Bytes,
properties: Option<PublishProperties>,
) {
let mut packet = Publish::new(topic, qos.to_client(), payload, properties);
packet.retain = retain;
self.receive(shared, &packet);
}
pub(crate) fn inject(
self: &Arc<Self>,
shared: &Shared,
message: &OutgoingMessage<'_>,
) -> Result<(), MqttError> {
check_topic(message.name())?;
let packet = Publish::new(
message.name(),
Qos::default().to_client(),
Bytes::copy_from_slice(message.payload()),
to_wire_properties(message)?,
);
self.receive(shared, &packet);
Ok(())
}
pub(crate) fn published(&self, topic: &str) -> Vec<RawMessage> {
self.server().log.get(topic).cloned().unwrap_or_default()
}
fn receive(self: &Arc<Self>, shared: &Shared, packet: &Publish) {
let topic = String::from_utf8_lossy(&packet.topic).into_owned();
let packets: Vec<(Publish, Option<usize>)> = {
let mut server = self.server();
server.log.entry(topic.clone()).or_default().push(
RawMessage::new(topic.clone(), packet.payload.clone())
.with_headers(headers_of(packet)),
);
if packet.retain {
if packet.payload.is_empty() {
server.retained.remove(&topic);
} else {
server.retained.insert(topic.clone(), packet.clone());
}
}
server
.subscriptions
.iter()
.filter(|held| server_matches(&topic, &held.filter))
.map(|held| {
let mut copy = packet.clone();
copy.qos = lesser(packet.qos, held.qos);
copy.retain = false;
named(&mut copy, held.identifier);
(copy, held.identifier)
})
.collect()
};
for (packet, identifier) in packets {
self.send(shared, &packet, identifier);
}
}
fn send(self: &Arc<Self>, shared: &Shared, packet: &Publish, identifier: Option<usize>) {
let dead = self.hand_over(shared, packet, identifier);
self.forget(shared, dead);
}
fn forget(&self, shared: &Shared, dead: Vec<u64>) {
for member in dead {
self.release(shared, member);
}
}
fn hand_over(
self: &Arc<Self>,
shared: &Shared,
packet: &Publish,
identifier: Option<usize>,
) -> Vec<u64> {
let mut sized = packet.clone();
if sized.qos != QoS::AtMostOnce {
sized.pkid = 1;
}
if sized.size() > self.max_packet_size as usize {
tracing::debug!(
topic = %String::from_utf8_lossy(&packet.topic),
size = sized.size(),
max_packet_size = self.max_packet_size,
"mqtt in-process delivery discarded: larger than the session accepts"
);
return Vec::new();
}
let Ok(topic) = std::str::from_utf8(&packet.topic) else {
return Vec::new();
};
let coordinator = self.coordinator.get();
demultiplex(shared, topic, identifier.as_slice(), |acknowledges| {
MqttMessage::new(
topic.to_owned(),
packet,
acknowledges.then(|| Link::InProcess(Arc::clone(self))),
)
.counted(coordinator.map(InFlight::new))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_dollar_topic_is_reached_only_by_a_dollar_filter() {
assert!(server_matches("$SYS/broker/uptime", "$SYS/#"));
assert!(!server_matches("$SYS/broker/uptime", "#"));
assert!(!server_matches("$SYS/broker/uptime", "+/broker/uptime"));
assert!(server_matches(
"devices/dev42/telemetry",
"devices/+/telemetry"
));
assert!(server_matches("devices/dev42", "devices/#"));
assert!(!server_matches("devices/dev42/telemetry", "devices/+"));
}
#[test]
fn a_share_group_is_one_server_subscription_on_its_plain_filter() {
let shared =
ServerSubscription::new("$share/workers/jobs/+".to_owned(), QoS::AtLeastOnce, None);
assert!(shared.shared);
assert_eq!(shared.filter, "jobs/+");
let plain = ServerSubscription::new("jobs/+".to_owned(), QoS::AtLeastOnce, None);
assert!(!plain.shared);
assert_eq!(plain.filter, "jobs/+");
}
}