use crate::broker::QoS;
use crate::topics::topic_matches;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
pub const SHARED_PREFIX: &str = "$share/";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LoadBalanceStrategy {
#[default]
RoundRobin,
Random,
LeastPending,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SharedSubscriber {
pub client_id: String,
pub qos: QoS,
}
impl SharedSubscriber {
pub fn new(client_id: impl Into<String>, qos: QoS) -> Self {
Self {
client_id: client_id.into(),
qos,
}
}
}
#[derive(Debug, Clone)]
pub struct SharedSubscription {
pub group_name: String,
pub topic_filter: String,
pub subscribers: Vec<SharedSubscriber>,
pub strategy: LoadBalanceStrategy,
pub rr_index: usize,
}
impl SharedSubscription {
pub fn new(group_name: impl Into<String>, topic_filter: impl Into<String>) -> Self {
Self {
group_name: group_name.into(),
topic_filter: topic_filter.into(),
subscribers: Vec::new(),
strategy: LoadBalanceStrategy::default(),
rr_index: 0,
}
}
pub fn with_strategy(mut self, strategy: LoadBalanceStrategy) -> Self {
self.strategy = strategy;
self
}
pub fn add_subscriber(&mut self, subscriber: SharedSubscriber) -> bool {
if self
.subscribers
.iter()
.any(|s| s.client_id == subscriber.client_id)
{
return false; }
self.subscribers.push(subscriber);
true
}
pub fn remove_subscriber(&mut self, client_id: &str) -> bool {
let before = self.subscribers.len();
self.subscribers.retain(|s| s.client_id != client_id);
self.subscribers.len() != before
}
pub fn upsert_subscriber(&mut self, client_id: &str, qos: QoS) {
if let Some(s) = self
.subscribers
.iter_mut()
.find(|s| s.client_id == client_id)
{
s.qos = qos;
} else {
self.subscribers.push(SharedSubscriber::new(client_id, qos));
}
}
pub fn subscriber_count(&self) -> usize {
self.subscribers.len()
}
pub fn contains(&self, client_id: &str) -> bool {
self.subscribers.iter().any(|s| s.client_id == client_id)
}
pub fn matches(&self, topic: &str) -> bool {
topic_matches(topic, &self.topic_filter)
}
pub fn select_next(&mut self) -> Option<usize> {
if self.subscribers.is_empty() {
return None;
}
let idx = match self.strategy {
LoadBalanceStrategy::RoundRobin => {
let i = self.rr_index % self.subscribers.len();
self.rr_index = self.rr_index.wrapping_add(1);
i
}
LoadBalanceStrategy::Random => {
let seed = self.rr_index.wrapping_add(self.subscribers.len());
seed % self.subscribers.len()
}
LoadBalanceStrategy::LeastPending => {
let i = self.rr_index % self.subscribers.len();
self.rr_index = self.rr_index.wrapping_add(1);
i
}
};
Some(idx)
}
}
pub fn parse_shared_filter(filter: &str) -> Option<(&str, &str)> {
let rest = filter.strip_prefix(SHARED_PREFIX)?;
let slash = rest.find('/')?;
let group = &rest[..slash];
let topic = &rest[slash + 1..];
if group.is_empty() || topic.is_empty() {
return None;
}
Some((group, topic))
}
pub fn is_shared_filter(filter: &str) -> bool {
parse_shared_filter(filter).is_some()
}
#[derive(Debug, Default)]
pub struct SharedSubscriptionRegistry {
subscriptions: Arc<RwLock<HashMap<String, SharedSubscription>>>,
}
impl SharedSubscriptionRegistry {
pub fn new() -> Self {
Self {
subscriptions: Arc::new(RwLock::new(HashMap::new())),
}
}
fn key(group: &str, filter: &str) -> String {
format!("{}/{}", group, filter)
}
pub async fn subscribe(
&self,
group: &str,
filter: &str,
client_id: &str,
qos: QoS,
) -> Result<(), String> {
let _ = crate::topics::TopicFilter::new(filter)
.map_err(|e| format!("invalid topic filter: {}", e))?;
let mut subs = self.subscriptions.write().await;
let key = Self::key(group, filter);
let entry = subs
.entry(key)
.or_insert_with(|| SharedSubscription::new(group, filter));
entry.upsert_subscriber(client_id, qos);
Ok(())
}
pub async fn unsubscribe(&self, group: &str, filter: &str, client_id: &str) -> bool {
let mut subs = self.subscriptions.write().await;
let key = Self::key(group, filter);
if let Some(sub) = subs.get_mut(&key) {
let removed = sub.remove_subscriber(client_id);
if sub.subscriber_count() == 0 {
subs.remove(&key);
}
return removed;
}
false
}
pub async fn unsubscribe_all(&self, client_id: &str) -> usize {
let mut subs = self.subscriptions.write().await;
let mut removed_groups = 0;
let mut empty_keys = Vec::new();
for sub in subs.values_mut() {
if sub.remove_subscriber(client_id) {
removed_groups += 1;
}
if sub.subscriber_count() == 0 {
empty_keys.push(Self::key(&sub.group_name, &sub.topic_filter));
}
}
for key in empty_keys {
subs.remove(&key);
}
removed_groups
}
pub async fn subscriber_count(&self, group: &str, filter: &str) -> usize {
let subs = self.subscriptions.read().await;
let key = Self::key(group, filter);
subs.get(&key).map(|s| s.subscriber_count()).unwrap_or(0)
}
pub async fn groups_matching(&self, topic: &str) -> Vec<(String, String, usize)> {
let subs = self.subscriptions.read().await;
let mut result: Vec<(String, String, usize)> = subs
.values()
.filter(|s| s.matches(topic))
.map(|s| {
(
s.group_name.clone(),
s.topic_filter.clone(),
s.subscriber_count(),
)
})
.collect();
result.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
result
}
pub async fn select_recipients(&self, topic: &str) -> Vec<(String, QoS)> {
let mut subs = self.subscriptions.write().await;
let mut recipients = Vec::new();
let matching_keys: Vec<String> = subs
.iter()
.filter(|(_, s)| s.matches(topic))
.map(|(k, _)| k.clone())
.collect();
for key in matching_keys {
if let Some(sub) = subs.get_mut(&key) {
if let Some(idx) = sub.select_next() {
let s = &sub.subscribers[idx];
recipients.push((s.client_id.clone(), s.qos));
}
}
}
recipients.sort_by(|a, b| a.0.cmp(&b.0));
recipients
}
pub async fn group_count(&self) -> usize {
let subs = self.subscriptions.read().await;
subs.len()
}
pub async fn list_groups(&self) -> Vec<(String, String, usize)> {
let subs = self.subscriptions.read().await;
let mut result: Vec<(String, String, usize)> = subs
.values()
.map(|s| {
(
s.group_name.clone(),
s.topic_filter.clone(),
s.subscriber_count(),
)
})
.collect();
result.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_shared_filter_valid() {
let (group, filter) = parse_shared_filter("$share/g1/home/+/temp").unwrap();
assert_eq!(group, "g1");
assert_eq!(filter, "home/+/temp");
}
#[test]
fn test_parse_shared_filter_no_prefix() {
assert!(parse_shared_filter("home/temp").is_none());
}
#[test]
fn test_parse_shared_filter_empty_group() {
assert!(parse_shared_filter("$share//home/temp").is_none());
}
#[test]
fn test_parse_shared_filter_empty_topic() {
assert!(parse_shared_filter("$share/g1/").is_none());
}
#[test]
fn test_is_shared_filter() {
assert!(is_shared_filter("$share/g/home/#"));
assert!(!is_shared_filter("home/#"));
}
#[test]
fn test_shared_subscriber_new() {
let s = SharedSubscriber::new("c1", QoS::AtLeastOnce);
assert_eq!(s.client_id, "c1");
assert_eq!(s.qos, QoS::AtLeastOnce);
}
#[test]
fn test_shared_subscription_new() {
let sub = SharedSubscription::new("g1", "home/#");
assert_eq!(sub.group_name, "g1");
assert_eq!(sub.topic_filter, "home/#");
assert_eq!(sub.strategy, LoadBalanceStrategy::RoundRobin);
assert_eq!(sub.subscriber_count(), 0);
}
#[test]
fn test_shared_subscription_add_subscriber() {
let mut sub = SharedSubscription::new("g1", "t");
assert!(sub.add_subscriber(SharedSubscriber::new("c1", QoS::AtMostOnce)));
assert!(sub.add_subscriber(SharedSubscriber::new("c2", QoS::AtLeastOnce)));
assert_eq!(sub.subscriber_count(), 2);
assert!(sub.contains("c1"));
assert!(sub.contains("c2"));
}
#[test]
fn test_shared_subscription_add_duplicate_returns_false() {
let mut sub = SharedSubscription::new("g1", "t");
sub.add_subscriber(SharedSubscriber::new("c1", QoS::AtMostOnce));
assert!(!sub.add_subscriber(SharedSubscriber::new("c1", QoS::AtLeastOnce)));
assert_eq!(sub.subscriber_count(), 1);
}
#[test]
fn test_shared_subscription_remove_subscriber() {
let mut sub = SharedSubscription::new("g1", "t");
sub.add_subscriber(SharedSubscriber::new("c1", QoS::AtMostOnce));
assert!(sub.remove_subscriber("c1"));
assert!(!sub.contains("c1"));
assert_eq!(sub.subscriber_count(), 0);
}
#[test]
fn test_shared_subscription_remove_missing_returns_false() {
let mut sub = SharedSubscription::new("g1", "t");
assert!(!sub.remove_subscriber("ghost"));
}
#[test]
fn test_shared_subscription_upsert_updates_qos() {
let mut sub = SharedSubscription::new("g1", "t");
sub.upsert_subscriber("c1", QoS::AtMostOnce);
sub.upsert_subscriber("c1", QoS::ExactlyOnce); assert_eq!(sub.subscriber_count(), 1);
assert_eq!(sub.subscribers[0].qos, QoS::ExactlyOnce);
}
#[test]
fn test_shared_subscription_upsert_adds_new() {
let mut sub = SharedSubscription::new("g1", "t");
sub.upsert_subscriber("c1", QoS::AtMostOnce);
sub.upsert_subscriber("c2", QoS::AtLeastOnce);
assert_eq!(sub.subscriber_count(), 2);
}
#[test]
fn test_shared_subscription_matches() {
let sub = SharedSubscription::new("g1", "home/+/temp");
assert!(sub.matches("home/living/temp"));
assert!(!sub.matches("office/temp"));
}
#[test]
fn test_select_next_empty_returns_none() {
let mut sub = SharedSubscription::new("g1", "t");
assert!(sub.select_next().is_none());
}
#[test]
fn test_select_next_round_robin_cycles() {
let mut sub = SharedSubscription::new("g1", "t");
sub.add_subscriber(SharedSubscriber::new("c1", QoS::AtMostOnce));
sub.add_subscriber(SharedSubscriber::new("c2", QoS::AtMostOnce));
sub.add_subscriber(SharedSubscriber::new("c3", QoS::AtMostOnce));
let i1 = sub.select_next().unwrap();
let i2 = sub.select_next().unwrap();
let i3 = sub.select_next().unwrap();
let i4 = sub.select_next().unwrap();
assert_eq!(i1, 0);
assert_eq!(i2, 1);
assert_eq!(i3, 2);
assert_eq!(i4, 0); }
#[test]
fn test_select_next_random_returns_valid_index() {
let mut sub = SharedSubscription::new("g1", "t").with_strategy(LoadBalanceStrategy::Random);
sub.add_subscriber(SharedSubscriber::new("c1", QoS::AtMostOnce));
sub.add_subscriber(SharedSubscriber::new("c2", QoS::AtMostOnce));
let idx = sub.select_next().unwrap();
assert!(idx < 2);
}
#[test]
fn test_with_strategy() {
let sub = SharedSubscription::new("g1", "t").with_strategy(LoadBalanceStrategy::Random);
assert_eq!(sub.strategy, LoadBalanceStrategy::Random);
}
#[tokio::test]
async fn test_registry_subscribe_creates_group() {
let reg = SharedSubscriptionRegistry::new();
reg.subscribe("g1", "home/#", "c1", QoS::AtMostOnce)
.await
.unwrap();
assert_eq!(reg.group_count().await, 1);
assert_eq!(reg.subscriber_count("g1", "home/#").await, 1);
}
#[tokio::test]
async fn test_registry_subscribe_adds_to_existing_group() {
let reg = SharedSubscriptionRegistry::new();
reg.subscribe("g1", "home/#", "c1", QoS::AtMostOnce)
.await
.unwrap();
reg.subscribe("g1", "home/#", "c2", QoS::AtLeastOnce)
.await
.unwrap();
assert_eq!(reg.subscriber_count("g1", "home/#").await, 2);
assert_eq!(reg.group_count().await, 1);
}
#[tokio::test]
async fn test_registry_subscribe_invalid_filter_fails() {
let reg = SharedSubscriptionRegistry::new();
let result = reg.subscribe("g1", "", "c1", QoS::AtMostOnce).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_registry_subscribe_updates_qos_for_existing() {
let reg = SharedSubscriptionRegistry::new();
reg.subscribe("g1", "t", "c1", QoS::AtMostOnce)
.await
.unwrap();
reg.subscribe("g1", "t", "c1", QoS::ExactlyOnce)
.await
.unwrap();
assert_eq!(reg.subscriber_count("g1", "t").await, 1);
let recipients = reg.select_recipients("t").await;
assert_eq!(recipients.len(), 1);
assert_eq!(recipients[0].1, QoS::ExactlyOnce);
}
#[tokio::test]
async fn test_registry_unsubscribe_removes_subscriber() {
let reg = SharedSubscriptionRegistry::new();
reg.subscribe("g1", "t", "c1", QoS::AtMostOnce)
.await
.unwrap();
reg.subscribe("g1", "t", "c2", QoS::AtMostOnce)
.await
.unwrap();
assert!(reg.unsubscribe("g1", "t", "c1").await);
assert_eq!(reg.subscriber_count("g1", "t").await, 1);
}
#[tokio::test]
async fn test_registry_unsubscribe_removes_empty_group() {
let reg = SharedSubscriptionRegistry::new();
reg.subscribe("g1", "t", "c1", QoS::AtMostOnce)
.await
.unwrap();
assert!(reg.unsubscribe("g1", "t", "c1").await);
assert_eq!(reg.group_count().await, 0);
}
#[tokio::test]
async fn test_registry_unsubscribe_missing_returns_false() {
let reg = SharedSubscriptionRegistry::new();
assert!(!reg.unsubscribe("g1", "t", "c1").await);
}
#[tokio::test]
async fn test_registry_unsubscribe_all() {
let reg = SharedSubscriptionRegistry::new();
reg.subscribe("g1", "t1", "c1", QoS::AtMostOnce)
.await
.unwrap();
reg.subscribe("g2", "t2", "c1", QoS::AtMostOnce)
.await
.unwrap();
reg.subscribe("g1", "t1", "c2", QoS::AtMostOnce)
.await
.unwrap();
let removed = reg.unsubscribe_all("c1").await;
assert_eq!(removed, 2);
assert_eq!(reg.group_count().await, 1); }
#[tokio::test]
async fn test_registry_groups_matching() {
let reg = SharedSubscriptionRegistry::new();
reg.subscribe("g1", "home/#", "c1", QoS::AtMostOnce)
.await
.unwrap();
reg.subscribe("g2", "office/#", "c2", QoS::AtMostOnce)
.await
.unwrap();
reg.subscribe("g3", "home/+/temp", "c3", QoS::AtMostOnce)
.await
.unwrap();
let matched = reg.groups_matching("home/living/temp").await;
assert_eq!(matched.len(), 2);
assert_eq!(matched[0].0, "g1");
assert_eq!(matched[1].0, "g3");
}
#[tokio::test]
async fn test_registry_groups_matching_none() {
let reg = SharedSubscriptionRegistry::new();
reg.subscribe("g1", "home/#", "c1", QoS::AtMostOnce)
.await
.unwrap();
let matched = reg.groups_matching("office/temp").await;
assert!(matched.is_empty());
}
#[tokio::test]
async fn test_registry_select_recipients_round_robin() {
let reg = SharedSubscriptionRegistry::new();
reg.subscribe("g1", "t", "c1", QoS::AtMostOnce)
.await
.unwrap();
reg.subscribe("g1", "t", "c2", QoS::AtMostOnce)
.await
.unwrap();
reg.subscribe("g1", "t", "c3", QoS::AtMostOnce)
.await
.unwrap();
let r1 = reg.select_recipients("t").await;
let r2 = reg.select_recipients("t").await;
let r3 = reg.select_recipients("t").await;
assert_eq!(r1.len(), 1);
assert_eq!(r2.len(), 1);
assert_eq!(r3.len(), 1);
let mut clients: Vec<String> = vec![r1[0].0.clone(), r2[0].0.clone(), r3[0].0.clone()];
clients.sort();
assert_eq!(clients, vec!["c1", "c2", "c3"]);
}
#[tokio::test]
async fn test_registry_select_recipients_multiple_groups() {
let reg = SharedSubscriptionRegistry::new();
reg.subscribe("g1", "t", "c1", QoS::AtMostOnce)
.await
.unwrap();
reg.subscribe("g2", "t", "c2", QoS::AtLeastOnce)
.await
.unwrap();
let recipients = reg.select_recipients("t").await;
assert_eq!(recipients.len(), 2); }
#[tokio::test]
async fn test_registry_select_recipients_no_match() {
let reg = SharedSubscriptionRegistry::new();
reg.subscribe("g1", "home/#", "c1", QoS::AtMostOnce)
.await
.unwrap();
let recipients = reg.select_recipients("office/temp").await;
assert!(recipients.is_empty());
}
#[tokio::test]
async fn test_registry_list_groups_sorted() {
let reg = SharedSubscriptionRegistry::new();
reg.subscribe("g2", "t2", "c1", QoS::AtMostOnce)
.await
.unwrap();
reg.subscribe("g1", "t1", "c2", QoS::AtMostOnce)
.await
.unwrap();
let groups = reg.list_groups().await;
assert_eq!(groups[0].0, "g1");
assert_eq!(groups[1].0, "g2");
}
#[tokio::test]
async fn test_registry_multiple_filters_same_group() {
let reg = SharedSubscriptionRegistry::new();
reg.subscribe("g1", "home/#", "c1", QoS::AtMostOnce)
.await
.unwrap();
reg.subscribe("g1", "office/#", "c1", QoS::AtMostOnce)
.await
.unwrap();
assert_eq!(reg.group_count().await, 2);
let removed = reg.unsubscribe_all("c1").await;
assert_eq!(removed, 2);
assert_eq!(reg.group_count().await, 0);
}
}