use std::collections::{BTreeMap, HashMap, VecDeque};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, MutexGuard, OnceLock, PoisonError};
use google_cloud_pubsub::model::Message as GcpMessage;
use ruststream::RawMessage;
use ruststream::testing::Coordinator;
use tokio::runtime::Handle;
use tokio::sync::mpsc;
use super::check_resource_name;
use super::limits;
use super::settle::Consumer;
use crate::broker::{subscription_path, topic_path};
use crate::error::PubSubError;
use crate::message::PubSubMessage;
use crate::subscription::{DeclaredRetries, GooglePubSub};
#[derive(Debug, Clone)]
pub(crate) struct Pending {
pub(crate) message: GcpMessage,
pub(crate) attempt: i32,
}
#[derive(Debug)]
pub(crate) struct Handed {
pub(crate) pending: Pending,
pub(crate) counted: bool,
}
pub(crate) type HandedSender = mpsc::UnboundedSender<Handed>;
pub(crate) type HandedReceiver = mpsc::UnboundedReceiver<Handed>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct ConsumerId(u64);
#[derive(Debug, Clone)]
struct DeadLetter {
topic: String,
max_attempts: i32,
}
#[derive(Debug)]
struct Subscription {
topic: String,
dead_letter: Option<DeadLetter>,
backlog: VecDeque<Pending>,
consumers: BTreeMap<ConsumerId, HandedSender>,
turn: usize,
}
impl Subscription {
fn attached_to(topic: String) -> Self {
Self {
topic,
dead_letter: None,
backlog: VecDeque::new(),
consumers: BTreeMap::new(),
turn: 0,
}
}
fn hand(&mut self, pending: Pending, coordinator: Option<&Coordinator>) {
if self.consumers.is_empty() {
self.backlog.push_back(pending);
return;
}
let index = self.turn % self.consumers.len();
self.turn = self.turn.wrapping_add(1);
let Some(sender) = self.consumers.values().nth(index) else {
self.backlog.push_back(pending);
return;
};
if let Some(coordinator) = coordinator {
coordinator.enqueued();
}
let handed = Handed {
pending,
counted: coordinator.is_some(),
};
if let Err(mpsc::error::SendError(handed)) = sender.send(handed) {
if let Some(coordinator) = coordinator {
coordinator.consumed();
}
self.backlog.push_back(handed.pending);
}
}
}
#[derive(Debug, Default)]
struct State {
subscriptions: HashMap<String, Subscription>,
log: HashMap<String, Vec<RawMessage>>,
}
#[derive(Debug)]
pub(crate) struct Project {
id: String,
state: Mutex<State>,
closed: AtomicBool,
coordinator: OnceLock<Coordinator>,
declared_retries: DeclaredRetries,
next_consumer: AtomicU64,
runtime: Handle,
}
impl Project {
pub(crate) fn new(id: String, runtime: Handle) -> Arc<Self> {
Arc::new(Self {
runtime,
id,
state: Mutex::new(State::default()),
closed: AtomicBool::new(false),
coordinator: OnceLock::new(),
declared_retries: DeclaredRetries::default(),
next_consumer: AtomicU64::new(0),
})
}
pub(crate) const fn runtime(&self) -> &Handle {
&self.runtime
}
fn state(&self) -> MutexGuard<'_, State> {
self.state.lock().unwrap_or_else(PoisonError::into_inner)
}
pub(crate) fn install(&self, coordinator: Coordinator) {
let _ = self.coordinator.set(coordinator);
}
pub(crate) fn coordinator(&self) -> Option<&Coordinator> {
self.coordinator.get()
}
pub(crate) fn declared_retries(&self) -> &DeclaredRetries {
&self.declared_retries
}
pub(crate) fn ensure_open(&self) -> Result<(), PubSubError> {
if self.closed.load(Ordering::Acquire) {
return Err(PubSubError::NotConnected);
}
Ok(())
}
pub(crate) fn close(&self) {
self.closed.store(true, Ordering::Release);
}
pub(crate) fn topic_path(&self, topic: &str) -> String {
topic_path(&self.id, topic)
}
pub(crate) fn subscription_path(&self, subscription: &str) -> String {
subscription_path(&self.id, subscription)
}
pub(crate) fn routes(&self, destination: &str, subscriptions: &[&str]) -> Vec<usize> {
let topic = self.topic_path(destination);
let paths: Vec<String> = subscriptions
.iter()
.map(|name| self.subscription_path(name))
.collect();
let state = self.state();
let mut routed = Vec::new();
for (first, path) in paths.iter().enumerate() {
if paths[..first].contains(path) {
continue;
}
let Some(subscription) = state
.subscriptions
.get(path)
.filter(|subscription| subscription.topic == topic)
else {
continue;
};
let listed: Vec<usize> = paths
.iter()
.enumerate()
.filter(|(_, listed)| *listed == path)
.map(|(position, _)| position)
.collect();
let turn = match subscription.consumers.len() {
0 => 0,
open => subscription.turn % open,
};
routed.extend(listed.get(turn));
}
drop(state);
routed.sort_unstable();
routed
}
pub(crate) fn open(
self: &Arc<Self>,
descriptor: &GooglePubSub,
) -> Result<Consumer, PubSubError> {
let name = descriptor.subscription();
let path = self.subscription_path(name);
let topic = if let Some(topic) = descriptor.create_topic_ref() {
check_resource_name("topics", topic)
.map_err(|reason| admin(self.topic_path(topic), reason))?;
check_resource_name("subscriptions", name)
.map_err(|reason| admin(path.clone(), reason))?;
self.topic_path(topic)
} else {
check_resource_name("subscriptions", name).map_err(|reason| PubSubError::Receive {
subscription: path.clone(),
source: reason.into(),
})?;
path.replacen("/subscriptions/", "/topics/", 1)
};
let dead_letter = match descriptor.dead_letter_policy() {
Some((dead_letter, max_attempts)) => {
check_resource_name("topics", dead_letter)
.map_err(|reason| admin(self.topic_path(dead_letter), reason))?;
Some(DeadLetter {
topic: self.topic_path(dead_letter),
max_attempts,
})
}
None => None,
};
let id = ConsumerId(self.next_consumer.fetch_add(1, Ordering::Relaxed));
let (sender, receiver) = mpsc::unbounded_channel();
let coordinator = self.coordinator();
let mut state = self.state();
let subscription = state
.subscriptions
.entry(path.clone())
.or_insert_with(|| Subscription::attached_to(topic));
if dead_letter.is_some() {
subscription.dead_letter = dead_letter;
}
subscription.consumers.insert(id, sender);
let held: Vec<Pending> = subscription.backlog.drain(..).collect();
for pending in held {
subscription.hand(pending, coordinator);
}
drop(state);
Ok(Consumer::new(
Arc::clone(self),
Arc::from(path),
id,
receiver,
descriptor.limits(),
))
}
pub(crate) fn cancel(&self, subscription: &str, id: ConsumerId, receiver: &mut HandedReceiver) {
let mut state = self.state();
if let Some(subscription) = state.subscriptions.get_mut(subscription) {
subscription.consumers.remove(&id);
}
drop(state);
while let Ok(handed) = receiver.try_recv() {
self.reject(subscription, handed.pending);
if handed.counted
&& let Some(coordinator) = self.coordinator()
{
coordinator.consumed();
}
}
}
pub(crate) fn publish(&self, topic: &str, message: &GcpMessage) -> Result<(), PubSubError> {
self.ensure_open()?;
check_resource_name("topics", topic)
.and_then(|()| check_message(message))
.map_err(|reason| PubSubError::Publish {
topic: self.topic_path(topic),
source: reason.into(),
})?;
self.route(topic, message);
Ok(())
}
fn route(&self, topic: &str, message: &GcpMessage) {
let path = self.topic_path(topic);
let coordinator = self.coordinator();
let mut state = self.state();
let delivered = PubSubMessage::published(topic, message.clone());
state.log.entry(path.clone()).or_default().push(delivered);
for subscription in state
.subscriptions
.values_mut()
.filter(|subscription| subscription.topic == path)
{
subscription.hand(
Pending {
message: message.clone(),
attempt: 1,
},
coordinator,
);
}
}
pub(crate) fn published(&self, topic: &str) -> Vec<RawMessage> {
self.state()
.log
.get(&self.topic_path(topic))
.cloned()
.unwrap_or_default()
}
pub(crate) fn reported_attempt(&self, subscription: &str, attempt: i32) -> Option<i32> {
self.state()
.subscriptions
.get(subscription)
.and_then(|subscription| subscription.dead_letter.as_ref())
.map(|_| attempt)
}
pub(crate) fn reject(&self, subscription: &str, pending: Pending) {
let coordinator = self.coordinator();
let mut state = self.state();
let Some(held) = state.subscriptions.get_mut(subscription) else {
return;
};
if let Some(policy) = held
.dead_letter
.as_ref()
.filter(|policy| pending.attempt >= policy.max_attempts)
{
let topic = policy.topic.clone();
drop(state);
self.route(&topic, &pending.message);
return;
}
held.hand(
Pending {
message: pending.message,
attempt: pending.attempt.saturating_add(1),
},
coordinator,
);
}
}
fn admin(name: String, reason: String) -> PubSubError {
PubSubError::Admin {
name,
source: reason.into(),
}
}
fn check_message(message: &GcpMessage) -> Result<(), String> {
if message.data.is_empty() && message.attributes.is_empty() {
return Err("a message must carry data or at least one attribute".to_owned());
}
if message.attributes.len() > limits::ATTRIBUTES {
return Err(format!(
"a message carries at most {} attributes, this one carries {}",
limits::ATTRIBUTES,
message.attributes.len()
));
}
let mut size = message.data.len() + message.ordering_key.len();
for (key, value) in &message.attributes {
if key.is_empty() || key.len() > limits::ATTRIBUTE_KEY {
return Err(format!(
"attribute key {key:?} must be 1 to {} bytes",
limits::ATTRIBUTE_KEY
));
}
if key.starts_with("goog") {
return Err(format!(
"attribute key {key:?} starts with the reserved \"goog\""
));
}
if value.len() > limits::ATTRIBUTE_VALUE {
return Err(format!(
"attribute {key:?} is {} bytes, past the {} a value may carry",
value.len(),
limits::ATTRIBUTE_VALUE
));
}
size += key.len() + value.len();
}
if message.ordering_key.len() > limits::ORDERING_KEY {
return Err(format!(
"the ordering key is {} bytes, past the {} it may carry",
message.ordering_key.len(),
limits::ORDERING_KEY
));
}
if size > limits::MESSAGE {
return Err(format!(
"the message is {size} bytes, past the {} the service accepts",
limits::MESSAGE
));
}
Ok(())
}