use std::collections::hash_map::DefaultHasher;
use std::collections::{BTreeMap, HashMap, VecDeque};
use std::fmt;
use std::hash::{Hash, Hasher};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Mutex, MutexGuard};
use std::task::{Context, Poll};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use bytes::Bytes;
use futures::task::AtomicWaker;
use ruststream::testing::Coordinator;
use ruststream::{HeaderMap, RawMessage};
use tokio::time::Instant;
use crate::in_process::route::Route;
use crate::message::{PARTITION_KEY_HEADER, PulsarPosition};
use crate::subscription::{DeadLetterRoute, SubscriptionType};
use crate::topic::PulsarTopic;
const PATTERN_REFRESH: Duration = Duration::from_secs(30);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct ConsumerId(u64);
#[derive(Debug, Clone)]
pub(crate) struct Delivery {
pub(crate) payload: Bytes,
pub(crate) headers: HeaderMap,
pub(crate) seq: usize,
pub(crate) topic: String,
pub(crate) redeliveries: u32,
}
#[derive(Debug, Clone)]
pub(crate) struct Membership {
name: String,
sharing: SubscriptionType,
}
impl Membership {
pub(crate) const fn new(name: String, sharing: SubscriptionType) -> Self {
Self { name, sharing }
}
}
#[derive(Debug)]
pub(crate) struct ExclusiveHeld {
pub(crate) subscription: String,
}
impl fmt::Display for ExclusiveHeld {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"subscription '{}' is exclusive and already has a consumer",
self.subscription
)
}
}
impl std::error::Error for ExclusiveHeld {}
#[derive(Debug, Clone, Copy)]
struct Moment {
at: Instant,
order: u64,
}
struct Consumer {
route: Route,
opened: Moment,
membership: Membership,
dead_letter: Option<DeadLetterRoute>,
queue: VecDeque<Delivery>,
unacked: Vec<Delivery>,
waker: AtomicWaker,
}
struct LogEntry {
message: RawMessage,
at: u64,
}
#[derive(Default)]
struct RouterState {
consumers: HashMap<ConsumerId, Consumer>,
log: HashMap<String, Vec<LogEntry>>,
created: HashMap<String, Moment>,
events: u64,
rotation: HashMap<String, usize>,
parked: HashMap<String, Vec<Delivery>>,
departed: HashMap<ConsumerId, (Membership, Option<DeadLetterRoute>, Route)>,
}
impl RouterState {
fn moment(&mut self) -> Moment {
self.events += 1;
Moment {
at: Instant::now(),
order: self.events,
}
}
fn entries(&self, topic: &str) -> &[LogEntry] {
self.log.get(topic).map_or(&[], Vec::as_slice)
}
fn reads(&self, consumer: &Consumer, topic: &str, now: Instant) -> bool {
let Ok(parsed) = PulsarTopic::parse(topic) else {
return false;
};
if !consumer.route.selects(&parsed) {
return false;
}
if !consumer.route.is_pattern() {
return true;
}
self.created
.get(topic)
.is_some_and(|&created| listed_by(consumer.opened, created, now))
}
fn subscriptions_over(
&self,
topic: &str,
now: Instant,
) -> Vec<(String, SubscriptionType, Vec<ConsumerId>)> {
let mut grouped: BTreeMap<&str, Vec<ConsumerId>> = BTreeMap::new();
for (id, consumer) in &self.consumers {
if self.reads(consumer, topic, now) {
grouped
.entry(consumer.membership.name.as_str())
.or_default()
.push(*id);
}
}
grouped
.into_iter()
.map(|(name, mut members)| {
members.sort_unstable();
let sharing = self.consumers[&members[0]].membership.sharing;
(name.to_owned(), sharing, members)
})
.collect()
}
fn members_of(&self, topic: &str, subscription: &str, now: Instant) -> Vec<ConsumerId> {
let mut members: Vec<ConsumerId> = self
.consumers
.iter()
.filter(|(_, consumer)| {
consumer.membership.name == subscription && self.reads(consumer, topic, now)
})
.map(|(id, _)| *id)
.collect();
members.sort_unstable();
members
}
fn choose(
&mut self,
subscription: &str,
sharing: SubscriptionType,
members: &[ConsumerId],
delivery: &Delivery,
) -> ConsumerId {
match sharing {
SubscriptionType::Exclusive | SubscriptionType::Failover => members[0],
SubscriptionType::Shared => {
let turn = self.rotation.entry(subscription.to_owned()).or_default();
let chosen = members[*turn % members.len()];
*turn = turn.wrapping_add(1);
chosen
}
SubscriptionType::KeyShared => members[key_slot(delivery, members.len())],
}
}
fn deliver(
&mut self,
topic: &str,
payload: Bytes,
headers: HeaderMap,
coordinator: Option<&Coordinator>,
) {
let moment = self.moment();
let now = moment.at;
self.created.entry(topic.to_owned()).or_insert(moment);
let snapshot = RawMessage::new(topic, payload.clone()).with_headers(headers.clone());
let entries = self.log.entry(topic.to_owned()).or_default();
let seq = entries.len();
entries.push(LogEntry {
message: snapshot,
at: now_millis(),
});
let delivery = Delivery {
payload,
headers,
seq,
topic: topic.to_owned(),
redeliveries: 0,
};
for (name, sharing, members) in self.subscriptions_over(topic, now) {
let target = self.choose(&name, sharing, &members, &delivery);
if self.enqueue(target, delivery.clone())
&& let Some(coordinator) = coordinator
{
coordinator.enqueued();
}
}
}
fn hand_back(
&mut self,
membership: &Membership,
delivery: Delivery,
counted: bool,
coordinator: Option<&Coordinator>,
) {
let members = self.members_of(&delivery.topic, &membership.name, Instant::now());
if members.is_empty() {
self.parked
.entry(membership.name.clone())
.or_default()
.push(delivery);
if counted && let Some(coordinator) = coordinator {
coordinator.consumed();
}
return;
}
let target = self.choose(&membership.name, membership.sharing, &members, &delivery);
if self.enqueue(target, delivery)
&& !counted
&& let Some(coordinator) = coordinator
{
coordinator.enqueued();
}
}
fn enqueue(&mut self, id: ConsumerId, delivery: Delivery) -> bool {
let Some(consumer) = self.consumers.get_mut(&id) else {
return false;
};
consumer.queue.push_back(delivery);
consumer.waker.wake();
true
}
}
fn listed_by(opened: Moment, created: Moment, now: Instant) -> bool {
if created.order < opened.order {
return true;
}
let refresh = PATTERN_REFRESH.as_nanos();
let since = created.at.saturating_duration_since(opened.at).as_nanos();
let ticks = since.div_ceil(refresh).max(1);
let listed_at = u64::try_from(ticks.saturating_mul(refresh))
.ok()
.and_then(|nanos| opened.at.checked_add(Duration::from_nanos(nanos)));
listed_at.is_some_and(|listed_at| listed_at <= now)
}
fn key_slot(delivery: &Delivery, consumers: usize) -> usize {
slot_of_key(
delivery
.headers
.get(PARTITION_KEY_HEADER)
.unwrap_or_default(),
consumers,
)
}
fn slot_of_key(key: &[u8], consumers: usize) -> usize {
let mut hasher = DefaultHasher::new();
key.hash(&mut hasher);
let count = u64::try_from(consumers).unwrap_or(1).max(1);
usize::try_from(hasher.finish() % count).unwrap_or(0)
}
#[derive(Default)]
pub(crate) struct AddressRouter {
state: Mutex<RouterState>,
next_id: AtomicU64,
}
impl AddressRouter {
fn lock(&self) -> MutexGuard<'_, RouterState> {
self.state
.lock()
.expect("the router is never held across a panic")
}
#[allow(clippy::significant_drop_tightening)]
pub(crate) fn subscribe(
&self,
route: Route,
membership: Membership,
dead_letter: Option<DeadLetterRoute>,
coordinator: Option<&Coordinator>,
) -> Result<ConsumerId, ExclusiveHeld> {
let mut state = self.lock();
let contested = state.consumers.values().any(|consumer| {
consumer.membership.name == membership.name
&& consumer.route.overlaps(&route)
&& (consumer.membership.sharing == SubscriptionType::Exclusive
|| membership.sharing == SubscriptionType::Exclusive)
});
if contested {
return Err(ExclusiveHeld {
subscription: membership.name,
});
}
let opened = state.moment();
for topic in route.topics() {
state.created.entry(topic.clone()).or_insert(opened);
}
let id = ConsumerId(self.next_id.fetch_add(1, Ordering::Relaxed));
let name = membership.name.clone();
let consumer = Consumer {
route,
opened,
membership,
dead_letter,
queue: VecDeque::new(),
unacked: Vec::new(),
waker: AtomicWaker::new(),
};
let now = opened.at;
let (backlog, kept): (Vec<Delivery>, Vec<Delivery>) = state
.parked
.remove(&name)
.unwrap_or_default()
.into_iter()
.partition(|delivery| state.reads(&consumer, &delivery.topic, now));
if !kept.is_empty() {
state.parked.insert(name, kept);
}
if let Some(coordinator) = coordinator {
for _ in &backlog {
coordinator.enqueued();
}
}
let mut consumer = consumer;
consumer.queue.extend(backlog);
state.consumers.insert(id, consumer);
Ok(id)
}
pub(crate) fn next_pick(&self, topic: &str, subscription: &str) -> Option<ConsumerId> {
let state = self.lock();
let members = state.members_of(topic, subscription, Instant::now());
let first = *members.first()?;
let pick = match state.consumers[&first].membership.sharing {
SubscriptionType::Exclusive | SubscriptionType::Failover => first,
SubscriptionType::Shared => {
let turn = state
.rotation
.get(subscription)
.copied()
.unwrap_or_default();
members[turn % members.len()]
}
SubscriptionType::KeyShared => members[slot_of_key(&[], members.len())],
};
drop(state);
Some(pick)
}
pub(crate) fn unsubscribe(&self, id: ConsumerId, coordinator: Option<&Coordinator>) {
let mut state = self.lock();
let Some(consumer) = state.consumers.remove(&id) else {
return;
};
for delivery in consumer.queue {
state.hand_back(&consumer.membership, delivery, true, coordinator);
}
for mut delivery in consumer.unacked {
delivery.redeliveries = delivery.redeliveries.saturating_add(1);
state.hand_back(&consumer.membership, delivery, false, coordinator);
}
state.departed.insert(
id,
(consumer.membership, consumer.dead_letter, consumer.route),
);
}
pub(crate) fn hold(
&self,
id: ConsumerId,
mut delivery: Delivery,
coordinator: Option<&Coordinator>,
) {
let mut state = self.lock();
if let Some(consumer) = state.consumers.get_mut(&id) {
consumer.unacked.push(delivery);
return;
}
let Some((membership, ..)) = state.departed.get(&id).cloned() else {
return;
};
delivery.redeliveries = delivery.redeliveries.saturating_add(1);
state.hand_back(&membership, delivery, false, coordinator);
}
pub(crate) fn poll_delivery(&self, id: ConsumerId, cx: &Context<'_>) -> Poll<Option<Delivery>> {
let mut state = self.lock();
let Some(consumer) = state.consumers.get_mut(&id) else {
return Poll::Ready(None);
};
consumer.waker.register(cx.waker());
let next = consumer.queue.pop_front();
drop(state);
next.map_or(Poll::Pending, |delivery| Poll::Ready(Some(delivery)))
}
#[allow(clippy::significant_drop_tightening)]
pub(crate) fn requeue(
&self,
id: ConsumerId,
mut delivery: Delivery,
coordinator: Option<&Coordinator>,
) -> Option<bool> {
let mut state = self.lock();
let (membership, dead_letter, route) = match state.consumers.get(&id) {
Some(consumer) => (
consumer.membership.clone(),
consumer.dead_letter.clone(),
consumer.route.clone(),
),
None => state.departed.get(&id).cloned()?,
};
delivery.redeliveries = delivery.redeliveries.saturating_add(1);
if let Some(policy) = dead_letter
&& delivery.redeliveries >= policy.max_deliveries
{
let topic = PulsarTopic::parse(&policy.topic)
.map_or(policy.topic, |topic| topic.as_str().to_owned());
let origin = route.named(&delivery.topic);
let headers = stamp_origin(delivery.headers, origin, delivery.seq);
state.deliver(&topic, delivery.payload, headers, coordinator);
return Some(false);
}
let members = state.members_of(&delivery.topic, &membership.name, Instant::now());
if members.is_empty() {
state
.parked
.entry(membership.name)
.or_default()
.push(delivery);
return Some(false);
}
let target = state.choose(&membership.name, membership.sharing, &members, &delivery);
Some(state.enqueue(target, delivery))
}
pub(crate) fn settles(&self, id: ConsumerId) -> bool {
let state = self.lock();
state.consumers.contains_key(&id) || state.departed.contains_key(&id)
}
pub(crate) fn publish(
&self,
topic: &str,
payload: Bytes,
headers: HeaderMap,
coordinator: Option<&Coordinator>,
) {
self.lock().deliver(topic, payload, headers, coordinator);
}
pub(crate) fn published(&self, topic: &str) -> Vec<RawMessage> {
self.lock()
.entries(topic)
.iter()
.map(|entry| entry.message.clone())
.collect()
}
#[allow(clippy::significant_drop_tightening)]
pub(crate) fn seek(
&self,
id: ConsumerId,
position: &PulsarPosition,
coordinator: Option<&Coordinator>,
) -> bool {
let mut state = self.lock();
let now = Instant::now();
let Some(seeker) = state.consumers.get(&id) else {
return false;
};
let subscription = seeker.membership.clone();
let route = seeker.route.clone();
let mut topics: Vec<String> = state
.log
.keys()
.chain(seeker.route.topics())
.filter(|topic| {
!topic.starts_with("non-persistent://") && state.reads(seeker, topic, now)
})
.cloned()
.collect();
topics.sort_unstable();
topics.dedup();
let mut merged: Vec<(u64, Delivery)> = Vec::new();
for topic in &topics {
let entries = state.entries(topic);
let target = resolve(entries, position);
merged.extend(entries.iter().enumerate().skip(target).map(|(seq, entry)| {
(
entry.at,
Delivery {
payload: entry.message.payload_bytes(),
headers: entry.message.headers().clone(),
seq,
topic: topic.clone(),
redeliveries: 0,
},
)
}));
}
merged.sort_by_key(|(at, _)| *at);
let siblings: Vec<ConsumerId> = state
.consumers
.iter()
.filter(|(_, consumer)| {
consumer.membership.name == subscription.name && consumer.route.overlaps(&route)
})
.map(|(sibling, _)| *sibling)
.collect();
let mut discarded = 0;
for sibling in &siblings {
if let Some(consumer) = state.consumers.get_mut(sibling) {
let before = consumer.queue.len();
consumer
.queue
.retain(|delivery| !topics.contains(&delivery.topic));
discarded += before - consumer.queue.len();
}
}
let mut replayed = 0;
for (_, delivery) in merged {
let members = state.members_of(&delivery.topic, &subscription.name, now);
if members.is_empty() {
continue;
}
let target = state.choose(
&subscription.name,
subscription.sharing,
&members,
&delivery,
);
if state.enqueue(target, delivery) {
replayed += 1;
}
}
if let Some(coordinator) = coordinator {
for _ in 0..replayed {
coordinator.enqueued();
}
for _ in 0..discarded {
coordinator.consumed();
}
}
for sibling in &siblings {
if let Some(consumer) = state.consumers.get(sibling) {
consumer.waker.wake();
}
}
true
}
pub(crate) fn clear(&self) {
let mut state = self.lock();
for consumer in state.consumers.values() {
consumer.waker.wake();
}
state.consumers.clear();
state.log.clear();
state.created.clear();
state.rotation.clear();
state.parked.clear();
state.departed.clear();
}
}
const REAL_TOPIC: &str = "REAL_TOPIC";
const ORIGIN_MESSAGE_ID: &str = "ORIGIN_MESSAGE_ID";
fn stamp_origin(mut headers: HeaderMap, topic: &str, seq: usize) -> HeaderMap {
if !headers.contains(REAL_TOPIC) {
headers.insert(REAL_TOPIC, topic.to_owned());
}
if !headers.contains(ORIGIN_MESSAGE_ID) {
headers.insert(ORIGIN_MESSAGE_ID, format!("0:{seq}:-1"));
}
headers
}
fn resolve(entries: &[LogEntry], position: &PulsarPosition) -> usize {
let target = match position {
PulsarPosition::Earliest => 0,
PulsarPosition::Latest => entries.len(),
PulsarPosition::MessageId(id) => usize::try_from(id.entry_id).unwrap_or(usize::MAX),
PulsarPosition::Timestamp(millis) => entries
.iter()
.position(|entry| entry.at >= *millis)
.unwrap_or(entries.len()),
};
target.min(entries.len())
}
fn now_millis() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |since| {
u64::try_from(since.as_millis()).unwrap_or(u64::MAX)
})
}
impl fmt::Debug for AddressRouter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let state = self.lock();
f.debug_struct("AddressRouter")
.field("consumers", &state.consumers.len())
.field("topics", &state.log.len())
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_pattern_reads_a_later_topic_from_the_next_refresh() {
let start = Instant::now();
let at = |order, after: u64| Moment {
at: start + Duration::from_secs(after),
order,
};
let opened = at(2, 0);
assert!(listed_by(opened, at(1, 0), start), "it existed at open");
assert!(
!listed_by(opened, at(3, 0), start + Duration::from_secs(29)),
"created at the same instant, after the open",
);
assert!(listed_by(opened, at(3, 0), start + PATTERN_REFRESH));
assert!(!listed_by(
opened,
at(3, 5),
start + Duration::from_secs(29)
));
assert!(listed_by(opened, at(3, 5), start + PATTERN_REFRESH));
assert!(!listed_by(
opened,
at(3, 31),
start + Duration::from_secs(59)
));
assert!(listed_by(opened, at(3, 31), start + PATTERN_REFRESH * 2));
}
}