mirror of
https://github.com/modrinth/code.git
synced 2026-08-26 09:34:50 +00:00
* 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
294 lines
8.0 KiB
Rust
294 lines
8.0 KiB
Rust
use std::{fmt, str::FromStr};
|
|
|
|
use eyre::{Result, WrapErr};
|
|
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> {
|
|
if max == 0 || min > max {
|
|
return Err(
|
|
RedisConfigError::InvalidPoolSize { name, max, min }.into()
|
|
);
|
|
}
|
|
|
|
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> {
|
|
if cache_locking_strategy == CacheLockingStrategy::Distributed {
|
|
return Err(RedisConfigError::UnsupportedCacheLockingStrategy {
|
|
strategy: cache_locking_strategy,
|
|
}
|
|
.into());
|
|
}
|
|
|
|
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.into());
|
|
}
|
|
|
|
let backend = match (mode, connection_type) {
|
|
(RedisTopology::Standalone, RedisConnectionType::Pooled) => {
|
|
if seed_urls.len() != 1 {
|
|
return Err(RedisConfigError::MultipleStandaloneUrls.into());
|
|
}
|
|
RedisBackendConfig::StandalonePooled(
|
|
RedisPoolSize::new(
|
|
"standalone",
|
|
standalone_pool_size.0,
|
|
standalone_pool_size.1,
|
|
)
|
|
.wrap_err("validating standalone Redis pool size")?,
|
|
)
|
|
}
|
|
(RedisTopology::Cluster, RedisConnectionType::Pooled) => {
|
|
RedisBackendConfig::ClusterPooled(
|
|
RedisPoolSize::new(
|
|
"cluster",
|
|
cluster_pool_size.0,
|
|
cluster_pool_size.1,
|
|
)
|
|
.wrap_err("validating clustered Redis pool size")?,
|
|
)
|
|
}
|
|
(RedisTopology::Cluster, RedisConnectionType::Multiplexed) => {
|
|
RedisBackendConfig::ClusterMultiplexed
|
|
}
|
|
(mode, connection_type) => {
|
|
return Err(RedisConfigError::UnsupportedConnectionType {
|
|
mode,
|
|
connection_type,
|
|
}
|
|
.into());
|
|
}
|
|
};
|
|
|
|
Ok(Self {
|
|
mode,
|
|
backend,
|
|
seed_urls,
|
|
wait_timeout_ms,
|
|
blocking_pool_size: RedisPoolSize::new(
|
|
"blocking",
|
|
blocking_pool_size.0,
|
|
blocking_pool_size.1,
|
|
)
|
|
.wrap_err("validating blocking Redis pool size")?,
|
|
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
|
|
}
|
|
}
|