mirror of
https://github.com/modrinth/code.git
synced 2026-08-26 17:44:50 +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,732 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::future::Future;
|
||||
use std::hash::Hash;
|
||||
use std::str::FromStr;
|
||||
|
||||
use ariadne::ids::base62_impl::{parse_base62, to_base62};
|
||||
use chrono::{TimeZone, Utc};
|
||||
use dashmap::DashMap;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use redis::aio::ConnectionLike;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use tokio::time::{Instant, timeout_at};
|
||||
use tracing::{Instrument, info_span};
|
||||
|
||||
use crate::Error;
|
||||
|
||||
use super::commands;
|
||||
use super::connection::RoutableConnection;
|
||||
use super::key::KeyBuilder;
|
||||
|
||||
mod locking;
|
||||
|
||||
use locking::{
|
||||
LockAcquisition, LockCoordinator, LockWaiter, WAIT_TIMEOUT, normalize_key,
|
||||
};
|
||||
|
||||
const FILL_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
|
||||
|
||||
pub(super) trait ConnectionProvider {
|
||||
type Connection: ConnectionLike + RoutableConnection;
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
) -> impl Future<Output = Result<Self::Connection, Error>> + Send;
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum CacheReadRouting {
|
||||
ReplicaOptional,
|
||||
Primary,
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Codec {
|
||||
Raw = 0,
|
||||
Lz4 = 1,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum EncodingFormat {
|
||||
Json,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("invalid redis codec")]
|
||||
pub struct InvalidCodec;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("invalid redis encoding format")]
|
||||
pub struct InvalidEncodingFormat;
|
||||
|
||||
impl TryFrom<u8> for Codec {
|
||||
type Error = InvalidCodec;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(Self::Raw),
|
||||
1 => Ok(Self::Lz4),
|
||||
_ => Err(InvalidCodec),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Codec {
|
||||
type Err = InvalidCodec;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value {
|
||||
"lz4" => Ok(Self::Lz4),
|
||||
_ => Err(InvalidCodec),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for EncodingFormat {
|
||||
type Err = InvalidEncodingFormat;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value {
|
||||
"json" => Ok(Self::Json),
|
||||
_ => Err(InvalidEncodingFormat),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CacheSettings {
|
||||
pub default_expiry: i64,
|
||||
pub actual_expiry: i64,
|
||||
pub version_default_expiry: i64,
|
||||
pub version_actual_expiry: i64,
|
||||
pub encoding_format: EncodingFormat,
|
||||
pub compression_algorithm: Codec,
|
||||
pub compression_level: i32,
|
||||
pub compression_threshold_bytes: usize,
|
||||
pub compression_min_savings_ratio: f64,
|
||||
}
|
||||
|
||||
impl CacheSettings {
|
||||
pub fn encode_value<T: Serialize>(
|
||||
&self,
|
||||
value: &T,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
let mut value = match self.encoding_format {
|
||||
EncodingFormat::Json => serde_json::to_vec(value)?,
|
||||
};
|
||||
|
||||
if self.compression_level > 0
|
||||
&& self.compression_algorithm == Codec::Lz4
|
||||
&& value.len() >= self.compression_threshold_bytes
|
||||
{
|
||||
let compressed = lz4_flex::block::compress_prepend_size(&value);
|
||||
let savings_ratio = value.len().saturating_sub(compressed.len())
|
||||
as f64
|
||||
/ value.len().max(1) as f64
|
||||
* 100.0;
|
||||
|
||||
if savings_ratio >= self.compression_min_savings_ratio {
|
||||
let mut encoded = Vec::with_capacity(compressed.len() + 1);
|
||||
encoded.push(Codec::Lz4 as u8);
|
||||
encoded.extend(compressed);
|
||||
return Ok(encoded);
|
||||
}
|
||||
}
|
||||
|
||||
let mut encoded = Vec::with_capacity(value.len() + 1);
|
||||
encoded.push(Codec::Raw as u8);
|
||||
encoded.append(&mut value);
|
||||
Ok(encoded)
|
||||
}
|
||||
|
||||
pub fn decode_value<T>(&self, value: &[u8]) -> Option<T>
|
||||
where
|
||||
T: for<'a> Deserialize<'a>,
|
||||
{
|
||||
let (codec, value) = value.split_first()?;
|
||||
let value = match Codec::try_from(*codec).ok()? {
|
||||
Codec::Raw => Cow::Borrowed(value),
|
||||
Codec::Lz4 => Cow::Owned(
|
||||
lz4_flex::block::decompress_size_prepended(value).ok()?,
|
||||
),
|
||||
};
|
||||
|
||||
match self.encoding_format {
|
||||
EncodingFormat::Json => serde_json::from_slice(&value).ok(),
|
||||
}
|
||||
}
|
||||
|
||||
fn expiries(&self, namespace: &str) -> (i64, i64) {
|
||||
match namespace
|
||||
.split_once(':')
|
||||
.map(|value| value.0)
|
||||
.unwrap_or(namespace)
|
||||
{
|
||||
"versions" | "versions_files" => {
|
||||
(self.version_default_expiry, self.version_actual_expiry)
|
||||
}
|
||||
_ => (self.default_expiry, self.actual_expiry),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CacheManager {
|
||||
key_builder: KeyBuilder,
|
||||
settings: CacheSettings,
|
||||
locking: LockCoordinator,
|
||||
}
|
||||
|
||||
impl CacheManager {
|
||||
pub fn new(key_builder: KeyBuilder, settings: CacheSettings) -> Self {
|
||||
Self {
|
||||
locking: LockCoordinator::new(),
|
||||
key_builder,
|
||||
settings,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn settings(&self) -> &CacheSettings {
|
||||
&self.settings
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, provider, keys, closure))]
|
||||
pub async fn get_cached_keys<P, F, Fut, T, K, E>(
|
||||
&self,
|
||||
provider: &P,
|
||||
namespace: &str,
|
||||
keys: &[K],
|
||||
closure: F,
|
||||
) -> Result<Vec<T>, E>
|
||||
where
|
||||
P: ConnectionProvider,
|
||||
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,
|
||||
{
|
||||
Ok(self
|
||||
.get_cached_keys_raw(provider, namespace, keys, closure)
|
||||
.await?
|
||||
.into_values()
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, provider, keys, closure))]
|
||||
pub async fn get_cached_keys_raw<P, F, Fut, T, K, E>(
|
||||
&self,
|
||||
provider: &P,
|
||||
namespace: &str,
|
||||
keys: &[K],
|
||||
closure: F,
|
||||
) -> Result<HashMap<K, T>, E>
|
||||
where
|
||||
P: ConnectionProvider,
|
||||
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.get_cached_keys_raw_with_slug(
|
||||
provider,
|
||||
namespace,
|
||||
None,
|
||||
false,
|
||||
keys,
|
||||
|ids| async move {
|
||||
Ok(closure(ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|(key, value)| (key, (None::<String>, value)))
|
||||
.collect())
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, provider, keys, closure))]
|
||||
pub async fn get_cached_keys_with_slug<P, F, Fut, T, I, K, S, E>(
|
||||
&self,
|
||||
provider: &P,
|
||||
namespace: &str,
|
||||
slug_namespace: &str,
|
||||
case_sensitive: bool,
|
||||
keys: &[I],
|
||||
closure: F,
|
||||
) -> Result<Vec<T>, E>
|
||||
where
|
||||
P: ConnectionProvider,
|
||||
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,
|
||||
{
|
||||
Ok(self
|
||||
.get_cached_keys_raw_with_slug(
|
||||
provider,
|
||||
namespace,
|
||||
Some(slug_namespace),
|
||||
case_sensitive,
|
||||
keys,
|
||||
closure,
|
||||
)
|
||||
.await?
|
||||
.into_values()
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, provider, keys, closure))]
|
||||
pub async fn get_cached_keys_raw_with_slug<P, F, Fut, T, I, K, S, E>(
|
||||
&self,
|
||||
provider: &P,
|
||||
namespace: &str,
|
||||
slug_namespace: Option<&str>,
|
||||
case_sensitive: bool,
|
||||
keys: &[I],
|
||||
closure: F,
|
||||
) -> Result<HashMap<K, T>, E>
|
||||
where
|
||||
P: ConnectionProvider,
|
||||
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,
|
||||
{
|
||||
let ids = keys
|
||||
.iter()
|
||||
.map(|key| {
|
||||
(normalize_key(&key.to_string(), case_sensitive), key.clone())
|
||||
})
|
||||
.collect::<DashMap<String, I>>();
|
||||
|
||||
if ids.is_empty() {
|
||||
return Ok(HashMap::new());
|
||||
}
|
||||
|
||||
let get_cached_values =
|
||||
|ids: DashMap<String, I>, routing: CacheReadRouting| {
|
||||
async move {
|
||||
let slug_ids = if let Some(slug_namespace) = slug_namespace
|
||||
{
|
||||
async {
|
||||
let keys = ids
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
let logical_key = normalize_key(
|
||||
&entry.value().to_string(),
|
||||
case_sensitive,
|
||||
);
|
||||
self.key_builder
|
||||
.entity(slug_namespace, logical_key)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut connection =
|
||||
provider.connect().await.map_err(E::from)?;
|
||||
let values = match routing {
|
||||
CacheReadRouting::ReplicaOptional => {
|
||||
commands::get_many_strings(
|
||||
&mut connection,
|
||||
&keys,
|
||||
)
|
||||
.await
|
||||
}
|
||||
CacheReadRouting::Primary => {
|
||||
commands::get_many_strings_primary(
|
||||
&mut connection,
|
||||
&keys,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
.map_err(E::from)?;
|
||||
Ok::<_, E>(
|
||||
values
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
.instrument(info_span!("get slug ids"))
|
||||
.await?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let keys = ids
|
||||
.iter()
|
||||
.map(|entry| entry.value().to_string())
|
||||
.chain(ids.iter().filter_map(|entry| {
|
||||
parse_base62(&entry.value().to_string())
|
||||
.ok()
|
||||
.map(|value| value.to_string())
|
||||
}))
|
||||
.chain(slug_ids)
|
||||
.map(|key| self.key_builder.entity(namespace, key))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut connection =
|
||||
provider.connect().await.map_err(E::from)?;
|
||||
let mut cached_values = HashMap::new();
|
||||
let values = match routing {
|
||||
CacheReadRouting::ReplicaOptional => {
|
||||
commands::get_many(&mut connection, &keys).await
|
||||
}
|
||||
CacheReadRouting::Primary => {
|
||||
commands::get_many_primary(&mut connection, &keys)
|
||||
.await
|
||||
}
|
||||
}
|
||||
.map_err(E::from)?;
|
||||
for value in values {
|
||||
if let Some(value) = value.and_then(|value| {
|
||||
self.settings
|
||||
.decode_value::<RedisValue<T, K, S>>(&value)
|
||||
}) {
|
||||
cached_values.insert(value.key.clone(), value);
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<_, E>((cached_values, ids))
|
||||
}
|
||||
.instrument(info_span!("get_cached_values_closure"))
|
||||
};
|
||||
|
||||
let (default_expiry, actual_expiry) = self.settings.expiries(namespace);
|
||||
let current_time = Utc::now();
|
||||
let mut expired_values = HashMap::new();
|
||||
let mut expired_identities = HashMap::new();
|
||||
let deadline = Instant::now() + WAIT_TIMEOUT;
|
||||
|
||||
let (cached_values_raw, ids) =
|
||||
get_cached_values(ids, CacheReadRouting::ReplicaOptional).await?;
|
||||
let mut cached_values = cached_values_raw
|
||||
.into_iter()
|
||||
.filter_map(|(key, value)| {
|
||||
if Utc.timestamp_opt(value.iat + actual_expiry, 0).unwrap()
|
||||
< current_time
|
||||
{
|
||||
let canonical_key = value.key.to_string();
|
||||
for identity in value_identities(&value, case_sensitive) {
|
||||
expired_identities
|
||||
.insert(identity, canonical_key.clone());
|
||||
}
|
||||
expired_values.insert(canonical_key, value);
|
||||
None
|
||||
} else {
|
||||
remove_resolved_ids(&ids, &value, case_sensitive);
|
||||
Some((key, value))
|
||||
}
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let mut waiters = Vec::new();
|
||||
let mut owned_locks = HashMap::new();
|
||||
|
||||
if !ids.is_empty() {
|
||||
let fetch_ids = ids
|
||||
.iter()
|
||||
.map(|entry| entry.key().clone())
|
||||
.collect::<Vec<_>>();
|
||||
for key in fetch_ids {
|
||||
if !ids.contains_key(&key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let lock_key = self.key_builder.entity(namespace, &key);
|
||||
let acquisition = self.locking.acquire(lock_key);
|
||||
|
||||
match acquisition {
|
||||
LockAcquisition::Owned(guard) => {
|
||||
owned_locks.insert(key, guard);
|
||||
}
|
||||
LockAcquisition::Waiting(waiter) => {
|
||||
if let Some(canonical_key) =
|
||||
expired_identities.get(&key).cloned()
|
||||
&& let Some(value) =
|
||||
expired_values.remove(&canonical_key)
|
||||
{
|
||||
remove_resolved_ids(&ids, &value, case_sensitive);
|
||||
expired_identities.retain(|_, canonical| {
|
||||
canonical != &canonical_key
|
||||
});
|
||||
cached_values.insert(value.key.clone(), value);
|
||||
} else if let Some((_, raw_key)) = ids.remove(&key) {
|
||||
waiters.push((raw_key, waiter));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let fill_result = if !ids.is_empty() {
|
||||
async {
|
||||
let fetch_ids = ids
|
||||
.iter()
|
||||
.map(|entry| entry.value().clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let fill_deadline = Instant::now() + FILL_TIMEOUT;
|
||||
|
||||
let values = timeout_at(fill_deadline, closure(fetch_ids))
|
||||
.await
|
||||
.map_err(|_| lock_timeout_error(0, waiters.len()))??;
|
||||
|
||||
let mut return_values = HashMap::new();
|
||||
let mut encoded_values = Vec::with_capacity(values.len());
|
||||
|
||||
for (key, (slug, value)) in values {
|
||||
let value = RedisValue {
|
||||
key: key.clone(),
|
||||
iat: Utc::now().timestamp(),
|
||||
val: value,
|
||||
alias: slug.clone(),
|
||||
};
|
||||
let encoded =
|
||||
self.settings.encode_value(&value).map_err(E::from)?;
|
||||
encoded_values.push((key, slug, value, encoded));
|
||||
}
|
||||
|
||||
let mut connection =
|
||||
provider.connect().await.map_err(E::from)?;
|
||||
for (key, slug, _, encoded) in &encoded_values {
|
||||
let redis_key =
|
||||
self.key_builder.entity(namespace, key.to_string());
|
||||
commands::set(
|
||||
&mut connection,
|
||||
&redis_key,
|
||||
encoded,
|
||||
default_expiry,
|
||||
)
|
||||
.await
|
||||
.map_err(E::from)?;
|
||||
if let Some(slug) = slug
|
||||
&& let Some(slug_namespace) = slug_namespace
|
||||
{
|
||||
let canonical_key = key.to_string();
|
||||
let actual_slug =
|
||||
normalize_key(&slug.to_string(), case_sensitive);
|
||||
let slug_key = self
|
||||
.key_builder
|
||||
.entity(slug_namespace, actual_slug);
|
||||
commands::set(
|
||||
&mut connection,
|
||||
&slug_key,
|
||||
canonical_key.as_bytes(),
|
||||
default_expiry,
|
||||
)
|
||||
.await
|
||||
.map_err(E::from)?;
|
||||
}
|
||||
}
|
||||
|
||||
for (key, _, value, _) in encoded_values {
|
||||
remove_resolved_ids(&ids, &value, case_sensitive);
|
||||
return_values.insert(key, value);
|
||||
}
|
||||
|
||||
Result::<_, E>::Ok(return_values)
|
||||
}
|
||||
.await
|
||||
} else {
|
||||
Ok(HashMap::new())
|
||||
};
|
||||
|
||||
drop(owned_locks);
|
||||
|
||||
let operation_result = match fill_result {
|
||||
Ok(mut values) => {
|
||||
if waiters.is_empty() {
|
||||
Ok(values)
|
||||
} else {
|
||||
match wait_for_locks(waiters, deadline).await {
|
||||
Ok(released_ids) => {
|
||||
let fetch_ids = released_ids
|
||||
.into_iter()
|
||||
.map(|key| {
|
||||
(
|
||||
normalize_key(
|
||||
&key.to_string(),
|
||||
case_sensitive,
|
||||
),
|
||||
key,
|
||||
)
|
||||
})
|
||||
.collect::<DashMap<_, _>>();
|
||||
match get_cached_values(
|
||||
fetch_ids,
|
||||
CacheReadRouting::Primary,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((released_values, _)) => {
|
||||
values.extend(released_values);
|
||||
Ok(values)
|
||||
}
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
Err(error) => Err(E::from(error)),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => Err(error),
|
||||
};
|
||||
cached_values.extend(operation_result?);
|
||||
|
||||
Ok(cached_values
|
||||
.into_iter()
|
||||
.map(|(key, value)| (key, value.val))
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_resolved_ids<I, T, K, S>(
|
||||
ids: &DashMap<String, I>,
|
||||
value: &RedisValue<T, K, S>,
|
||||
case_sensitive: bool,
|
||||
) where
|
||||
K: Display,
|
||||
S: Display,
|
||||
{
|
||||
for identity in value_identities(value, case_sensitive) {
|
||||
ids.remove(&normalize_key(&identity, case_sensitive));
|
||||
}
|
||||
}
|
||||
|
||||
fn value_identities<T, K, S>(
|
||||
value: &RedisValue<T, K, S>,
|
||||
case_sensitive: bool,
|
||||
) -> Vec<String>
|
||||
where
|
||||
K: Display,
|
||||
S: Display,
|
||||
{
|
||||
let mut identities = Vec::with_capacity(5);
|
||||
let canonical_key = value.key.to_string();
|
||||
|
||||
push_identity(&mut identities, canonical_key.clone());
|
||||
if !case_sensitive {
|
||||
push_identity(&mut identities, canonical_key.to_lowercase());
|
||||
}
|
||||
|
||||
if let Ok(decimal_id) = canonical_key.parse::<u64>() {
|
||||
let base62_id = to_base62(decimal_id);
|
||||
push_identity(&mut identities, base62_id.clone());
|
||||
|
||||
if !case_sensitive {
|
||||
push_identity(&mut identities, base62_id.to_lowercase());
|
||||
}
|
||||
} else if let Ok(decimal_id) = parse_base62(&canonical_key) {
|
||||
push_identity(&mut identities, decimal_id.to_string());
|
||||
}
|
||||
|
||||
if let Some(alias) = &value.alias {
|
||||
let alias = alias.to_string();
|
||||
push_identity(&mut identities, alias.clone());
|
||||
if !case_sensitive {
|
||||
push_identity(&mut identities, alias.to_lowercase());
|
||||
}
|
||||
}
|
||||
|
||||
identities
|
||||
}
|
||||
|
||||
fn push_identity(identities: &mut Vec<String>, identity: String) {
|
||||
if !identities.contains(&identity) {
|
||||
identities.push(identity);
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_locks<I>(
|
||||
waiters: Vec<(I, LockWaiter)>,
|
||||
deadline: Instant,
|
||||
) -> Result<Vec<I>, Error> {
|
||||
let total = waiters.len();
|
||||
let mut released = Vec::with_capacity(total);
|
||||
let mut futures = FuturesUnordered::new();
|
||||
for (key, waiter) in waiters {
|
||||
futures.push(async move {
|
||||
let result = waiter.wait(deadline).await;
|
||||
(key, result)
|
||||
});
|
||||
}
|
||||
|
||||
while let Some((key, result)) = futures.next().await {
|
||||
match result {
|
||||
Ok(()) => {
|
||||
released.push(key);
|
||||
}
|
||||
Err(error)
|
||||
if is_lock_timeout(&error) || Instant::now() >= deadline =>
|
||||
{
|
||||
return Err(lock_timeout_error(released.len(), total));
|
||||
}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
Ok(released)
|
||||
}
|
||||
|
||||
fn is_lock_timeout(error: &Error) -> bool {
|
||||
matches!(error, Error::LocalCacheTimeout { .. })
|
||||
}
|
||||
|
||||
fn lock_timeout_error(locks_released: usize, locks_waiting: usize) -> Error {
|
||||
Error::LocalCacheTimeout {
|
||||
released: locks_released,
|
||||
total: locks_waiting,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct RedisValue<T, K, S> {
|
||||
key: K,
|
||||
alias: Option<S>,
|
||||
iat: i64,
|
||||
val: T,
|
||||
}
|
||||
|
||||
impl<T, K, S> RedisValue<T, K, S> {
|
||||
pub fn value(&self) -> &T {
|
||||
&self.val
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user