use std::sync::{Mutex, MutexGuard};
use crate::error::PulsarError;
use crate::in_process::{AddressRouter, ConsumerId};
use crate::pattern::TopicPattern;
use crate::subscription::{PulsarSubscription, Topics};
use crate::topic::PulsarTopic;
#[derive(Debug, Clone)]
pub(crate) enum Route {
Topics {
qualified: Vec<String>,
named: Vec<String>,
},
Pattern(TopicPattern),
}
impl Route {
pub(crate) fn of(descriptor: &PulsarSubscription) -> Result<Self, PulsarError> {
match &descriptor.topics {
Topics::List(topics) => topics
.iter()
.map(|topic| PulsarTopic::parse(topic).map(|topic| topic.as_str().to_owned()))
.collect::<Result<_, _>>()
.map(|qualified| Self::Topics {
qualified,
named: topics.clone(),
}),
Topics::Pattern(pattern) => TopicPattern::parse(pattern).map(Self::Pattern),
}
}
pub(crate) fn selects(&self, topic: &PulsarTopic) -> bool {
match self {
Self::Topics { qualified, .. } => qualified.iter().any(|name| name == topic.as_str()),
Self::Pattern(pattern) => pattern.selects(topic),
}
}
pub(crate) const fn is_pattern(&self) -> bool {
matches!(self, Self::Pattern(_))
}
pub(crate) fn topics(&self) -> &[String] {
match self {
Self::Topics { qualified, .. } => qualified,
Self::Pattern(_) => &[],
}
}
pub(crate) fn named<'a>(&'a self, topic: &'a str) -> &'a str {
match self {
Self::Topics { qualified, named } => qualified
.iter()
.position(|name| name == topic)
.map_or(topic, |index| named[index].as_str()),
Self::Pattern(_) => topic,
}
}
pub(crate) fn overlaps(&self, other: &Self) -> bool {
match (self, other) {
(
Self::Topics {
qualified: mine, ..
},
Self::Topics {
qualified: theirs, ..
},
) => mine.iter().any(|topic| theirs.contains(topic)),
(Self::Topics { qualified, .. }, pattern @ Self::Pattern(_))
| (pattern @ Self::Pattern(_), Self::Topics { qualified, .. }) => qualified
.iter()
.any(|topic| PulsarTopic::parse(topic).is_ok_and(|topic| pattern.selects(&topic))),
(Self::Pattern(mine), Self::Pattern(theirs)) => mine.as_str() == theirs.as_str(),
}
}
}
type Members = Vec<(usize, Option<ConsumerId>)>;
#[derive(Debug)]
struct Opened {
name: String,
route: Route,
subscription: String,
consumer: Option<ConsumerId>,
}
#[derive(Debug, Default)]
pub(crate) struct Subscriptions {
opened: Mutex<Vec<Opened>>,
}
impl Subscriptions {
pub(crate) fn record(
&self,
name: String,
subscription: String,
route: Route,
consumer: Option<ConsumerId>,
) {
self.lock().push(Opened {
name,
route,
subscription,
consumer,
});
}
pub(crate) fn routes(
&self,
destination: &str,
names: &[&str],
router: Option<&AddressRouter>,
) -> Vec<usize> {
let Ok(topic) = PulsarTopic::parse(destination) else {
return Vec::new();
};
let opened = self.lock();
let mut reached: Vec<(&str, Members)> = Vec::new();
for (position, name) in names.iter().enumerate() {
let occurrence = names[..position]
.iter()
.filter(|seen| *seen == name)
.count();
let record = opened
.iter()
.filter(|record| record.name == *name)
.nth(occurrence);
let (selects, subscription, consumer) = record.map_or_else(
|| {
(
PulsarTopic::parse(name).is_ok_and(|named| named == topic),
*name,
None,
)
},
|record| {
(
record.route.selects(&topic),
record.subscription.as_str(),
record.consumer,
)
},
);
if !selects {
continue;
}
match reached.iter_mut().find(|(seen, _)| *seen == subscription) {
Some((_, consumers)) => consumers.push((position, consumer)),
None => reached.push((subscription, vec![(position, consumer)])),
}
}
let mut positions: Vec<usize> = reached
.iter()
.map(|(subscription, consumers)| {
let pick = router.and_then(|router| router.next_pick(topic.as_str(), subscription));
consumers
.iter()
.find(|(_, consumer)| pick.is_some() && *consumer == pick)
.unwrap_or(&consumers[0])
.0
})
.collect();
drop(opened);
positions.sort_unstable();
positions
}
fn lock(&self) -> MutexGuard<'_, Vec<Opened>> {
self.opened
.lock()
.expect("the record is read and written without panicking")
}
}
#[cfg(test)]
mod tests {
use super::*;
fn record(subscriptions: &Subscriptions, descriptor: &PulsarSubscription) {
subscriptions.record(
descriptor.source_name().to_owned(),
descriptor.subscription().to_owned(),
Route::of(descriptor).expect("a valid descriptor"),
None,
);
}
#[test]
fn a_message_is_owed_once_per_subscription() {
let subscriptions = Subscriptions::default();
record(
&subscriptions,
&PulsarSubscription::new("orders", "workers"),
);
record(
&subscriptions,
&PulsarSubscription::new("orders", "workers"),
);
record(&subscriptions, &PulsarSubscription::new("orders", "audit"));
assert_eq!(
subscriptions.routes("orders", &["orders", "orders", "orders"], None),
[0, 2]
);
}
#[test]
fn both_spellings_of_a_topic_are_one_topic() {
let subscriptions = Subscriptions::default();
record(
&subscriptions,
&PulsarSubscription::new("orders", "workers"),
);
assert_eq!(
subscriptions.routes("persistent://public/default/orders", &["orders"], None),
[0]
);
assert_eq!(
subscriptions.routes("non-persistent://public/default/orders", &["orders"], None),
Vec::<usize>::new()
);
}
#[test]
fn a_pattern_reads_the_lookup_namespace_by_full_name() {
let subscriptions = Subscriptions::default();
record(
&subscriptions,
&PulsarSubscription::pattern("audit-.*", "audit"),
);
record(
&subscriptions,
&PulsarSubscription::pattern("^audit-.*", "anchored"),
);
assert_eq!(
subscriptions.routes("audit-eu", &["audit", "anchored"], None),
[0]
);
assert_eq!(
subscriptions.routes("acme/eu/audit-eu", &["audit", "anchored"], None),
Vec::<usize>::new()
);
}
}