Files
modrinth/packages/xredis/src/pubsub.rs
T
aecsocket 5148e8ec35 feat: use postcard for redis serde (#6956)
* redo error handling in xredis

* give proper types to metadata fields

* add round-trip tests

* inline loader enum metadata fields

* postcard roundtrips

* prepare

* bump redis key version

* serde-binhum

* clippy

* fix

* fix frontend checking existence of component fields rather than non-null-ness

* prepare
2026-08-05 10:36:05 +00:00

144 lines
4.0 KiB
Rust

use std::time::Duration;
use eyre::{Result, WrapErr};
use futures::StreamExt;
use redis::ToRedisArgs;
use tokio::sync::mpsc;
use tracing::{info, warn};
use super::RedisPool;
const PUBSUB_BUFFER_SIZE: usize = 1024;
const INITIAL_RECONNECT_BACKOFF: Duration = Duration::from_millis(250);
const MAX_RECONNECT_BACKOFF: Duration = Duration::from_secs(30);
enum SubscriptionOutcome {
SubscriberClosed,
Disconnected,
}
impl RedisPool {
pub fn subscribe(&self, channel: &'static str) -> mpsc::Receiver<Vec<u8>> {
let seed_urls = self.config.seed_urls().to_vec();
let (sender, receiver) = mpsc::channel(PUBSUB_BUFFER_SIZE);
tokio::spawn(run_subscription(seed_urls, channel, sender));
receiver
}
#[doc(hidden)]
#[cfg(feature = "test")]
pub fn subscribe_with_seed_urls(
seed_urls: Vec<String>,
channel: &'static str,
) -> mpsc::Receiver<Vec<u8>> {
let (sender, receiver) = mpsc::channel(PUBSUB_BUFFER_SIZE);
tokio::spawn(run_subscription(seed_urls, channel, sender));
receiver
}
pub async fn publish<M>(&self, channel: &str, message: M) -> Result<()>
where
M: ToRedisArgs + Send + Sync,
{
let mut connection = self
.connect()
.await
.wrap_err("connecting to Redis for publishing")?;
let _: usize = redis::cmd("PUBLISH")
.arg(channel)
.arg(message)
.query_async(&mut connection)
.await
.wrap_err("publishing to Redis channel")?;
Ok(())
}
}
async fn run_subscription(
seed_urls: Vec<String>,
channel: &'static str,
sender: mpsc::Sender<Vec<u8>>,
) {
let mut next_seed = 0;
let mut reconnect_backoff = INITIAL_RECONNECT_BACKOFF;
loop {
let mut connected = false;
for _ in 0..seed_urls.len() {
let seed_url = &seed_urls[next_seed];
next_seed = (next_seed + 1) % seed_urls.len();
match forward_from_seed(seed_url, channel, &sender).await {
Ok(SubscriptionOutcome::SubscriberClosed) => return,
Ok(SubscriptionOutcome::Disconnected) => {
warn!(channel, "Redis Pub/Sub connection disconnected");
connected = true;
break;
}
Err(error) => {
warn!(
?error,
channel,
"Failed to establish Redis Pub/Sub subscription"
);
}
}
}
if sender.is_closed() {
return;
}
let delay = if connected {
INITIAL_RECONNECT_BACKOFF
} else {
reconnect_backoff
};
tokio::time::sleep(delay).await;
reconnect_backoff = if connected {
INITIAL_RECONNECT_BACKOFF
} else {
reconnect_backoff
.saturating_mul(2)
.min(MAX_RECONNECT_BACKOFF)
};
}
}
async fn forward_from_seed(
seed_url: &str,
channel: &'static str,
sender: &mpsc::Sender<Vec<u8>>,
) -> Result<SubscriptionOutcome> {
let client = redis::Client::open(seed_url)
.wrap_err("configuring Redis Pub/Sub client")?;
let mut pubsub = client
.get_async_pubsub()
.await
.wrap_err("connecting to Redis Pub/Sub")?;
pubsub
.subscribe(channel)
.await
.wrap_err("subscribing to Redis channel")?;
info!(channel, "Established Redis Pub/Sub subscription");
let mut stream = pubsub.into_on_message();
while let Some(message) = stream.next().await {
if message.get_channel_name() != channel {
continue;
}
if sender
.send(message.get_payload_bytes().to_vec())
.await
.is_err()
{
return Ok(SubscriptionOutcome::SubscriberClosed);
}
}
Ok(SubscriptionOutcome::Disconnected)
}