use std::sync::Arc;
use std::time::Duration;
use bytes::Bytes;
use dashmap::DashMap;
use tokio::sync::Notify;
use tracing::{error, info, warn};
use crate::broker::fanout::{ConnectionSink, FanoutEngine, SubscribeIntent};
use crate::frame::Frame;
use crate::redis::connection::RedisPool;
struct TopicState {
count: usize,
}
pub struct FanoutBridge {
pool: RedisPool,
fanout: FanoutEngine,
topics: DashMap<String, TopicState>,
shutdown: Arc<Notify>,
}
impl FanoutBridge {
pub fn new(pool: RedisPool) -> Arc<Self> {
let shutdown = Arc::new(Notify::new());
let bridge = Arc::new(Self {
pool,
fanout: FanoutEngine::new(),
topics: DashMap::new(),
shutdown: shutdown.clone(),
});
let bridge_clone = bridge.clone();
tokio::spawn(async move {
bridge_clone.listen_loop().await;
});
bridge
}
pub fn fanout(&self) -> &FanoutEngine {
&self.fanout
}
pub fn ensure_topic(&self, topic: &str) {
let mut entry = self
.topics
.entry(topic.to_string())
.or_insert(TopicState { count: 0 });
entry.count += 1;
}
pub fn drop_topic(&self, topic: &str) {
self.topics.remove(topic);
}
fn deliver_to_local(&self, topic: &str, frame: Frame) {
let framed = crate::broker::broker::serialize_frame_for_fanout(&frame, 0);
self.fanout.deliver(topic, framed);
}
pub fn subscribe(
&self,
topic: &str,
intent: SubscribeIntent,
sink: ConnectionSink,
) -> crate::broker::fanout::SubscriptionId {
self.ensure_topic(topic);
self.fanout.subscribe(topic, intent, sink)
}
pub fn unsubscribe(&self, id: crate::broker::fanout::SubscriptionId) -> Option<String> {
self.fanout.unsubscribe(id)
}
pub fn drop_sink(&self, sink_id: u64) -> Vec<String> {
self.fanout.drop_sink(sink_id)
}
pub fn topic_subscriber_count(&self, topic: &str) -> usize {
self.fanout.topic_subscriber_count(topic)
}
async fn listen_loop(&self) {
use futures_util::StreamExt;
let prefix = self.pool.prefix().to_string();
let channel_pattern = format!("{prefix}:fanout:*");
let backoff_base = Duration::from_secs(1);
let backoff_max = Duration::from_secs(30);
let mut retry_count = 0u32;
loop {
let client = match redis::Client::open(self.pool.url()) {
Ok(c) => c,
Err(e) => {
error!(error = %e, "redis fanout: failed to open client");
retry_count += 1;
let delay = backoff_base
.saturating_mul(retry_count.min(5))
.min(backoff_max);
tokio::select! {
_ = self.shutdown.notified() => return,
_ = tokio::time::sleep(delay) => {}
}
continue;
}
};
let mut pubsub = match client.get_async_pubsub().await {
Ok(ps) => ps,
Err(e) => {
warn!(error = %e, "redis fanout: failed to get pubsub connection");
retry_count += 1;
let delay = backoff_base
.saturating_mul(retry_count.min(5))
.min(backoff_max);
tokio::select! {
_ = self.shutdown.notified() => return,
_ = tokio::time::sleep(delay) => {}
}
continue;
}
};
if let Err(e) = pubsub.psubscribe(&channel_pattern).await {
warn!(error = %e, pattern = %channel_pattern, "redis fanout: psubscribe failed");
retry_count += 1;
let delay = backoff_base
.saturating_mul(retry_count.min(5))
.min(backoff_max);
tokio::select! {
_ = self.shutdown.notified() => return,
_ = tokio::time::sleep(delay) => {}
}
continue;
}
info!(pattern = %channel_pattern, "redis fanout: subscribed");
retry_count = 0;
let mut stream = pubsub.into_on_message();
loop {
tokio::select! {
_ = self.shutdown.notified() => {
return;
}
msg = stream.next() => {
let Some(msg) = msg else {
warn!("redis fanout: Pub/Sub stream ended, reconnecting");
break;
};
let payload: Bytes = Bytes::from(msg.get_payload_bytes().to_vec());
let channel: String = msg.get_channel_name().into();
let topic = channel
.strip_prefix(&format!("{}:fanout:", prefix))
.unwrap_or(&channel);
let frame = match serde_json::from_slice::<Frame>(&payload) {
Ok(mut f) => {
f.topic = Some(topic.to_string());
f
}
Err(_) => Frame {
topic: Some(topic.to_string()),
payload: Some(payload),
..Frame::default()
},
};
self.deliver_to_local(topic, frame);
}
}
}
}
}
}