Files
modrinth/packages/xredis/src/lib.rs
T
François-Xavier Talbot b4d681e713 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
2026-07-23 11:35:02 +02:00

387 lines
9.8 KiB
Rust

#![recursion_limit = "256"]
use std::fmt::{Debug, Display};
use std::future::Future;
use std::hash::Hash;
use std::sync::Arc;
use dashmap::DashMap;
use prometheus::Registry;
use redis::aio::ConnectionLike;
use redis::{FromRedisValue, ToRedisArgs};
use serde::Serialize;
use serde::de::DeserializeOwned;
mod blocking;
mod cache;
mod commands;
mod config;
mod connection;
mod key;
mod metrics;
mod pubsub;
mod routing;
mod util;
use cache::{CacheManager, ConnectionProvider};
pub use cache::{
CacheSettings, Codec, EncodingFormat, InvalidCodec, InvalidEncodingFormat,
RedisValue,
};
pub use config::{
CacheLockingStrategy, InvalidCacheLockingStrategy,
InvalidRedisConnectionType, InvalidRedisMode,
InvalidRedisReadReplicaStrategy, ReadReplicaStrategy, RedisConfig,
RedisConfigError, RedisConnectionType, RedisTopology,
};
use connection::RedisBackend;
pub use connection::RedisBackendBuildError;
pub use key::KeyBuilder;
use thiserror::Error as ThisError;
#[derive(Debug, ThisError)]
pub enum Error {
#[error("error while interacting with Redis: {0}")]
Redis(#[from] redis::RedisError),
#[error("Redis pool error: {0}")]
Pool(#[from] deadpool_redis::PoolError),
#[error("error while serializing a Redis cache value: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Redis blocking timeout must be greater than zero")]
InvalidBlockingTimeout,
#[error(
"timeout waiting on local cache lock ({released}/{total} released)"
)]
LocalCacheTimeout { released: usize, total: usize },
}
#[derive(Clone)]
pub struct RedisPool {
backend: RedisBackend,
blocking: blocking::RedisBlockingPool,
cache: CacheManager,
config: RedisConfig,
key_builder: KeyBuilder,
}
pub struct RedisConnection {
inner: connection::RedisConnection,
key_builder: KeyBuilder,
settings: CacheSettings,
}
impl RedisPool {
pub async fn new(
meta_namespace: impl Into<Arc<str>>,
config: RedisConfig,
cache_settings: CacheSettings,
) -> Result<Self, RedisBackendBuildError> {
tracing::info!(
strategy = %config.cache_locking_strategy(),
"configured Redis cache locking"
);
let backend = RedisBackend::new(&config).await?;
let blocking = blocking::RedisBlockingPool::new(&config).await?;
let key_builder = KeyBuilder::new(meta_namespace, config.topology());
let cache = CacheManager::new(key_builder.clone(), cache_settings);
Ok(Self {
backend,
blocking,
cache,
config,
key_builder,
})
}
pub fn key(&self) -> &KeyBuilder {
&self.key_builder
}
}
impl RedisPool {
pub async fn connect(&self) -> Result<RedisConnection, Error> {
Ok(RedisConnection {
inner: self.backend.connect().await?,
key_builder: self.key_builder.clone(),
settings: self.cache.settings().clone(),
})
}
pub async fn register_and_set_metrics(
&self,
registry: &Registry,
) -> Result<(), prometheus::Error> {
self.backend.register_metrics(registry)?;
self.blocking.register_metrics(registry)
}
pub async fn get_cached_keys<F, Fut, T, K, E>(
&self,
namespace: &str,
keys: &[K],
closure: F,
) -> Result<Vec<T>, E>
where
F: FnOnce(Vec<K>) -> Fut,
Fut: Future<Output = Result<DashMap<K, T>, E>>,
E: From<Error>,
T: Serialize + DeserializeOwned,
K: Display
+ Hash
+ Eq
+ PartialEq
+ Clone
+ DeserializeOwned
+ Serialize
+ Debug,
{
self.cache
.get_cached_keys(self, namespace, keys, closure)
.await
}
pub async fn get_cached_keys_raw<F, Fut, T, K, E>(
&self,
namespace: &str,
keys: &[K],
closure: F,
) -> Result<std::collections::HashMap<K, T>, E>
where
F: FnOnce(Vec<K>) -> Fut,
Fut: Future<Output = Result<DashMap<K, T>, E>>,
E: From<Error>,
T: Serialize + DeserializeOwned,
K: Display
+ Hash
+ Eq
+ PartialEq
+ Clone
+ DeserializeOwned
+ Serialize
+ Debug,
{
self.cache
.get_cached_keys_raw(self, namespace, keys, closure)
.await
}
pub async fn get_cached_keys_with_slug<F, Fut, T, I, K, S, E>(
&self,
namespace: &str,
slug_namespace: &str,
case_sensitive: bool,
keys: &[I],
closure: F,
) -> Result<Vec<T>, E>
where
F: FnOnce(Vec<I>) -> Fut,
Fut: Future<Output = Result<DashMap<K, (Option<S>, T)>, E>>,
E: From<Error>,
T: Serialize + DeserializeOwned,
I: Display + Hash + Eq + PartialEq + Clone + Debug,
K: Display
+ Hash
+ Eq
+ PartialEq
+ Clone
+ DeserializeOwned
+ Serialize,
S: Display + Clone + DeserializeOwned + Serialize + Debug,
{
self.cache
.get_cached_keys_with_slug(
self,
namespace,
slug_namespace,
case_sensitive,
keys,
closure,
)
.await
}
pub async fn get_cached_keys_raw_with_slug<F, Fut, T, I, K, S, E>(
&self,
namespace: &str,
slug_namespace: Option<&str>,
case_sensitive: bool,
keys: &[I],
closure: F,
) -> Result<std::collections::HashMap<K, T>, E>
where
F: FnOnce(Vec<I>) -> Fut,
Fut: Future<Output = Result<DashMap<K, (Option<S>, T)>, E>>,
E: From<Error>,
T: Serialize + DeserializeOwned,
I: Display + Hash + Eq + PartialEq + Clone + Debug,
K: Display
+ Hash
+ Eq
+ PartialEq
+ Clone
+ DeserializeOwned
+ Serialize,
S: Display + Clone + DeserializeOwned + Serialize + Debug,
{
self.cache
.get_cached_keys_raw_with_slug(
self,
namespace,
slug_namespace,
case_sensitive,
keys,
closure,
)
.await
}
}
impl ConnectionProvider for RedisPool {
type Connection = RedisConnection;
fn connect(
&self,
) -> impl Future<Output = Result<Self::Connection, Error>> + Send {
RedisPool::connect(self)
}
}
impl RedisConnection {
pub fn key(&self) -> &KeyBuilder {
&self.key_builder
}
pub async fn set<D>(
&mut self,
key: &str,
data: D,
expiry: Option<i64>,
) -> Result<(), Error>
where
D: ToRedisArgs + Send + Sync + Debug,
{
commands::set(
&mut self.inner,
key,
data,
expiry.unwrap_or(self.settings.default_expiry),
)
.await
}
pub async fn set_serialized<D>(
&mut self,
key: &str,
data: D,
expiry: Option<i64>,
) -> Result<(), Error>
where
D: Serialize,
{
commands::set_serialized(
&mut self.inner,
key,
data,
expiry,
&self.settings,
)
.await
}
pub async fn get(&mut self, key: &str) -> Result<Option<String>, Error> {
commands::get(&mut self.inner, key).await
}
pub async fn get_many(
&mut self,
keys: &[String],
) -> Result<Vec<Option<Vec<u8>>>, Error> {
commands::get_many(&mut self.inner, keys).await
}
pub async fn get_many_typed<R>(
&mut self,
keys: &[String],
) -> Result<Vec<Option<R>>, Error>
where
R: FromRedisValue,
{
commands::get_many_as(&mut self.inner, keys).await
}
pub async fn get_deserialized<R>(
&mut self,
key: &str,
) -> Result<Option<R>, Error>
where
R: for<'a> serde::Deserialize<'a>,
{
commands::get_deserialized(&mut self.inner, key, &self.settings).await
}
pub async fn get_many_deserialized<R>(
&mut self,
keys: &[String],
) -> Result<Vec<Option<R>>, Error>
where
R: for<'a> serde::Deserialize<'a>,
{
commands::get_many_deserialized(&mut self.inner, keys, &self.settings)
.await
}
pub async fn delete(&mut self, key: &str) -> Result<(), Error> {
commands::delete(&mut self.inner, key).await
}
pub async fn delete_many(&mut self, keys: &[String]) -> Result<(), Error> {
commands::delete_many(&mut self.inner, keys).await
}
pub async fn lpush<D>(&mut self, key: &str, value: D) -> Result<(), Error>
where
D: ToRedisArgs + Send + Sync + Debug,
{
commands::lpush(&mut self.inner, key, value).await
}
pub async fn incr(&mut self, key: &str) -> Result<Option<u64>, Error> {
commands::incr(&mut self.inner, key).await
}
}
impl ConnectionLike for RedisConnection {
fn req_packed_command<'a>(
&'a mut self,
command: &'a redis::Cmd,
) -> redis::RedisFuture<'a, redis::Value> {
self.inner.req_packed_command(command)
}
fn req_packed_commands<'a>(
&'a mut self,
pipeline: &'a redis::Pipeline,
offset: usize,
count: usize,
) -> redis::RedisFuture<'a, Vec<redis::Value>> {
self.inner.req_packed_commands(pipeline, offset, count)
}
fn get_db(&self) -> i64 {
self.inner.get_db()
}
}
impl connection::RoutableConnection for RedisConnection {
fn route_command<'a>(
&'a mut self,
command: redis::Cmd,
routing: redis::cluster_routing::RoutingInfo,
) -> redis::RedisFuture<'a, redis::Value> {
self.inner.route_command(command, routing)
}
}