use chrono::{DateTime, Utc};
use dashmap::DashMap;
use serde_json::Value;
use tokio::sync::broadcast;
const CHANNEL_CAPACITY: usize = 256;
const MAX_SUBSCRIPTION_PATTERNS: usize = 10_000;
#[derive(Debug, Clone)]
pub struct TopicMessage {
pub topic: String,
pub payload: Value,
pub timestamp: DateTime<Utc>,
}
pub struct PubSub {
subscriptions: DashMap<String, broadcast::Sender<TopicMessage>>,
}
impl PubSub {
pub fn new() -> Self {
Self {
subscriptions: DashMap::new(),
}
}
pub fn subscribe(&self, pattern: &str) -> Option<broadcast::Receiver<TopicMessage>> {
if let Some(entry) = self.subscriptions.get(pattern) {
return Some(entry.subscribe());
}
if self.subscriptions.len() >= MAX_SUBSCRIPTION_PATTERNS {
tracing::warn!(
pattern,
limit = MAX_SUBSCRIPTION_PATTERNS,
"pubsub subscription rejected: pattern limit reached"
);
return None;
}
Some(
self.subscriptions
.entry(pattern.to_owned())
.or_insert_with(|| broadcast::channel(CHANNEL_CAPACITY).0)
.subscribe(),
)
}
pub fn publish(&self, topic: &str, payload: Value) {
let msg = TopicMessage {
topic: topic.to_owned(),
payload,
timestamp: Utc::now(),
};
for entry in self.subscriptions.iter() {
let pattern = entry.key();
if matches_pattern(pattern, topic) {
let _ = entry.value().send(msg.clone());
}
}
}
pub fn unsubscribe_all(&self, pattern: &str) {
self.subscriptions.remove(pattern);
}
#[inline]
#[must_use]
pub fn pattern_count(&self) -> usize {
self.subscriptions.len()
}
}
impl Default for PubSub {
fn default() -> Self {
Self::new()
}
}
#[must_use]
pub fn matches_pattern(pattern: &str, topic: &str) -> bool {
let mut pat_buf: [&str; 16] = [""; 16];
let mut topic_buf: [&str; 16] = [""; 16];
let pat_count = pattern.split('.').count();
let topic_count = topic.split('.').count();
if pat_count <= 16 && topic_count <= 16 {
for (i, seg) in pattern.split('.').enumerate() {
pat_buf[i] = seg;
}
for (i, seg) in topic.split('.').enumerate() {
topic_buf[i] = seg;
}
matches_recursive(&pat_buf[..pat_count], &topic_buf[..topic_count])
} else {
let pat_segments: Vec<&str> = pattern.split('.').collect();
let topic_segments: Vec<&str> = topic.split('.').collect();
matches_recursive(&pat_segments, &topic_segments)
}
}
const MAX_MATCH_DEPTH: usize = 32;
fn matches_recursive(pattern: &[&str], topic: &[&str]) -> bool {
matches_recursive_inner(pattern, topic, 0)
}
fn matches_recursive_inner(pattern: &[&str], topic: &[&str], depth: usize) -> bool {
if depth > MAX_MATCH_DEPTH {
return false;
}
match (pattern.first(), topic.first()) {
(None, None) => true,
(None, Some(_)) => false,
(Some(&"#"), _) => {
let rest_pat = &pattern[1..];
if rest_pat.is_empty() {
return true;
}
for i in 0..=topic.len() {
if matches_recursive_inner(rest_pat, &topic[i..], depth + 1) {
return true;
}
}
false
}
(Some(_), None) => pattern.iter().all(|&s| s == "#"),
(Some(&"*"), Some(_)) => matches_recursive_inner(&pattern[1..], &topic[1..], depth + 1),
(Some(p), Some(t)) => {
if *p == *t {
matches_recursive_inner(&pattern[1..], &topic[1..], depth + 1)
} else {
false
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn exact_match() {
assert!(matches_pattern("task.completed", "task.completed"));
}
#[test]
fn exact_no_match() {
assert!(!matches_pattern("task.completed", "task.failed"));
}
#[test]
fn star_matches_one_segment() {
assert!(matches_pattern("task.*", "task.completed"));
assert!(matches_pattern("task.*", "task.failed"));
}
#[test]
fn star_does_not_match_zero_segments() {
assert!(!matches_pattern("task.*", "task"));
}
#[test]
fn star_does_not_match_multiple_segments() {
assert!(!matches_pattern("task.*", "task.sub.completed"));
}
#[test]
fn star_in_middle() {
assert!(matches_pattern("task.*.done", "task.build.done"));
assert!(!matches_pattern("task.*.done", "task.build.not_done"));
assert!(!matches_pattern("task.*.done", "task.a.b.done"));
}
#[test]
fn hash_matches_zero_segments() {
assert!(matches_pattern("agent.#", "agent"));
}
#[test]
fn hash_matches_one_segment() {
assert!(matches_pattern("agent.#", "agent.status"));
}
#[test]
fn hash_matches_multiple_segments() {
assert!(matches_pattern("agent.#", "agent.status.changed"));
assert!(matches_pattern("agent.#", "agent.a.b.c.d"));
}
#[test]
fn hash_alone_matches_everything() {
assert!(matches_pattern("#", "anything"));
assert!(matches_pattern("#", "a.b.c"));
assert!(matches_pattern("#", ""));
}
#[test]
fn hash_in_middle() {
assert!(matches_pattern("task.#.done", "task.done"));
assert!(matches_pattern("task.#.done", "task.build.done"));
assert!(matches_pattern("task.#.done", "task.a.b.done"));
assert!(!matches_pattern("task.#.done", "task.a.b.failed"));
}
#[test]
fn mixed_wildcards() {
assert!(matches_pattern("*.#.done", "task.a.b.done"));
assert!(matches_pattern("*.status.#", "agent.status"));
assert!(matches_pattern("*.status.#", "agent.status.changed"));
}
#[test]
fn no_match_different_prefix() {
assert!(!matches_pattern("task.*", "agent.completed"));
}
#[test]
fn no_match_shorter_topic() {
assert!(!matches_pattern("task.sub.*", "task"));
}
#[tokio::test]
async fn publish_reaches_exact_subscriber() {
let ps = PubSub::new();
let mut rx = ps.subscribe("task.completed").unwrap();
ps.publish("task.completed", json!({"id": 1}));
let msg = rx.recv().await.unwrap();
assert_eq!(msg.topic, "task.completed");
assert_eq!(msg.payload, json!({"id": 1}));
}
#[tokio::test]
async fn publish_reaches_wildcard_subscriber() {
let ps = PubSub::new();
let mut rx = ps.subscribe("task.*").unwrap();
ps.publish("task.completed", json!({"id": 2}));
ps.publish("task.failed", json!({"id": 3}));
let m1 = rx.recv().await.unwrap();
assert_eq!(m1.topic, "task.completed");
let m2 = rx.recv().await.unwrap();
assert_eq!(m2.topic, "task.failed");
}
#[tokio::test]
async fn publish_reaches_hash_subscriber() {
let ps = PubSub::new();
let mut rx = ps.subscribe("agent.#").unwrap();
ps.publish("agent.status.changed", json!({"status": "idle"}));
let msg = rx.recv().await.unwrap();
assert_eq!(msg.topic, "agent.status.changed");
}
#[tokio::test]
async fn non_matching_subscriber_gets_nothing() {
let ps = PubSub::new();
let mut rx = ps.subscribe("task.*").unwrap();
ps.publish("agent.started", json!({}));
assert!(rx.try_recv().is_err());
}
#[tokio::test]
async fn multiple_subscribers_same_pattern() {
let ps = PubSub::new();
let mut rx1 = ps.subscribe("task.*").unwrap();
let mut rx2 = ps.subscribe("task.*").unwrap();
ps.publish("task.done", json!({"ok": true}));
let m1 = rx1.recv().await.unwrap();
let m2 = rx2.recv().await.unwrap();
assert_eq!(m1.topic, "task.done");
assert_eq!(m2.topic, "task.done");
}
#[tokio::test]
async fn unsubscribe_all_closes_receivers() {
let ps = PubSub::new();
let mut rx = ps.subscribe("task.*").unwrap();
ps.unsubscribe_all("task.*");
assert!(rx.recv().await.is_err());
}
#[tokio::test]
async fn message_has_timestamp() {
let ps = PubSub::new();
let mut rx = ps.subscribe("t").unwrap();
let before = Utc::now();
ps.publish("t", json!(null));
let msg = rx.recv().await.unwrap();
assert!(msg.timestamp >= before);
assert!(msg.timestamp <= Utc::now());
}
}