use async_trait::async_trait;
use bytes::Bytes;
use klieo_core::bus::{AckHandle, AckHandleImpl, Headers, Msg, MsgStream, Pubsub};
use klieo_core::error::BusError;
use klieo_core::ids::DurableName;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{broadcast, Mutex};
use tokio_stream::wrappers::BroadcastStream;
use tokio_stream::StreamExt;
const DEFAULT_CAPACITY: usize = 1024;
type PatternSender = broadcast::Sender<(String, Bytes, Headers)>;
struct State {
concretes: HashMap<String, PatternSender>,
wildcards: Vec<(String, PatternSender)>,
capacity: usize,
}
impl Default for State {
fn default() -> Self {
Self {
concretes: HashMap::new(),
wildcards: Vec::new(),
capacity: DEFAULT_CAPACITY,
}
}
}
fn is_wildcard_pattern(pattern: &str) -> bool {
pattern.contains('*') || pattern.contains('>')
}
#[derive(Clone)]
pub struct MemoryPubsub {
state: Arc<Mutex<State>>,
}
impl MemoryPubsub {
pub fn new() -> Self {
Self::with_buffer_size(DEFAULT_CAPACITY)
}
pub fn with_buffer_size(buffer: usize) -> Self {
Self {
state: Arc::new(Mutex::new(State {
concretes: HashMap::new(),
wildcards: Vec::new(),
capacity: buffer.max(1),
})),
}
}
async fn subscribe_sender(&self, pattern: &str) -> PatternSender {
let mut g = self.state.lock().await;
let cap = g.capacity;
if is_wildcard_pattern(pattern) {
if let Some((_, tx)) = g.wildcards.iter().find(|(p, _)| p == pattern) {
return tx.clone();
}
let (tx, _rx) = broadcast::channel(cap);
g.wildcards.push((pattern.to_string(), tx.clone()));
tx
} else {
g.concretes
.entry(pattern.to_string())
.or_insert_with(|| {
let (tx, _rx) = broadcast::channel(cap);
tx
})
.clone()
}
}
pub async fn remove_subject(&self, pattern: &str) {
let mut g = self.state.lock().await;
if g.concretes.remove(pattern).is_some() {
return;
}
g.wildcards.retain(|(p, _)| p != pattern);
}
pub async fn subject_count(&self) -> usize {
let g = self.state.lock().await;
g.concretes.len() + g.wildcards.len()
}
}
impl Default for MemoryPubsub {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Pubsub for MemoryPubsub {
async fn publish(
&self,
subject: &str,
payload: Bytes,
headers: Headers,
) -> Result<(), BusError> {
let matched: Vec<PatternSender> = {
let g = self.state.lock().await;
let mut out: Vec<PatternSender> = Vec::new();
if let Some(tx) = g.concretes.get(subject) {
out.push(tx.clone());
}
for (pattern, tx) in &g.wildcards {
if subject_matches(pattern, subject) {
out.push(tx.clone());
}
}
out
};
for tx in matched {
let _ = tx.send((subject.to_string(), payload.clone(), headers.clone()));
}
Ok(())
}
async fn subscribe(&self, subject: &str, _durable: DurableName) -> Result<MsgStream, BusError> {
let tx = self.subscribe_sender(subject).await;
let rx = tx.subscribe();
let pattern = subject.to_string();
let stream = BroadcastStream::new(rx).map(move |res| match res {
Ok((concrete_subject, payload, headers)) => Ok(Msg {
subject: concrete_subject,
payload,
headers,
ack: AckHandle::new(Box::new(NoopAck)),
}),
Err(_) => {
tracing::warn!(
target: "klieo.bus.memory",
pattern = %pattern,
"subscriber lagged - increase MemoryPubsub buffer size or speed up consumer"
);
Err(BusError::Retryable("subscriber lagged".into()))
}
});
Ok(Box::pin(stream))
}
}
fn subject_matches(pattern: &str, concrete: &str) -> bool {
if pattern.is_empty() || concrete.is_empty() {
return false;
}
let mut p = pattern.split('.');
let mut c = concrete.split('.');
loop {
match (p.next(), c.next()) {
(Some(""), _) | (_, Some("")) => return false,
(Some(">"), Some(_)) => return p.next().is_none(),
(Some(">"), None) => return false,
(Some("*"), Some(_)) => continue,
(Some("*"), None) => return false,
(Some(pt), Some(ct)) if pt == ct => continue,
(Some(_), Some(_)) => return false,
(None, None) => return true,
(Some(_), None) | (None, Some(_)) => return false,
}
}
}
pub(crate) struct NoopAck;
#[async_trait]
impl AckHandleImpl for NoopAck {
async fn ack(self: Box<Self>) -> Result<(), BusError> {
Ok(())
}
async fn nak(self: Box<Self>, _delay: Duration) -> Result<(), BusError> {
Ok(())
}
async fn term(self: Box<Self>) -> Result<(), BusError> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use klieo_core::Pubsub;
use tokio_stream::StreamExt;
#[tokio::test]
async fn publish_delivers_to_subscriber() {
let bus = MemoryPubsub::new();
let mut sub = bus
.subscribe("subject.a", DurableName::new("d1"))
.await
.unwrap();
bus.publish("subject.a", Bytes::from_static(b"hi"), Headers::new())
.await
.unwrap();
let msg = sub.next().await.unwrap().unwrap();
assert_eq!(msg.subject, "subject.a");
assert_eq!(msg.payload, Bytes::from_static(b"hi"));
msg.ack.ack().await.unwrap();
}
#[tokio::test]
async fn two_subscribers_both_receive() {
let bus = MemoryPubsub::new();
let mut s1 = bus
.subscribe("subject.b", DurableName::new("d1"))
.await
.unwrap();
let mut s2 = bus
.subscribe("subject.b", DurableName::new("d2"))
.await
.unwrap();
bus.publish("subject.b", Bytes::from_static(b"x"), Headers::new())
.await
.unwrap();
assert_eq!(
s1.next().await.unwrap().unwrap().payload,
Bytes::from_static(b"x")
);
assert_eq!(
s2.next().await.unwrap().unwrap().payload,
Bytes::from_static(b"x")
);
}
#[tokio::test]
async fn publish_with_no_subscribers_succeeds() {
let bus = MemoryPubsub::new();
bus.publish("nobody.home", Bytes::from_static(b"x"), Headers::new())
.await
.unwrap();
}
#[tokio::test]
async fn headers_preserved() {
let bus = MemoryPubsub::new();
let mut sub = bus
.subscribe("subject.h", DurableName::new("d"))
.await
.unwrap();
let mut h = Headers::new();
h.insert("k".into(), "v".into());
bus.publish("subject.h", Bytes::from_static(b"hi"), h)
.await
.unwrap();
let msg = sub.next().await.unwrap().unwrap();
assert_eq!(msg.headers.get("k").map(|s| s.as_str()), Some("v"));
}
#[tokio::test]
async fn small_buffer_lags_on_burst() {
let bus = MemoryPubsub::with_buffer_size(2);
let mut sub = bus
.subscribe("subject.lag", DurableName::new("d"))
.await
.unwrap();
for i in 0..4u8 {
bus.publish("subject.lag", Bytes::from(vec![i]), Headers::new())
.await
.unwrap();
}
let mut saw_lag = false;
for _ in 0..6 {
let next = sub.next().await.unwrap();
if let Err(BusError::Retryable(ref m)) = next {
if m.contains("lagged") {
saw_lag = true;
break;
}
}
}
assert!(saw_lag, "expected lagged err within the burst");
}
#[tokio::test]
async fn large_buffer_does_not_lag() {
let bus = MemoryPubsub::with_buffer_size(64);
let mut sub = bus
.subscribe("subject.nolag", DurableName::new("d"))
.await
.unwrap();
for i in 0..10u8 {
bus.publish("subject.nolag", Bytes::from(vec![i]), Headers::new())
.await
.unwrap();
}
for _ in 0..10 {
let msg = sub.next().await.unwrap().expect("no lag");
let _ = msg.payload;
}
}
#[test]
fn concrete_matches_self() {
assert!(subject_matches("a.b.c", "a.b.c"));
assert!(!subject_matches("a.b.c", "a.b"));
assert!(!subject_matches("a.b.c", "a.b.c.d"));
assert!(!subject_matches("a.b.c", "a.x.c"));
}
#[test]
fn single_token_wildcard() {
assert!(subject_matches("a.*.c", "a.x.c"));
assert!(subject_matches("a.*.c", "a.42.c"));
assert!(!subject_matches("a.*.c", "a.b.c.d"));
assert!(!subject_matches("a.*.c", "a.c"));
assert!(!subject_matches("*", "a.b"));
assert!(subject_matches("*", "a"));
}
#[test]
fn greedy_wildcard_matches_remaining_tokens() {
assert!(subject_matches("a.>", "a.b"));
assert!(subject_matches("a.>", "a.b.c.d"));
assert!(!subject_matches("a.>", "a"));
assert!(!subject_matches("a.>", "b.x"));
assert!(subject_matches("a.b.>", "a.b.x"));
assert!(!subject_matches("a.b.>", "a.b"));
}
#[test]
fn greedy_only_valid_as_last_token() {
assert!(!subject_matches("a.>.c", "a.x.c"));
assert!(!subject_matches("a.>.c", "a.b.c"));
assert!(!subject_matches("a.>.c", "anything"));
assert!(!subject_matches(">.a", "x.a"));
}
#[test]
fn bare_greedy_matches_anything_with_one_plus_token() {
assert!(subject_matches(">", "a"));
assert!(subject_matches(">", "a.b.c"));
assert!(!subject_matches(">", ""));
}
#[test]
fn prefix_does_not_partially_match() {
assert!(!subject_matches("a.b", "a"));
assert!(!subject_matches("a.b", "a.b.c"));
}
#[test]
fn empty_token_rejected() {
assert!(!subject_matches("a..b", "a..b"));
assert!(!subject_matches("a..b", "a.x.b"));
assert!(!subject_matches("a.b", ""));
assert!(!subject_matches("", "a.b"));
assert!(!subject_matches(".a", "x.a"));
assert!(!subject_matches("a.", "a.x"));
}
#[tokio::test]
async fn greedy_wildcard_subscriber_receives_matching_publishes() {
let bus = MemoryPubsub::new();
let mut sub = bus
.subscribe("klieo.a2a.cancel.>", DurableName::new("d"))
.await
.unwrap();
bus.publish(
"klieo.a2a.cancel.t-1",
Bytes::from_static(b"signal"),
Headers::new(),
)
.await
.unwrap();
let msg = sub.next().await.unwrap().unwrap();
assert_eq!(msg.subject, "klieo.a2a.cancel.t-1");
assert_eq!(msg.payload, Bytes::from_static(b"signal"));
}
}