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
+237
View File
@@ -0,0 +1,237 @@
use std::fmt::Debug;
use redis::aio::ConnectionLike;
use redis::{FromRedisValue, ToRedisArgs};
use crate::Error;
use super::cache::CacheSettings;
use super::connection::RoutableConnection;
use super::routing::primary_mget_routing;
use super::util::cmd;
pub const MGET_CHUNK_SIZE: usize = 32;
#[tracing::instrument(skip_all)]
pub async fn set<C, D>(
connection: &mut C,
key: &str,
data: D,
expiry: i64,
) -> Result<(), Error>
where
C: ConnectionLike,
D: ToRedisArgs + Send + Sync + Debug,
{
cmd("SET")
.arg(key)
.arg(data)
.arg("EX")
.arg(expiry)
.query_async::<()>(connection)
.await?;
Ok(())
}
#[tracing::instrument(skip_all)]
pub async fn set_serialized<C, D>(
connection: &mut C,
key: &str,
data: D,
expiry: Option<i64>,
settings: &CacheSettings,
) -> Result<(), Error>
where
C: ConnectionLike,
D: serde::Serialize,
{
set(
connection,
key,
settings.encode_value(&data)?,
expiry.unwrap_or(settings.default_expiry),
)
.await
}
#[tracing::instrument(skip_all)]
pub async fn get<C>(
connection: &mut C,
key: &str,
) -> Result<Option<String>, Error>
where
C: ConnectionLike,
{
Ok(cmd("GET").arg(key).query_async(connection).await?)
}
/// Issues ordinary `MGET` commands in bounded chunks. Cluster routing and
/// result ordering remain redis-rs's responsibility; multiple chunks are not
/// an atomic snapshot.
#[tracing::instrument(skip_all)]
pub async fn get_many<C>(
connection: &mut C,
keys: &[String],
) -> Result<Vec<Option<Vec<u8>>>, Error>
where
C: ConnectionLike,
{
get_many_as(connection, keys).await
}
#[tracing::instrument(skip_all)]
pub async fn get_many_strings<C>(
connection: &mut C,
keys: &[String],
) -> Result<Vec<Option<String>>, Error>
where
C: ConnectionLike,
{
get_many_as(connection, keys).await
}
pub(super) async fn get_many_primary<C>(
connection: &mut C,
keys: &[String],
) -> Result<Vec<Option<Vec<u8>>>, Error>
where
C: RoutableConnection,
{
get_many_primary_as(connection, keys).await
}
pub(super) async fn get_many_strings_primary<C>(
connection: &mut C,
keys: &[String],
) -> Result<Vec<Option<String>>, Error>
where
C: RoutableConnection,
{
get_many_primary_as(connection, keys).await
}
pub(super) async fn get_many_as<C, T>(
connection: &mut C,
keys: &[String],
) -> Result<Vec<Option<T>>, Error>
where
C: ConnectionLike,
T: FromRedisValue,
{
let mut values = Vec::with_capacity(keys.len());
for chunk in keys.chunks(MGET_CHUNK_SIZE) {
let part = cmd("MGET")
.arg(chunk)
.query_async::<Vec<Option<T>>>(connection)
.await?;
values.extend(part);
}
Ok(values)
}
async fn get_many_primary_as<C, T>(
connection: &mut C,
keys: &[String],
) -> Result<Vec<Option<T>>, Error>
where
C: RoutableConnection,
T: FromRedisValue,
{
let mut values = Vec::with_capacity(keys.len());
for chunk in keys.chunks(MGET_CHUNK_SIZE) {
let mut command = redis::cmd("MGET");
command.arg(chunk);
let value = connection
.route_command(command, primary_mget_routing(chunk))
.await?;
let part =
redis::from_redis_value::<Vec<Option<T>>>(value.extract_error()?)
.map_err(redis::RedisError::from)?;
values.extend(part);
}
Ok(values)
}
#[tracing::instrument(skip_all)]
pub async fn get_deserialized<C, R>(
connection: &mut C,
key: &str,
settings: &CacheSettings,
) -> Result<Option<R>, Error>
where
C: ConnectionLike,
R: for<'a> serde::Deserialize<'a>,
{
let value: Option<Vec<u8>> =
cmd("GET").arg(key).query_async(connection).await?;
Ok(value.and_then(|value| settings.decode_value(&value)))
}
#[tracing::instrument(skip_all)]
pub async fn get_many_deserialized<C, R>(
connection: &mut C,
keys: &[String],
settings: &CacheSettings,
) -> Result<Vec<Option<R>>, Error>
where
C: ConnectionLike,
R: for<'a> serde::Deserialize<'a>,
{
Ok(get_many(connection, keys)
.await?
.into_iter()
.map(|value| value.and_then(|value| settings.decode_value(&value)))
.collect())
}
#[tracing::instrument(skip_all)]
pub async fn delete<C>(connection: &mut C, key: &str) -> Result<(), Error>
where
C: ConnectionLike,
{
cmd("DEL").arg(key).query_async::<()>(connection).await?;
Ok(())
}
#[tracing::instrument(skip_all)]
pub async fn delete_many<C>(
connection: &mut C,
keys: &[String],
) -> Result<(), Error>
where
C: ConnectionLike,
{
if !keys.is_empty() {
cmd("DEL").arg(keys).query_async::<()>(connection).await?;
}
Ok(())
}
#[tracing::instrument(skip_all)]
pub async fn lpush<C, D>(
connection: &mut C,
key: &str,
value: D,
) -> Result<(), Error>
where
C: ConnectionLike,
D: ToRedisArgs + Send + Sync + Debug,
{
cmd("LPUSH")
.arg(key)
.arg(value)
.query_async::<()>(connection)
.await?;
Ok(())
}
#[tracing::instrument(skip_all)]
pub async fn incr<C>(
connection: &mut C,
key: &str,
) -> Result<Option<u64>, Error>
where
C: ConnectionLike,
{
Ok(cmd("INCR").arg(key).query_async(connection).await?)
}