mirror of
https://github.com/modrinth/code.git
synced 2026-09-03 13:36:48 +00:00
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:
@@ -0,0 +1,285 @@
|
||||
use std::{fmt, str::FromStr};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CacheLockingStrategy {
|
||||
#[default]
|
||||
Local,
|
||||
Distributed,
|
||||
}
|
||||
|
||||
impl CacheLockingStrategy {
|
||||
pub(super) const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Local => "local",
|
||||
Self::Distributed => "distributed",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for CacheLockingStrategy {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("invalid cache locking strategy; expected `local` or `distributed`")]
|
||||
pub struct InvalidCacheLockingStrategy;
|
||||
|
||||
impl FromStr for CacheLockingStrategy {
|
||||
type Err = InvalidCacheLockingStrategy;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value {
|
||||
"local" => Ok(Self::Local),
|
||||
"distributed" => Ok(Self::Distributed),
|
||||
_ => Err(InvalidCacheLockingStrategy),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RedisTopology {
|
||||
Standalone,
|
||||
Cluster,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("invalid Redis topology; expected `standalone` or `cluster`")]
|
||||
pub struct InvalidRedisMode;
|
||||
|
||||
impl FromStr for RedisTopology {
|
||||
type Err = InvalidRedisMode;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value {
|
||||
"standalone" => Ok(Self::Standalone),
|
||||
"cluster" => Ok(Self::Cluster),
|
||||
_ => Err(InvalidRedisMode),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RedisConnectionType {
|
||||
Pooled,
|
||||
Multiplexed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("invalid Redis connection type; expected `pooled` or `multiplexed`")]
|
||||
pub struct InvalidRedisConnectionType;
|
||||
|
||||
impl FromStr for RedisConnectionType {
|
||||
type Err = InvalidRedisConnectionType;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value {
|
||||
"pooled" => Ok(Self::Pooled),
|
||||
"multiplexed" => Ok(Self::Multiplexed),
|
||||
_ => Err(InvalidRedisConnectionType),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct RedisPoolSize {
|
||||
max: usize,
|
||||
min: usize,
|
||||
}
|
||||
|
||||
impl RedisPoolSize {
|
||||
fn new(
|
||||
name: &'static str,
|
||||
max: usize,
|
||||
min: usize,
|
||||
) -> Result<Self, RedisConfigError> {
|
||||
if max == 0 || min > max {
|
||||
return Err(RedisConfigError::InvalidPoolSize { name, max, min });
|
||||
}
|
||||
|
||||
Ok(Self { max, min })
|
||||
}
|
||||
|
||||
pub(crate) fn max(self) -> usize {
|
||||
self.max
|
||||
}
|
||||
|
||||
pub(crate) fn min(self) -> usize {
|
||||
self.min
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) enum RedisBackendConfig {
|
||||
StandalonePooled(RedisPoolSize),
|
||||
ClusterPooled(RedisPoolSize),
|
||||
ClusterMultiplexed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RedisConfig {
|
||||
mode: RedisTopology,
|
||||
backend: RedisBackendConfig,
|
||||
seed_urls: Vec<String>,
|
||||
wait_timeout_ms: u64,
|
||||
blocking_pool_size: RedisPoolSize,
|
||||
cache_locking_strategy: CacheLockingStrategy,
|
||||
read_replica_strategy: ReadReplicaStrategy,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ReadReplicaStrategy {
|
||||
Primary,
|
||||
RoundRobinReplica,
|
||||
RandomReplica,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error(
|
||||
"invalid Redis read replica strategy; expected `primary`, `round_robin`, or `random`"
|
||||
)]
|
||||
pub struct InvalidRedisReadReplicaStrategy;
|
||||
|
||||
impl FromStr for ReadReplicaStrategy {
|
||||
type Err = InvalidRedisReadReplicaStrategy;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value {
|
||||
"primary" => Ok(Self::Primary),
|
||||
"round_robin" => Ok(Self::RoundRobinReplica),
|
||||
"random" => Ok(Self::RandomReplica),
|
||||
_ => Err(InvalidRedisReadReplicaStrategy),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RedisConfigError {
|
||||
#[error("Redis configuration must contain at least one URL")]
|
||||
MissingUrl,
|
||||
#[error("standalone Redis mode requires exactly one URL")]
|
||||
MultipleStandaloneUrls,
|
||||
#[error(
|
||||
"unsupported Redis configuration: `{mode:?}` mode with `{connection_type:?}` connections"
|
||||
)]
|
||||
UnsupportedConnectionType {
|
||||
mode: RedisTopology,
|
||||
connection_type: RedisConnectionType,
|
||||
},
|
||||
#[error(
|
||||
"invalid {name} Redis pool size: minimum {min} must not exceed nonzero maximum {max}"
|
||||
)]
|
||||
InvalidPoolSize {
|
||||
name: &'static str,
|
||||
max: usize,
|
||||
min: usize,
|
||||
},
|
||||
#[error("unsupported Redis cache locking strategy `{strategy}`")]
|
||||
UnsupportedCacheLockingStrategy { strategy: CacheLockingStrategy },
|
||||
}
|
||||
|
||||
impl RedisConfig {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
mode: RedisTopology,
|
||||
connection_type: RedisConnectionType,
|
||||
raw_urls: &str,
|
||||
wait_timeout_ms: u64,
|
||||
standalone_pool_size: (usize, usize),
|
||||
cluster_pool_size: (usize, usize),
|
||||
blocking_pool_size: (usize, usize),
|
||||
cache_locking_strategy: CacheLockingStrategy,
|
||||
read_replica_strategy: ReadReplicaStrategy,
|
||||
) -> Result<Self, RedisConfigError> {
|
||||
if cache_locking_strategy == CacheLockingStrategy::Distributed {
|
||||
return Err(RedisConfigError::UnsupportedCacheLockingStrategy {
|
||||
strategy: cache_locking_strategy,
|
||||
});
|
||||
}
|
||||
|
||||
let seed_urls = raw_urls
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|url| !url.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if seed_urls.is_empty() {
|
||||
return Err(RedisConfigError::MissingUrl);
|
||||
}
|
||||
|
||||
let backend = match (mode, connection_type) {
|
||||
(RedisTopology::Standalone, RedisConnectionType::Pooled) => {
|
||||
if seed_urls.len() != 1 {
|
||||
return Err(RedisConfigError::MultipleStandaloneUrls);
|
||||
}
|
||||
RedisBackendConfig::StandalonePooled(RedisPoolSize::new(
|
||||
"standalone",
|
||||
standalone_pool_size.0,
|
||||
standalone_pool_size.1,
|
||||
)?)
|
||||
}
|
||||
(RedisTopology::Cluster, RedisConnectionType::Pooled) => {
|
||||
RedisBackendConfig::ClusterPooled(RedisPoolSize::new(
|
||||
"cluster",
|
||||
cluster_pool_size.0,
|
||||
cluster_pool_size.1,
|
||||
)?)
|
||||
}
|
||||
(RedisTopology::Cluster, RedisConnectionType::Multiplexed) => {
|
||||
RedisBackendConfig::ClusterMultiplexed
|
||||
}
|
||||
(mode, connection_type) => {
|
||||
return Err(RedisConfigError::UnsupportedConnectionType {
|
||||
mode,
|
||||
connection_type,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
mode,
|
||||
backend,
|
||||
seed_urls,
|
||||
wait_timeout_ms,
|
||||
blocking_pool_size: RedisPoolSize::new(
|
||||
"blocking",
|
||||
blocking_pool_size.0,
|
||||
blocking_pool_size.1,
|
||||
)?,
|
||||
cache_locking_strategy,
|
||||
read_replica_strategy,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn topology(&self) -> RedisTopology {
|
||||
self.mode
|
||||
}
|
||||
|
||||
pub(crate) fn backend(&self) -> RedisBackendConfig {
|
||||
self.backend
|
||||
}
|
||||
|
||||
pub fn seed_urls(&self) -> &[String] {
|
||||
&self.seed_urls
|
||||
}
|
||||
|
||||
pub fn wait_timeout_ms(&self) -> u64 {
|
||||
self.wait_timeout_ms
|
||||
}
|
||||
|
||||
pub(crate) fn blocking_pool_size(&self) -> RedisPoolSize {
|
||||
self.blocking_pool_size
|
||||
}
|
||||
|
||||
pub fn read_replica_strategy(&self) -> ReadReplicaStrategy {
|
||||
self.read_replica_strategy
|
||||
}
|
||||
|
||||
pub fn cache_locking_strategy(&self) -> CacheLockingStrategy {
|
||||
self.cache_locking_strategy
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user