feat(labrinth): Redis Cluster (#6771)

* chore(labrinth): bump to redis 1.4.1

* feat(labrinth): redis cluster

* chore: cleanup

* feat(labrinth): cache locking

* fix(labrinth): clippy

* chore(labrinth): cleanup env, remove postcard support

* chore(ci): fix test env for labrinth

* chore(labrinth): bump all key versions

* chore(labrinth): improve redis key identities handling

* chore(labrinth): simplify deadline handling

* chore(labrinth): remove unused lease tracking

* chore(labrinth): remove distributed cache locking for now

* chore(labrinth): improve redis backend init error

* feat(labrinth): expose redis read replica strategy

* chore(ci): remove other connection mode tests

* chore: split xredis crate

* feat(xredis): primaries routing

* chore: tombi fmt

* chore: clippy

* chore: update query cache
This commit is contained in:
François-Xavier Talbot
2026-07-23 11:35:02 +02:00
committed by GitHub
parent 11af2651ef
commit b4d681e713
146 changed files with 4741 additions and 2000 deletions
+136
View File
@@ -0,0 +1,136 @@
use std::time::Duration;
use futures::StreamExt;
use redis::ToRedisArgs;
use tokio::sync::mpsc;
use tracing::{info, warn};
use super::{Error, 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<(), Error>
where
M: ToRedisArgs + Send + Sync,
{
let mut connection = self.connect().await?;
let _: usize = redis::cmd("PUBLISH")
.arg(channel)
.arg(message)
.query_async(&mut connection)
.await
.map_err(Error::from)?;
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>>,
) -> redis::RedisResult<SubscriptionOutcome> {
let client = redis::Client::open(seed_url)?;
let mut pubsub = client.get_async_pubsub().await?;
pubsub.subscribe(channel).await?;
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)
}