mod command;
mod error;
mod message;
mod metrics;
pub use command::*;
pub use error::*;
pub use message::*;
use std::collections::HashMap;
use std::sync::atomic::Ordering;
use tokio::sync::mpsc::error::TrySendError;
use tokio::sync::{mpsc, watch};
use tracing::{error, info};
const PUBSUB_ENGINE_BACKLOG_SIZE: usize = 1000;
const PUBSUB_ENGINE_METRICS_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5);
#[derive(Clone)]
pub struct PubSubEngine {
command_tx: mpsc::Sender<PubSubCommand>,
shutdown_tx: watch::Sender<()>,
}
impl Default for PubSubEngine {
fn default() -> Self {
Self::new()
}
}
impl PubSubEngine {
pub fn new() -> Self {
let (command_tx, command_rx) = mpsc::channel(PUBSUB_ENGINE_BACKLOG_SIZE);
let (shutdown_tx, shutdown_rx) = watch::channel(());
let pubsub_metrics = metrics::PubSubMetrics::new();
Self::start_engine(command_rx, shutdown_rx.clone(), pubsub_metrics.clone());
Self::start_metrics_watcher(shutdown_rx, pubsub_metrics);
Self {
command_tx,
shutdown_tx,
}
}
pub fn send(&self, command: PubSubCommand) -> Result<(), PubSubError> {
self.command_tx
.try_send(command)
.inspect_err(|error| error!(?error, "Failed to send command."))
.map_err(|error| match error {
TrySendError::Full(_) => PubSubError::CommandQueueFull,
TrySendError::Closed(_) => PubSubError::CommandQueueClosed,
})
}
pub fn shutdown(self) {
let _ = self.shutdown_tx.send(());
}
fn start_engine(
mut command_rx: mpsc::Receiver<PubSubCommand>,
mut shutdown_rx: watch::Receiver<()>,
pubsub_metrics: metrics::PubSubMetrics,
) {
tokio::spawn(async move {
let mut consumers: HashMap<String, Vec<mpsc::Sender<Message>>> = HashMap::new();
loop {
tokio::select! {
_ = shutdown_rx.changed() => {
info!("[PubSub] Engine shutting down.");
break;
}
Some(command) = command_rx.recv() => match command {
PubSubCommand::Publish { topic, message } => {
pubsub_metrics.messages_processed().fetch_add(1, Ordering::Relaxed);
if let Some(subscribers) = consumers.get_mut(&topic) {
subscribers.retain(|subscriber| {
let result = subscriber.try_send(message.clone());
match result {
Ok(_) => true,
Err(TrySendError::Full(_)) => {
info!(?topic, "[PubSub] Subscriber backlog is full. The message will be dropped.");
true
}
Err(TrySendError::Closed(_)) => {
info!(?topic, "[PubSub] Subscriber closed. Removing from the consumer list.");
false
}
}
});
}
}
PubSubCommand::Subscribe { topic, subscriber } => {
consumers.entry(topic).or_default().push(subscriber);
}
PubSubCommand::Unsubscribe { topic, subscriber } => {
if let Some(channels) = consumers.get_mut(&topic) {
if let Some(index) = channels.iter().position(|s| s.same_channel(&subscriber)) {
channels.swap_remove(index);
}
}
}
}
}
}
});
}
fn start_metrics_watcher(
mut shutdown_rx: watch::Receiver<()>,
pubsub_metrics: metrics::PubSubMetrics,
) {
tokio::spawn(async move {
let mut interval = tokio::time::interval(PUBSUB_ENGINE_METRICS_INTERVAL);
loop {
tokio::select! {
_ = shutdown_rx.changed() => {
info!("[PubSub] Metrics watcher shutting down.");
break;
}
_ = interval.tick() => {
let count = pubsub_metrics.messages_processed().swap(0, Ordering::Relaxed);
let rate = count as f64 / PUBSUB_ENGINE_METRICS_INTERVAL.as_secs_f64();
info!(messages_per_second = ?rate, "[PubSub] Metrics")
}
}
}
});
}
}