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:
@@ -1,17 +1,15 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::{StreamExt, TryStreamExt};
|
||||
use sqlx::types::Json;
|
||||
use xredis::RedisPool;
|
||||
|
||||
use crate::{
|
||||
database::{
|
||||
models::{DBAnalyticsEventId, DatabaseError},
|
||||
redis::RedisPool,
|
||||
},
|
||||
database::models::{DBAnalyticsEventId, DatabaseError},
|
||||
models::v3::analytics_event::AnalyticsEventMeta,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const ANALYTICS_EVENTS_NAMESPACE: &str = "analytics_events:v1";
|
||||
const ANALYTICS_EVENTS_NAMESPACE: &str = "analytics_events:v3";
|
||||
const ANALYTICS_EVENTS_ALL_KEY: &str = "all";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -86,14 +84,11 @@ impl DBAnalyticsEvent {
|
||||
redis: &RedisPool,
|
||||
) -> Result<Vec<DBAnalyticsEvent>, DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis
|
||||
.key()
|
||||
.metadata(ANALYTICS_EVENTS_NAMESPACE, ANALYTICS_EVENTS_ALL_KEY);
|
||||
|
||||
if let Some(events) = redis
|
||||
.get_deserialized(
|
||||
ANALYTICS_EVENTS_NAMESPACE,
|
||||
ANALYTICS_EVENTS_ALL_KEY,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
if let Some(events) = redis.get_deserialized(&key).await? {
|
||||
return Ok(events);
|
||||
}
|
||||
|
||||
@@ -118,23 +113,17 @@ impl DBAnalyticsEvent {
|
||||
.try_collect::<Vec<_>>()
|
||||
.await?;
|
||||
|
||||
redis
|
||||
.set_serialized(
|
||||
ANALYTICS_EVENTS_NAMESPACE,
|
||||
ANALYTICS_EVENTS_ALL_KEY,
|
||||
&events,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &events, None).await?;
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
pub async fn clear_cache(redis: &RedisPool) -> Result<(), DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
redis
|
||||
.delete(ANALYTICS_EVENTS_NAMESPACE, ANALYTICS_EVENTS_ALL_KEY)
|
||||
.await?;
|
||||
let key = redis
|
||||
.key()
|
||||
.metadata(ANALYTICS_EVENTS_NAMESPACE, ANALYTICS_EVENTS_ALL_KEY);
|
||||
redis.delete(&key).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::database::redis::RedisPool;
|
||||
use xredis::RedisPool;
|
||||
|
||||
use super::DatabaseError;
|
||||
use super::ids::*;
|
||||
use futures::TryStreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const TAGS_NAMESPACE: &str = "tags:v1";
|
||||
const TAGS_NAMESPACE: &str = "tags:v3";
|
||||
|
||||
pub struct ProjectType {
|
||||
pub id: ProjectTypeId,
|
||||
@@ -95,9 +95,10 @@ impl Category {
|
||||
{
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(TAGS_NAMESPACE, "category");
|
||||
|
||||
let res: Option<Vec<Category>> =
|
||||
redis.get_deserialized(TAGS_NAMESPACE, "category").await?;
|
||||
redis.get_deserialized(&key).await?;
|
||||
|
||||
if let Some(res) = res {
|
||||
return Ok(res);
|
||||
@@ -124,10 +125,9 @@ impl Category {
|
||||
.await?;
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(TAGS_NAMESPACE, "category");
|
||||
|
||||
redis
|
||||
.set_serialized(TAGS_NAMESPACE, "category", &result, None)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &result, None).await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -163,10 +163,10 @@ impl LinkPlatform {
|
||||
{
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(TAGS_NAMESPACE, "link_platform");
|
||||
|
||||
let res: Option<Vec<LinkPlatform>> = redis
|
||||
.get_deserialized(TAGS_NAMESPACE, "link_platform")
|
||||
.await?;
|
||||
let res: Option<Vec<LinkPlatform>> =
|
||||
redis.get_deserialized(&key).await?;
|
||||
|
||||
if let Some(res) = res {
|
||||
return Ok(res);
|
||||
@@ -188,10 +188,9 @@ impl LinkPlatform {
|
||||
.await?;
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(TAGS_NAMESPACE, "link_platform");
|
||||
|
||||
redis
|
||||
.set_serialized(TAGS_NAMESPACE, "link_platform", &result, None)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &result, None).await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -227,10 +226,9 @@ impl ReportType {
|
||||
{
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(TAGS_NAMESPACE, "report_type");
|
||||
|
||||
let res: Option<Vec<String>> = redis
|
||||
.get_deserialized(TAGS_NAMESPACE, "report_type")
|
||||
.await?;
|
||||
let res: Option<Vec<String>> = redis.get_deserialized(&key).await?;
|
||||
|
||||
if let Some(res) = res {
|
||||
return Ok(res);
|
||||
@@ -248,10 +246,9 @@ impl ReportType {
|
||||
.await?;
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(TAGS_NAMESPACE, "report_type");
|
||||
|
||||
redis
|
||||
.set_serialized(TAGS_NAMESPACE, "report_type", &result, None)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &result, None).await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -287,10 +284,9 @@ impl ProjectType {
|
||||
{
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(TAGS_NAMESPACE, "project_type");
|
||||
|
||||
let res: Option<Vec<String>> = redis
|
||||
.get_deserialized(TAGS_NAMESPACE, "project_type")
|
||||
.await?;
|
||||
let res: Option<Vec<String>> = redis.get_deserialized(&key).await?;
|
||||
|
||||
if let Some(res) = res {
|
||||
return Ok(res);
|
||||
@@ -308,10 +304,9 @@ impl ProjectType {
|
||||
.await?;
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(TAGS_NAMESPACE, "project_type");
|
||||
|
||||
redis
|
||||
.set_serialized(TAGS_NAMESPACE, "project_type", &result, None)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &result, None).await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use super::ids::*;
|
||||
use crate::database::models::DatabaseError;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::database::{PgTransaction, models};
|
||||
use crate::models::collections::CollectionStatus;
|
||||
use chrono::{DateTime, Utc};
|
||||
use dashmap::DashMap;
|
||||
use futures::TryStreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use xredis::RedisPool;
|
||||
|
||||
const COLLECTIONS_NAMESPACE: &str = "collections:v1";
|
||||
const COLLECTIONS_NAMESPACE: &str = "collections:v3";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CollectionBuilder {
|
||||
@@ -204,7 +204,7 @@ impl DBCollection {
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(collections)
|
||||
Ok::<_, DatabaseError>(collections)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -217,8 +217,9 @@ impl DBCollection {
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().entity(COLLECTIONS_NAMESPACE, id.0);
|
||||
|
||||
redis.delete(COLLECTIONS_NAMESPACE, id.0).await?;
|
||||
redis.delete(&key).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use super::ids::*;
|
||||
use crate::auth::oauth::uris::OAuthRedirectUris;
|
||||
use crate::database::models::DatabaseError;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::{auth::AuthProvider, routes::internal::flows::TempUser};
|
||||
use chrono::Duration;
|
||||
@@ -12,8 +11,9 @@ use rand_chacha::rand_core::SeedableRng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
use webauthn_rs::prelude::{DiscoverableAuthentication, PasskeyRegistration};
|
||||
use xredis::RedisPool;
|
||||
|
||||
const FLOWS_NAMESPACE: &str = "flows:v1";
|
||||
const FLOWS_NAMESPACE: &str = "flows:v3";
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub enum DBFlow {
|
||||
@@ -75,14 +75,10 @@ impl DBFlow {
|
||||
state: &str,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().entity(FLOWS_NAMESPACE, state);
|
||||
|
||||
redis
|
||||
.set_serialized(
|
||||
FLOWS_NAMESPACE,
|
||||
&state,
|
||||
&self,
|
||||
Some(expires.num_seconds()),
|
||||
)
|
||||
.set_serialized(&key, &self, Some(expires.num_seconds()))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -107,8 +103,9 @@ impl DBFlow {
|
||||
redis: &RedisPool,
|
||||
) -> Result<Option<DBFlow>, DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().entity(FLOWS_NAMESPACE, id);
|
||||
|
||||
redis.get_deserialized(FLOWS_NAMESPACE, id).await
|
||||
redis.get_deserialized(&key).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Gets the flow and removes it from the cache, but only removes if the flow was present and the predicate returned true
|
||||
@@ -132,8 +129,9 @@ impl DBFlow {
|
||||
redis: &RedisPool,
|
||||
) -> Result<Option<()>, DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().entity(FLOWS_NAMESPACE, id);
|
||||
|
||||
redis.delete(FLOWS_NAMESPACE, id).await?;
|
||||
redis.delete(&key).await?;
|
||||
Ok(Some(()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use super::ids::*;
|
||||
use crate::database::PgTransaction;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::{database::models::DatabaseError, models::images::ImageContext};
|
||||
use chrono::{DateTime, Utc};
|
||||
use dashmap::DashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use xredis::RedisPool;
|
||||
|
||||
const IMAGES_NAMESPACE: &str = "images:v1";
|
||||
const IMAGES_NAMESPACE: &str = "images:v3";
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct DBImage {
|
||||
@@ -217,7 +217,7 @@ impl DBImage {
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(images)
|
||||
Ok::<_, DatabaseError>(images)
|
||||
},
|
||||
).await?;
|
||||
|
||||
@@ -229,8 +229,9 @@ impl DBImage {
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().entity(IMAGES_NAMESPACE, id.0);
|
||||
|
||||
redis.delete(IMAGES_NAMESPACE, id.0).await?;
|
||||
redis.delete(&key).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::database::redis::RedisPool;
|
||||
use xredis::RedisPool;
|
||||
|
||||
use super::{
|
||||
DatabaseError, LoaderFieldEnumValueId,
|
||||
@@ -220,11 +220,11 @@ impl<'a> MinecraftGameVersionBuilder<'a> {
|
||||
.await?;
|
||||
|
||||
let mut conn = redis.connect().await?;
|
||||
conn.delete(
|
||||
let key = conn.key().entity(
|
||||
crate::database::models::loader_fields::LOADER_FIELD_ENUM_VALUES_NAMESPACE,
|
||||
game_versions_enum.id.0,
|
||||
)
|
||||
.await?;
|
||||
);
|
||||
conn.delete(&key).await?;
|
||||
|
||||
Ok(LoaderFieldEnumValueId(result.id))
|
||||
}
|
||||
|
||||
@@ -4,22 +4,22 @@ use std::hash::Hasher;
|
||||
use super::DatabaseError;
|
||||
use super::ids::*;
|
||||
use crate::database::PgTransaction;
|
||||
use crate::database::redis::RedisPool;
|
||||
use chrono::DateTime;
|
||||
use chrono::Utc;
|
||||
use dashmap::DashMap;
|
||||
use futures::TryStreamExt;
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use xredis::RedisPool;
|
||||
|
||||
const GAMES_LIST_NAMESPACE: &str = "games:v1";
|
||||
const LOADER_ID: &str = "loader_id:v1";
|
||||
const LOADERS_LIST_NAMESPACE: &str = "loaders:v1";
|
||||
const LOADER_FIELDS_NAMESPACE: &str = "loader_fields:v1";
|
||||
const LOADER_FIELDS_NAMESPACE_ALL: &str = "loader_fields_all:v1";
|
||||
const LOADER_FIELD_ENUMS_ID_NAMESPACE: &str = "loader_field_enums:v1";
|
||||
const GAMES_LIST_NAMESPACE: &str = "games:v3";
|
||||
const LOADER_ID: &str = "loader_id:v3";
|
||||
const LOADERS_LIST_NAMESPACE: &str = "loaders:v3";
|
||||
const LOADER_FIELDS_NAMESPACE: &str = "loader_fields:v3";
|
||||
const LOADER_FIELDS_NAMESPACE_ALL: &str = "loader_fields_all:v3";
|
||||
const LOADER_FIELD_ENUMS_ID_NAMESPACE: &str = "loader_field_enums:v3";
|
||||
pub const LOADER_FIELD_ENUM_VALUES_NAMESPACE: &str =
|
||||
"loader_field_enum_values:v1";
|
||||
"loader_field_enum_values:v3";
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct Game {
|
||||
@@ -54,9 +54,9 @@ impl Game {
|
||||
{
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let cached_games: Option<Vec<Game>> = redis
|
||||
.get_deserialized(GAMES_LIST_NAMESPACE, "games")
|
||||
.await?;
|
||||
let key = redis.key().metadata(GAMES_LIST_NAMESPACE, "games");
|
||||
let cached_games: Option<Vec<Game>> =
|
||||
redis.get_deserialized(&key).await?;
|
||||
if let Some(cached_games) = cached_games {
|
||||
return Ok(cached_games);
|
||||
}
|
||||
@@ -79,10 +79,9 @@ impl Game {
|
||||
.await?;
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(GAMES_LIST_NAMESPACE, "games");
|
||||
|
||||
redis
|
||||
.set_serialized(GAMES_LIST_NAMESPACE, "games", &result, None)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &result, None).await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -109,8 +108,8 @@ impl Loader {
|
||||
{
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let cached_id: Option<i32> =
|
||||
redis.get_deserialized(LOADER_ID, name).await?;
|
||||
let key = redis.key().metadata(LOADER_ID, name);
|
||||
let cached_id: Option<i32> = redis.get_deserialized(&key).await?;
|
||||
if let Some(cached_id) = cached_id {
|
||||
return Ok(Some(LoaderId(cached_id)));
|
||||
}
|
||||
@@ -129,9 +128,8 @@ impl Loader {
|
||||
|
||||
if let Some(result) = result {
|
||||
let mut redis = redis.connect().await?;
|
||||
redis
|
||||
.set_serialized(LOADER_ID, name, &result.0, None)
|
||||
.await?;
|
||||
let key = redis.key().metadata(LOADER_ID, name);
|
||||
redis.set_serialized(&key, &result.0, None).await?;
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
@@ -146,9 +144,9 @@ impl Loader {
|
||||
{
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let cached_loaders: Option<Vec<Loader>> = redis
|
||||
.get_deserialized(LOADERS_LIST_NAMESPACE, "all")
|
||||
.await?;
|
||||
let key = redis.key().metadata(LOADERS_LIST_NAMESPACE, "all");
|
||||
let cached_loaders: Option<Vec<Loader>> =
|
||||
redis.get_deserialized(&key).await?;
|
||||
if let Some(cached_loaders) = cached_loaders {
|
||||
return Ok(cached_loaders);
|
||||
}
|
||||
@@ -187,10 +185,9 @@ impl Loader {
|
||||
.await?;
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(LOADERS_LIST_NAMESPACE, "all");
|
||||
|
||||
redis
|
||||
.set_serialized(LOADERS_LIST_NAMESPACE, "all", &result, None)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &result, None).await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -441,7 +438,7 @@ impl LoaderField {
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
Ok::<_, DatabaseError>(result)
|
||||
},
|
||||
).await?;
|
||||
|
||||
@@ -460,10 +457,10 @@ impl LoaderField {
|
||||
{
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(LOADER_FIELDS_NAMESPACE_ALL, "");
|
||||
|
||||
let cached_fields: Option<Vec<LoaderField>> = redis
|
||||
.get_deserialized(LOADER_FIELDS_NAMESPACE_ALL, "")
|
||||
.await?;
|
||||
let cached_fields: Option<Vec<LoaderField>> =
|
||||
redis.get_deserialized(&key).await?;
|
||||
|
||||
if let Some(cached_fields) = cached_fields {
|
||||
return Ok(cached_fields);
|
||||
@@ -494,10 +491,9 @@ impl LoaderField {
|
||||
.collect();
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(LOADER_FIELDS_NAMESPACE_ALL, "");
|
||||
|
||||
redis
|
||||
.set_serialized(LOADER_FIELDS_NAMESPACE_ALL, "", &result, None)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &result, None).await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -513,10 +509,11 @@ impl LoaderFieldEnum {
|
||||
{
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis
|
||||
.key()
|
||||
.metadata(LOADER_FIELD_ENUMS_ID_NAMESPACE, enum_name);
|
||||
|
||||
let cached_enum = redis
|
||||
.get_deserialized(LOADER_FIELD_ENUMS_ID_NAMESPACE, enum_name)
|
||||
.await?;
|
||||
let cached_enum = redis.get_deserialized(&key).await?;
|
||||
if let Some(cached_enum) = cached_enum {
|
||||
return Ok(cached_enum);
|
||||
}
|
||||
@@ -541,15 +538,11 @@ impl LoaderFieldEnum {
|
||||
});
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis
|
||||
.key()
|
||||
.metadata(LOADER_FIELD_ENUMS_ID_NAMESPACE, enum_name);
|
||||
|
||||
redis
|
||||
.set_serialized(
|
||||
LOADER_FIELD_ENUMS_ID_NAMESPACE,
|
||||
enum_name,
|
||||
&result,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &result, None).await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -652,7 +645,7 @@ impl LoaderFieldEnumValue {
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(values)
|
||||
Ok::<_, DatabaseError>(values)
|
||||
},
|
||||
).await?;
|
||||
|
||||
|
||||
@@ -72,25 +72,12 @@ pub enum DatabaseError {
|
||||
RandomId,
|
||||
#[error("Error while interacting with the cache: {0}")]
|
||||
CacheError(#[from] redis::RedisError),
|
||||
#[error("Redis Pool Error: {0}")]
|
||||
RedisPool(#[from] deadpool_redis::PoolError),
|
||||
#[error("Error while serializing with the cache: {0}")]
|
||||
SerdeCacheError(#[from] serde_json::Error),
|
||||
#[error("error while encoding or decoding the cache: {0}")]
|
||||
PostcardCacheError(#[from] postcard::Error),
|
||||
#[error(transparent)]
|
||||
Redis(#[from] xredis::Error),
|
||||
#[error("Schema error: {0}")]
|
||||
SchemaError(String),
|
||||
#[error(
|
||||
"Timeout waiting on Redis cache lock ({locks_released}/{locks_waiting} released, spent {time_spent_pool_wait_ms}ms/{time_spent_total_ms}ms waiting on connections from pool)"
|
||||
)]
|
||||
CacheTimeout {
|
||||
locks_released: usize,
|
||||
locks_waiting: usize,
|
||||
time_spent_pool_wait_ms: u64,
|
||||
time_spent_total_ms: u64,
|
||||
},
|
||||
#[error(
|
||||
"Timeout waiting on local cache lock ({released}/{total} released)"
|
||||
)]
|
||||
LocalCacheTimeout { released: usize, total: usize },
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@ use std::collections::HashMap;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::database::redis::RedisPool;
|
||||
use xredis::RedisPool;
|
||||
|
||||
use super::{DBOrganizationId, DBUserId, DatabaseError};
|
||||
|
||||
const MODERATION_NOTES_USERS_NAMESPACE: &str = "moderation_notes_users:v1";
|
||||
const MODERATION_NOTES_USERS_NAMESPACE: &str = "moderation_notes_users:v3";
|
||||
const MODERATION_NOTES_ORGANIZATIONS_NAMESPACE: &str =
|
||||
"moderation_notes_organizations:v1";
|
||||
"moderation_notes_organizations:v3";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DBModerationNote {
|
||||
@@ -32,19 +32,15 @@ impl DBModerationNote {
|
||||
where
|
||||
E: crate::database::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let ids = user_ids
|
||||
.iter()
|
||||
.map(|id| id.0.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let cached = {
|
||||
let mut redis = redis.connect().await?;
|
||||
redis
|
||||
.get_many_deserialized::<Self>(
|
||||
MODERATION_NOTES_USERS_NAMESPACE,
|
||||
&ids,
|
||||
)
|
||||
.await?
|
||||
let keys = user_ids
|
||||
.iter()
|
||||
.map(|id| {
|
||||
redis.key().entity(MODERATION_NOTES_USERS_NAMESPACE, id.0)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
redis.get_many_deserialized::<Self>(&keys).await?
|
||||
};
|
||||
|
||||
let mut notes = HashMap::new();
|
||||
@@ -86,14 +82,10 @@ impl DBModerationNote {
|
||||
};
|
||||
|
||||
if let Some(user_id) = note.user_id {
|
||||
redis
|
||||
.set_serialized(
|
||||
MODERATION_NOTES_USERS_NAMESPACE,
|
||||
user_id.0,
|
||||
¬e,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let key = redis
|
||||
.key()
|
||||
.entity(MODERATION_NOTES_USERS_NAMESPACE, user_id.0);
|
||||
redis.set_serialized(&key, ¬e, None).await?;
|
||||
notes.insert(user_id, note);
|
||||
}
|
||||
}
|
||||
@@ -122,19 +114,17 @@ impl DBModerationNote {
|
||||
where
|
||||
E: crate::database::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let ids = organization_ids
|
||||
.iter()
|
||||
.map(|id| id.0.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let cached = {
|
||||
let mut redis = redis.connect().await?;
|
||||
redis
|
||||
.get_many_deserialized::<Self>(
|
||||
MODERATION_NOTES_ORGANIZATIONS_NAMESPACE,
|
||||
&ids,
|
||||
)
|
||||
.await?
|
||||
let keys = organization_ids
|
||||
.iter()
|
||||
.map(|id| {
|
||||
redis
|
||||
.key()
|
||||
.entity(MODERATION_NOTES_ORGANIZATIONS_NAMESPACE, id.0)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
redis.get_many_deserialized::<Self>(&keys).await?
|
||||
};
|
||||
|
||||
let mut notes = HashMap::new();
|
||||
@@ -176,14 +166,11 @@ impl DBModerationNote {
|
||||
};
|
||||
|
||||
if let Some(organization_id) = note.organization_id {
|
||||
redis
|
||||
.set_serialized(
|
||||
MODERATION_NOTES_ORGANIZATIONS_NAMESPACE,
|
||||
organization_id.0,
|
||||
¬e,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let key = redis.key().entity(
|
||||
MODERATION_NOTES_ORGANIZATIONS_NAMESPACE,
|
||||
organization_id.0,
|
||||
);
|
||||
redis.set_serialized(&key, ¬e, None).await?;
|
||||
notes.insert(organization_id, note);
|
||||
}
|
||||
}
|
||||
@@ -289,9 +276,10 @@ impl DBModerationNote {
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
redis
|
||||
.delete(MODERATION_NOTES_USERS_NAMESPACE, user_id.0)
|
||||
.await
|
||||
let key = redis
|
||||
.key()
|
||||
.entity(MODERATION_NOTES_USERS_NAMESPACE, user_id.0);
|
||||
redis.delete(&key).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn clear_organization_cache(
|
||||
@@ -299,8 +287,10 @@ impl DBModerationNote {
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
redis
|
||||
.delete(MODERATION_NOTES_ORGANIZATIONS_NAMESPACE, organization_id.0)
|
||||
.await
|
||||
let key = redis.key().entity(
|
||||
MODERATION_NOTES_ORGANIZATIONS_NAMESPACE,
|
||||
organization_id.0,
|
||||
);
|
||||
redis.delete(&key).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::ids::*;
|
||||
use crate::database::PgTransaction;
|
||||
use crate::database::{models::DatabaseError, redis::RedisPool};
|
||||
use crate::database::models::DatabaseError;
|
||||
use crate::models::notifications::{
|
||||
NotificationBody, NotificationChannel, NotificationDeliveryStatus,
|
||||
NotificationType,
|
||||
@@ -8,8 +8,9 @@ use crate::models::notifications::{
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::TryStreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use xredis::RedisPool;
|
||||
|
||||
const USER_NOTIFICATIONS_NAMESPACE: &str = "user_notifications:v1";
|
||||
const USER_NOTIFICATIONS_NAMESPACE: &str = "user_notifications:v3";
|
||||
|
||||
pub struct NotificationBuilder {
|
||||
pub body: NotificationBody,
|
||||
@@ -433,13 +434,11 @@ impl DBNotification {
|
||||
{
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let key =
|
||||
redis.key().entity(USER_NOTIFICATIONS_NAMESPACE, user_id.0);
|
||||
|
||||
let cached_notifications: Option<Vec<DBNotification>> = redis
|
||||
.get_deserialized(
|
||||
USER_NOTIFICATIONS_NAMESPACE,
|
||||
&user_id.0.to_string(),
|
||||
)
|
||||
.await?;
|
||||
let cached_notifications: Option<Vec<DBNotification>> =
|
||||
redis.get_deserialized(&key).await?;
|
||||
|
||||
if let Some(notifications) = cached_notifications {
|
||||
return Ok(notifications);
|
||||
@@ -491,15 +490,9 @@ impl DBNotification {
|
||||
.await?;
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().entity(USER_NOTIFICATIONS_NAMESPACE, user_id.0);
|
||||
|
||||
redis
|
||||
.set_serialized(
|
||||
USER_NOTIFICATIONS_NAMESPACE,
|
||||
user_id.0,
|
||||
&db_notifications,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &db_notifications, None).await?;
|
||||
|
||||
Ok(db_notifications)
|
||||
}
|
||||
@@ -638,12 +631,12 @@ impl DBNotification {
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
let keys = user_ids
|
||||
.into_iter()
|
||||
.map(|id| redis.key().entity(USER_NOTIFICATIONS_NAMESPACE, id.0))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
redis
|
||||
.delete_many(user_ids.into_iter().map(|id| {
|
||||
(USER_NOTIFICATIONS_NAMESPACE, Some(id.0.to_string()))
|
||||
}))
|
||||
.await?;
|
||||
redis.delete_many(&keys).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use crate::database::models::DatabaseError;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::v3::notifications::{NotificationChannel, NotificationType};
|
||||
use crate::routes::ApiError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use xredis::RedisPool;
|
||||
|
||||
const TEMPLATES_NAMESPACE: &str = "notifications_templates:v1";
|
||||
const TEMPLATES_NAMESPACE: &str = "notifications_templates:v3";
|
||||
const TEMPLATES_HTML_DATA_NAMESPACE: &str =
|
||||
"notifications_templates_html_data:v1";
|
||||
"notifications_templates_html_data:v3";
|
||||
const TEMPLATES_DYNAMIC_HTML_NAMESPACE: &str =
|
||||
"notifications_templates_dynamic_html:v1";
|
||||
"notifications_templates_dynamic_html:v3";
|
||||
|
||||
const HTML_DATA_CACHE_EXPIRY: i64 = 60 * 15; // 15 minutes
|
||||
const TEMPLATES_CACHE_EXPIRY: i64 = 60 * 30; // 30 minutes
|
||||
@@ -55,10 +55,10 @@ impl NotificationTemplate {
|
||||
) -> Result<Vec<NotificationTemplate>, DatabaseError> {
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let key =
|
||||
redis.key().metadata(TEMPLATES_NAMESPACE, channel.as_str());
|
||||
|
||||
let maybe_cached_templates = redis
|
||||
.get_deserialized(TEMPLATES_NAMESPACE, channel.as_str())
|
||||
.await?;
|
||||
let maybe_cached_templates = redis.get_deserialized(&key).await?;
|
||||
|
||||
if let Some(cached) = maybe_cached_templates {
|
||||
return Ok(cached);
|
||||
@@ -78,14 +78,10 @@ impl NotificationTemplate {
|
||||
let templates = results.into_iter().map(Into::into).collect();
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(TEMPLATES_NAMESPACE, channel.as_str());
|
||||
|
||||
redis
|
||||
.set_serialized(
|
||||
TEMPLATES_NAMESPACE,
|
||||
channel.as_str(),
|
||||
&templates,
|
||||
Some(TEMPLATES_CACHE_EXPIRY),
|
||||
)
|
||||
.set_serialized(&key, &templates, Some(TEMPLATES_CACHE_EXPIRY))
|
||||
.await?;
|
||||
|
||||
Ok(templates)
|
||||
@@ -96,12 +92,8 @@ impl NotificationTemplate {
|
||||
redis: &RedisPool,
|
||||
) -> Result<Option<String>, DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
redis
|
||||
.get_deserialized(
|
||||
TEMPLATES_HTML_DATA_NAMESPACE,
|
||||
&self.id.to_string(),
|
||||
)
|
||||
.await
|
||||
let key = redis.key().metadata(TEMPLATES_HTML_DATA_NAMESPACE, self.id);
|
||||
redis.get_deserialized(&key).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn set_cached_html_data(
|
||||
@@ -110,14 +102,11 @@ impl NotificationTemplate {
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(TEMPLATES_HTML_DATA_NAMESPACE, self.id);
|
||||
redis
|
||||
.set_serialized(
|
||||
TEMPLATES_HTML_DATA_NAMESPACE,
|
||||
&self.id.to_string(),
|
||||
&data,
|
||||
Some(HTML_DATA_CACHE_EXPIRY),
|
||||
)
|
||||
.set_serialized(&key, &data, Some(HTML_DATA_CACHE_EXPIRY))
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,9 +124,11 @@ where
|
||||
}
|
||||
|
||||
let mut redis_conn = redis.connect().await?;
|
||||
if let Some(body) = redis_conn
|
||||
.get_deserialized::<HtmlBody>(TEMPLATES_DYNAMIC_HTML_NAMESPACE, key)
|
||||
.await?
|
||||
let redis_key = redis_conn
|
||||
.key()
|
||||
.metadata(TEMPLATES_DYNAMIC_HTML_NAMESPACE, key);
|
||||
if let Some(body) =
|
||||
redis_conn.get_deserialized::<HtmlBody>(&redis_key).await?
|
||||
{
|
||||
return Ok(body.html);
|
||||
}
|
||||
@@ -146,14 +137,12 @@ where
|
||||
|
||||
let cached = HtmlBody { html: get().await? };
|
||||
let mut redis_conn = redis.connect().await?;
|
||||
let redis_key = redis_conn
|
||||
.key()
|
||||
.metadata(TEMPLATES_DYNAMIC_HTML_NAMESPACE, key);
|
||||
|
||||
redis_conn
|
||||
.set_serialized(
|
||||
TEMPLATES_DYNAMIC_HTML_NAMESPACE,
|
||||
key,
|
||||
&cached,
|
||||
Some(HTML_DATA_CACHE_EXPIRY),
|
||||
)
|
||||
.set_serialized(&redis_key, &cached, Some(HTML_DATA_CACHE_EXPIRY))
|
||||
.await?;
|
||||
|
||||
Ok(cached.html)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::database::models::DatabaseError;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::v3::notifications::NotificationType;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use xredis::RedisPool;
|
||||
|
||||
const NOTIFICATION_TYPES_NAMESPACE: &str = "notification_types:v1";
|
||||
const NOTIFICATION_TYPES_NAMESPACE: &str = "notification_types:v3";
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct NotificationTypeItem {
|
||||
@@ -41,10 +41,9 @@ impl NotificationTypeItem {
|
||||
{
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(NOTIFICATION_TYPES_NAMESPACE, "all");
|
||||
|
||||
let cached_types = redis
|
||||
.get_deserialized(NOTIFICATION_TYPES_NAMESPACE, "all")
|
||||
.await?;
|
||||
let cached_types = redis.get_deserialized(&key).await?;
|
||||
|
||||
if let Some(types) = cached_types {
|
||||
return Ok(types);
|
||||
@@ -61,10 +60,9 @@ impl NotificationTypeItem {
|
||||
let types = results.into_iter().map(Into::into).collect();
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(NOTIFICATION_TYPES_NAMESPACE, "all");
|
||||
|
||||
redis
|
||||
.set_serialized(NOTIFICATION_TYPES_NAMESPACE, "all", &types, None)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &types, None).await?;
|
||||
|
||||
Ok(types)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
use crate::database::PgTransaction;
|
||||
use crate::database::redis::RedisPool;
|
||||
use ariadne::ids::base62_impl::parse_base62;
|
||||
use dashmap::DashMap;
|
||||
use futures::TryStreamExt;
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::hash::Hash;
|
||||
use xredis::RedisPool;
|
||||
|
||||
use super::{DBTeamMember, ids::*};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const ORGANIZATIONS_NAMESPACE: &str = "organizations:v1";
|
||||
const ORGANIZATIONS_TITLES_NAMESPACE: &str = "organizations_titles:v1";
|
||||
const ORGANIZATIONS_NAMESPACE: &str = "organizations:v3";
|
||||
const ORGANIZATIONS_TITLES_NAMESPACE: &str = "organizations_titles:v3";
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
/// An organization of users who together control one or more projects and organizations.
|
||||
@@ -159,7 +159,9 @@ impl DBOrganization {
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(organizations)
|
||||
Ok::<_, crate::database::models::DatabaseError>(
|
||||
organizations,
|
||||
)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -256,16 +258,17 @@ impl DBOrganization {
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), super::DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
redis
|
||||
.delete_many([
|
||||
(ORGANIZATIONS_NAMESPACE, Some(id.0.to_string())),
|
||||
(
|
||||
let mut keys = vec![redis.key().entity(ORGANIZATIONS_NAMESPACE, id.0)];
|
||||
if let Some(slug) = slug {
|
||||
keys.push(
|
||||
redis.key().entity(
|
||||
ORGANIZATIONS_TITLES_NAMESPACE,
|
||||
slug.map(|x| x.to_lowercase()),
|
||||
slug.to_lowercase(),
|
||||
),
|
||||
])
|
||||
.await?;
|
||||
);
|
||||
}
|
||||
|
||||
redis.delete_many(&keys).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use super::ids::*;
|
||||
use crate::database::PgTransaction;
|
||||
use crate::database::models::DatabaseError;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::pats::Scopes;
|
||||
use ariadne::ids::base62_impl::parse_base62;
|
||||
use chrono::{DateTime, Utc};
|
||||
@@ -10,10 +9,11 @@ use futures::TryStreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::hash::Hash;
|
||||
use xredis::RedisPool;
|
||||
|
||||
const PATS_NAMESPACE: &str = "pats:v1";
|
||||
const PATS_TOKENS_NAMESPACE: &str = "pats_tokens:v1";
|
||||
const PATS_USERS_NAMESPACE: &str = "pats_users:v1";
|
||||
const PATS_NAMESPACE: &str = "pats:v3";
|
||||
const PATS_TOKENS_NAMESPACE: &str = "pats_tokens:v3";
|
||||
const PATS_USERS_NAMESPACE: &str = "pats_users:v3";
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct DBPersonalAccessToken {
|
||||
@@ -141,7 +141,7 @@ impl DBPersonalAccessToken {
|
||||
async move { Ok(acc) }
|
||||
})
|
||||
.await?;
|
||||
Ok(pats)
|
||||
Ok::<_, DatabaseError>(pats)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -159,13 +159,9 @@ impl DBPersonalAccessToken {
|
||||
{
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().entity(PATS_USERS_NAMESPACE, user_id.0);
|
||||
|
||||
let res = redis
|
||||
.get_deserialized::<Vec<i64>>(
|
||||
PATS_USERS_NAMESPACE,
|
||||
&user_id.0.to_string(),
|
||||
)
|
||||
.await?;
|
||||
let res = redis.get_deserialized::<Vec<i64>>(&key).await?;
|
||||
|
||||
if let Some(res) = res {
|
||||
return Ok(res.into_iter().map(DBPatId).collect());
|
||||
@@ -187,10 +183,9 @@ impl DBPersonalAccessToken {
|
||||
.await?;
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().entity(PATS_USERS_NAMESPACE, user_id.0);
|
||||
|
||||
redis
|
||||
.set_serialized(PATS_USERS_NAMESPACE, user_id.0, &db_pats, None)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &db_pats, None).await?;
|
||||
Ok(db_pats)
|
||||
}
|
||||
|
||||
@@ -204,20 +199,23 @@ impl DBPersonalAccessToken {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
redis
|
||||
.delete_many(clear_pats.into_iter().flat_map(
|
||||
|(id, token, user_id)| {
|
||||
[
|
||||
(PATS_NAMESPACE, id.map(|i| i.0.to_string())),
|
||||
(PATS_TOKENS_NAMESPACE, token),
|
||||
(
|
||||
PATS_USERS_NAMESPACE,
|
||||
user_id.map(|i| i.0.to_string()),
|
||||
),
|
||||
]
|
||||
},
|
||||
))
|
||||
.await?;
|
||||
let keys = clear_pats
|
||||
.into_iter()
|
||||
.flat_map(|(id, token, user_id)| {
|
||||
[
|
||||
id.map(|id| redis.key().entity(PATS_NAMESPACE, id.0)),
|
||||
token.map(|token| {
|
||||
redis.key().entity(PATS_TOKENS_NAMESPACE, token)
|
||||
}),
|
||||
user_id.map(|user_id| {
|
||||
redis.key().entity(PATS_USERS_NAMESPACE, user_id.0)
|
||||
}),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
redis.delete_many(&keys).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use crate::database::models::{
|
||||
DBProductId, DBProductPriceId, DatabaseError, product_item,
|
||||
};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::billing::{Price, ProductMetadata};
|
||||
use dashmap::DashMap;
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::TryFrom;
|
||||
use std::convert::TryInto;
|
||||
use xredis::RedisPool;
|
||||
|
||||
const PRODUCTS_NAMESPACE: &str = "products:v1";
|
||||
const PRODUCTS_NAMESPACE: &str = "products:v3";
|
||||
|
||||
pub struct DBProduct {
|
||||
pub id: DBProductId,
|
||||
@@ -152,9 +152,10 @@ impl QueryProductWithPrices {
|
||||
{
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(PRODUCTS_NAMESPACE, "all");
|
||||
|
||||
let res: Option<Vec<QueryProductWithPrices>> =
|
||||
redis.get_deserialized(PRODUCTS_NAMESPACE, "all").await?;
|
||||
redis.get_deserialized(&key).await?;
|
||||
|
||||
if let Some(res) = res {
|
||||
return Ok(res);
|
||||
@@ -193,10 +194,9 @@ impl QueryProductWithPrices {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(PRODUCTS_NAMESPACE, "all");
|
||||
|
||||
redis
|
||||
.set_serialized(PRODUCTS_NAMESPACE, "all", &products, None)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &products, None).await?;
|
||||
|
||||
Ok(products)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ use super::loader_fields::{
|
||||
};
|
||||
use super::{DBUser, ids::*};
|
||||
use crate::database::models::DatabaseError;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::database::{PgTransaction, models};
|
||||
use crate::file_hosting::FileHost;
|
||||
use crate::models::exp;
|
||||
@@ -22,10 +21,11 @@ use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::hash::Hash;
|
||||
use xredis::RedisPool;
|
||||
|
||||
pub const PROJECTS_NAMESPACE: &str = "projects:v1";
|
||||
pub const PROJECTS_SLUGS_NAMESPACE: &str = "projects_slugs:v1";
|
||||
const PROJECTS_DEPENDENCIES_NAMESPACE: &str = "projects_dependencies:v1";
|
||||
pub const PROJECTS_NAMESPACE: &str = "projects:v3";
|
||||
pub const PROJECTS_SLUGS_NAMESPACE: &str = "projects_slugs:v3";
|
||||
const PROJECTS_DEPENDENCIES_NAMESPACE: &str = "projects_dependencies:v3";
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct LinkUrl {
|
||||
@@ -942,7 +942,7 @@ impl DBProject {
|
||||
})
|
||||
?;
|
||||
|
||||
Ok(projects)
|
||||
Ok::<_, DatabaseError>(projects)
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -974,13 +974,10 @@ impl DBProject {
|
||||
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().entity(PROJECTS_DEPENDENCIES_NAMESPACE, id.0);
|
||||
|
||||
let dependencies = redis
|
||||
.get_deserialized::<Dependencies>(
|
||||
PROJECTS_DEPENDENCIES_NAMESPACE,
|
||||
&id.0.to_string(),
|
||||
)
|
||||
.await?;
|
||||
let dependencies =
|
||||
redis.get_deserialized::<Dependencies>(&key).await?;
|
||||
if let Some(dependencies) = dependencies {
|
||||
return Ok(dependencies);
|
||||
}
|
||||
@@ -1012,15 +1009,9 @@ impl DBProject {
|
||||
.await?;
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().entity(PROJECTS_DEPENDENCIES_NAMESPACE, id.0);
|
||||
|
||||
redis
|
||||
.set_serialized(
|
||||
PROJECTS_DEPENDENCIES_NAMESPACE,
|
||||
id.0,
|
||||
&dependencies,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &dependencies, None).await?;
|
||||
Ok(dependencies)
|
||||
}
|
||||
|
||||
@@ -1031,21 +1022,21 @@ impl DBProject {
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
let mut keys = vec![redis.key().entity(PROJECTS_NAMESPACE, id.0)];
|
||||
if let Some(slug) = slug {
|
||||
keys.push(
|
||||
redis
|
||||
.key()
|
||||
.entity(PROJECTS_SLUGS_NAMESPACE, slug.to_lowercase()),
|
||||
);
|
||||
}
|
||||
if clear_dependencies.unwrap_or(false) {
|
||||
keys.push(
|
||||
redis.key().entity(PROJECTS_DEPENDENCIES_NAMESPACE, id.0),
|
||||
);
|
||||
}
|
||||
|
||||
redis
|
||||
.delete_many([
|
||||
(PROJECTS_NAMESPACE, Some(id.0.to_string())),
|
||||
(PROJECTS_SLUGS_NAMESPACE, slug.map(|x| x.to_lowercase())),
|
||||
(
|
||||
PROJECTS_DEPENDENCIES_NAMESPACE,
|
||||
if clear_dependencies.unwrap_or(false) {
|
||||
Some(id.0.to_string())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
),
|
||||
])
|
||||
.await?;
|
||||
redis.delete_many(&keys).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use super::ids::*;
|
||||
use crate::database::PgTransaction;
|
||||
use crate::database::models::DatabaseError;
|
||||
use crate::database::redis::RedisPool;
|
||||
use ariadne::ids::base62_impl::parse_base62;
|
||||
use chrono::{DateTime, Utc};
|
||||
use dashmap::DashMap;
|
||||
@@ -9,10 +8,11 @@ use futures_util::TryStreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::hash::Hash;
|
||||
use xredis::RedisPool;
|
||||
|
||||
const SESSIONS_NAMESPACE: &str = "sessions:v1";
|
||||
const SESSIONS_IDS_NAMESPACE: &str = "sessions_ids:v1";
|
||||
const SESSIONS_USERS_NAMESPACE: &str = "sessions_users:v1";
|
||||
const SESSIONS_NAMESPACE: &str = "sessions:v3";
|
||||
const SESSIONS_IDS_NAMESPACE: &str = "sessions_ids:v3";
|
||||
const SESSIONS_USERS_NAMESPACE: &str = "sessions_users:v3";
|
||||
|
||||
pub struct SessionBuilder {
|
||||
pub session: String,
|
||||
@@ -208,7 +208,7 @@ impl DBSession {
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(db_sessions)
|
||||
Ok::<_, DatabaseError>(db_sessions)
|
||||
}).await?;
|
||||
|
||||
Ok(val)
|
||||
@@ -224,13 +224,9 @@ impl DBSession {
|
||||
{
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().entity(SESSIONS_USERS_NAMESPACE, user_id.0);
|
||||
|
||||
let res = redis
|
||||
.get_deserialized::<Vec<i64>>(
|
||||
SESSIONS_USERS_NAMESPACE,
|
||||
&user_id.0.to_string(),
|
||||
)
|
||||
.await?;
|
||||
let res = redis.get_deserialized::<Vec<i64>>(&key).await?;
|
||||
|
||||
if let Some(res) = res {
|
||||
return Ok(res.into_iter().map(DBSessionId).collect());
|
||||
@@ -253,15 +249,9 @@ impl DBSession {
|
||||
.await?;
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().entity(SESSIONS_USERS_NAMESPACE, user_id.0);
|
||||
|
||||
redis
|
||||
.set_serialized(
|
||||
SESSIONS_USERS_NAMESPACE,
|
||||
user_id.0,
|
||||
&db_sessions,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &db_sessions, None).await?;
|
||||
|
||||
Ok(db_sessions)
|
||||
}
|
||||
@@ -280,20 +270,23 @@ impl DBSession {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
redis
|
||||
.delete_many(clear_sessions.into_iter().flat_map(
|
||||
|(id, session, user_id)| {
|
||||
[
|
||||
(SESSIONS_NAMESPACE, id.map(|i| i.0.to_string())),
|
||||
(SESSIONS_IDS_NAMESPACE, session),
|
||||
(
|
||||
SESSIONS_USERS_NAMESPACE,
|
||||
user_id.map(|i| i.0.to_string()),
|
||||
),
|
||||
]
|
||||
},
|
||||
))
|
||||
.await?;
|
||||
let keys = clear_sessions
|
||||
.into_iter()
|
||||
.flat_map(|(id, session, user_id)| {
|
||||
[
|
||||
id.map(|id| redis.key().entity(SESSIONS_NAMESPACE, id.0)),
|
||||
session.map(|session| {
|
||||
redis.key().entity(SESSIONS_IDS_NAMESPACE, session)
|
||||
}),
|
||||
user_id.map(|user_id| {
|
||||
redis.key().entity(SESSIONS_USERS_NAMESPACE, user_id.0)
|
||||
}),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
redis.delete_many(&keys).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::{DBOrganization, DBProject, ids::*};
|
||||
use crate::{
|
||||
database::{PgTransaction, redis::RedisPool},
|
||||
database::PgTransaction,
|
||||
models::teams::{OrganizationPermissions, ProjectPermissions},
|
||||
};
|
||||
use dashmap::DashMap;
|
||||
@@ -8,8 +8,9 @@ use futures::TryStreamExt;
|
||||
use itertools::Itertools;
|
||||
use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use xredis::RedisPool;
|
||||
|
||||
const TEAMS_NAMESPACE: &str = "teams:v1";
|
||||
const TEAMS_NAMESPACE: &str = "teams:v3";
|
||||
|
||||
pub struct TeamBuilder {
|
||||
pub members: Vec<TeamMemberBuilder>,
|
||||
@@ -253,7 +254,7 @@ impl DBTeamMember {
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(teams)
|
||||
Ok::<_, crate::database::models::DatabaseError>(teams)
|
||||
},
|
||||
).await?;
|
||||
|
||||
@@ -265,7 +266,8 @@ impl DBTeamMember {
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), super::DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
redis.delete(TEAMS_NAMESPACE, id.0).await?;
|
||||
let key = redis.key().entity(TEAMS_NAMESPACE, id.0);
|
||||
redis.delete(&key).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ use super::{DBCollectionId, DBReportId, DBThreadId};
|
||||
use crate::database::models::charge_item::DBCharge;
|
||||
use crate::database::models::user_subscription_item::DBUserSubscription;
|
||||
use crate::database::models::{DBOrganizationId, DatabaseError};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::database::{PgTransaction, models};
|
||||
use crate::models::billing::ChargeStatus;
|
||||
use crate::models::users::Badges;
|
||||
@@ -15,10 +14,11 @@ use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::hash::Hash;
|
||||
use xredis::RedisPool;
|
||||
|
||||
const USERS_NAMESPACE: &str = "users:v1";
|
||||
const USER_USERNAMES_NAMESPACE: &str = "users_usernames:v1";
|
||||
const USERS_PROJECTS_NAMESPACE: &str = "users_projects:v1";
|
||||
const USERS_NAMESPACE: &str = "users:v3";
|
||||
const USER_USERNAMES_NAMESPACE: &str = "users_usernames:v3";
|
||||
const USERS_PROJECTS_NAMESPACE: &str = "users_projects:v3";
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct DBUser {
|
||||
@@ -273,7 +273,7 @@ impl DBUser {
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(users)
|
||||
Ok::<_, DatabaseError>(users)
|
||||
}).await?;
|
||||
Ok(val)
|
||||
}
|
||||
@@ -389,13 +389,10 @@ impl DBUser {
|
||||
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().entity(USERS_PROJECTS_NAMESPACE, user_id.0);
|
||||
|
||||
let cached_projects = redis
|
||||
.get_deserialized::<Vec<DBProjectId>>(
|
||||
USERS_PROJECTS_NAMESPACE,
|
||||
&user_id.0.to_string(),
|
||||
)
|
||||
.await?;
|
||||
let cached_projects =
|
||||
redis.get_deserialized::<Vec<DBProjectId>>(&key).await?;
|
||||
|
||||
if let Some(projects) = cached_projects {
|
||||
return Ok(projects);
|
||||
@@ -417,15 +414,9 @@ impl DBUser {
|
||||
.await?;
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().entity(USERS_PROJECTS_NAMESPACE, user_id.0);
|
||||
|
||||
redis
|
||||
.set_serialized(
|
||||
USERS_PROJECTS_NAMESPACE,
|
||||
user_id.0,
|
||||
&db_projects,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &db_projects, None).await?;
|
||||
|
||||
Ok(db_projects)
|
||||
}
|
||||
@@ -556,18 +547,24 @@ impl DBUser {
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
redis
|
||||
.delete_many(user_ids.iter().flat_map(|(id, username)| {
|
||||
let keys = user_ids
|
||||
.iter()
|
||||
.flat_map(|(id, username)| {
|
||||
[
|
||||
(USERS_NAMESPACE, Some(id.0.to_string())),
|
||||
(
|
||||
USER_USERNAMES_NAMESPACE,
|
||||
username.clone().map(|i| i.to_lowercase()),
|
||||
),
|
||||
Some(redis.key().entity(USERS_NAMESPACE, id.0)),
|
||||
username.as_ref().map(|username| {
|
||||
redis.key().entity(
|
||||
USER_USERNAMES_NAMESPACE,
|
||||
username.to_lowercase(),
|
||||
)
|
||||
}),
|
||||
]
|
||||
}))
|
||||
.await?;
|
||||
.into_iter()
|
||||
.flatten()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
redis.delete_many(&keys).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -576,14 +573,12 @@ impl DBUser {
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
let keys = user_ids
|
||||
.iter()
|
||||
.map(|id| redis.key().entity(USERS_PROJECTS_NAMESPACE, id.0))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
redis
|
||||
.delete_many(
|
||||
user_ids.iter().map(|id| {
|
||||
(USERS_PROJECTS_NAMESPACE, Some(id.0.to_string()))
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
redis.delete_many(&keys).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ use crate::database::PgTransaction;
|
||||
use crate::database::models::loader_fields::{
|
||||
QueryLoaderField, QueryLoaderFieldEnumValue, QueryVersionField,
|
||||
};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::file_hosting::FileHost;
|
||||
use crate::models::exp;
|
||||
use xredis::RedisPool;
|
||||
|
||||
use crate::models::projects::{FileType, VersionStatus};
|
||||
use crate::queue::file_scan::scan_file;
|
||||
@@ -19,11 +19,10 @@ use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
use std::iter;
|
||||
use tracing::error;
|
||||
|
||||
pub const VERSIONS_NAMESPACE: &str = "versions:v1";
|
||||
const VERSION_FILES_NAMESPACE: &str = "versions_files:v1";
|
||||
pub const VERSIONS_NAMESPACE: &str = "versions:v3";
|
||||
const VERSION_FILES_NAMESPACE: &str = "versions_files:v3";
|
||||
|
||||
pub async fn cleanup_unused_attribution_files_and_groups(
|
||||
transaction: &mut PgTransaction<'_>,
|
||||
@@ -948,7 +947,7 @@ impl DBVersion {
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(res)
|
||||
Ok::<_, DatabaseError>(res)
|
||||
},
|
||||
).await?;
|
||||
|
||||
@@ -1039,7 +1038,7 @@ impl DBVersion {
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(files)
|
||||
Ok::<_, DatabaseError>(files)
|
||||
}
|
||||
).await?;
|
||||
|
||||
@@ -1051,25 +1050,18 @@ impl DBVersion {
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
let mut keys =
|
||||
vec![redis.key().entity(VERSIONS_NAMESPACE, version.inner.id.0)];
|
||||
keys.extend(version.files.iter().flat_map(|file| {
|
||||
file.hashes.iter().map(|(algorithm, hash)| {
|
||||
redis.key().entity(
|
||||
VERSION_FILES_NAMESPACE,
|
||||
format!("{algorithm}_{hash}"),
|
||||
)
|
||||
})
|
||||
}));
|
||||
|
||||
redis
|
||||
.delete_many(
|
||||
iter::once((
|
||||
VERSIONS_NAMESPACE,
|
||||
Some(version.inner.id.0.to_string()),
|
||||
))
|
||||
.chain(version.files.iter().flat_map(
|
||||
|file| {
|
||||
file.hashes.iter().map(|(algo, hash)| {
|
||||
(
|
||||
VERSION_FILES_NAMESPACE,
|
||||
Some(format!("{algo}_{hash}")),
|
||||
)
|
||||
})
|
||||
},
|
||||
)),
|
||||
)
|
||||
.await?;
|
||||
redis.delete_many(&keys).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1078,14 +1070,12 @@ impl DBVersion {
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
let keys = version_ids
|
||||
.iter()
|
||||
.map(|id| redis.key().entity(VERSIONS_NAMESPACE, id.0))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
redis
|
||||
.delete_many(
|
||||
version_ids
|
||||
.iter()
|
||||
.map(|id| (VERSIONS_NAMESPACE, Some(id.0.to_string()))),
|
||||
)
|
||||
.await?;
|
||||
redis.delete_many(&keys).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::env::ENV;
|
||||
|
||||
struct RedisConfig {
|
||||
inner: xredis::RedisConfig,
|
||||
cache_settings: xredis::CacheSettings,
|
||||
}
|
||||
|
||||
impl RedisConfig {
|
||||
fn from_env() -> Result<Self, xredis::RedisConfigError> {
|
||||
let inner = xredis::RedisConfig::new(
|
||||
ENV.REDIS_TOPOLOGY,
|
||||
ENV.REDIS_CONNECTION_TYPE,
|
||||
&ENV.REDIS_URL,
|
||||
ENV.REDIS_WAIT_TIMEOUT_MS,
|
||||
(
|
||||
ENV.REDIS_MAX_CONNECTIONS as usize,
|
||||
ENV.REDIS_MIN_CONNECTIONS,
|
||||
),
|
||||
(
|
||||
ENV.REDIS_CLUSTER_MAX_CONNECTIONS as usize,
|
||||
ENV.REDIS_CLUSTER_MIN_CONNECTIONS,
|
||||
),
|
||||
(ENV.REDIS_BLOCKING_MAX_CONNECTIONS as usize, 0),
|
||||
ENV.REDIS_CACHE_LOCKING_STRATEGY,
|
||||
ENV.REDIS_READ_REPLICA_STRATEGY,
|
||||
)?;
|
||||
let cache_settings = xredis::CacheSettings {
|
||||
default_expiry: ENV.REDIS_DEFAULT_EXPIRY,
|
||||
actual_expiry: ENV.REDIS_ACTUAL_EXPIRY,
|
||||
version_default_expiry: ENV.REDIS_VERSION_DEFAULT_EXPIRY,
|
||||
version_actual_expiry: ENV.REDIS_VERSION_ACTUAL_EXPIRY,
|
||||
encoding_format: ENV.REDIS_ENCODING_FORMAT,
|
||||
compression_algorithm: ENV.REDIS_COMPRESSION_ALGORITHM,
|
||||
compression_level: ENV.REDIS_COMPRESSION_LEVEL,
|
||||
compression_threshold_bytes: ENV.REDIS_COMPRESSION_THRESHOLD_BYTES,
|
||||
compression_min_savings_ratio: ENV
|
||||
.REDIS_COMPRESSION_MIN_SAVINGS_RATIO,
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
inner,
|
||||
cache_settings,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_env(
|
||||
meta_namespace: impl Into<Arc<str>>,
|
||||
) -> xredis::RedisPool {
|
||||
let config = RedisConfig::from_env().expect("invalid Redis configuration");
|
||||
xredis::RedisPool::new(meta_namespace, config.inner, config.cache_settings)
|
||||
.await
|
||||
.expect("failed to initialize Redis connections")
|
||||
}
|
||||
@@ -1,957 +0,0 @@
|
||||
use crate::env::ENV;
|
||||
|
||||
use super::models::DatabaseError;
|
||||
use ariadne::ids::base62_impl::{parse_base62, to_base62};
|
||||
use chrono::{TimeZone, Utc};
|
||||
use dashmap::DashMap;
|
||||
use deadpool_redis::{Config, Runtime};
|
||||
use futures::TryStreamExt;
|
||||
use futures::future::Either;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use prometheus::{IntGauge, Registry};
|
||||
use redis::ToRedisArgs;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
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 std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use tracing::{Instrument, info, info_span};
|
||||
use util::{cmd, redis_pipe};
|
||||
|
||||
pub mod util;
|
||||
|
||||
// Bound how many commands we send in a single Redis pipeline. The multiplexed
|
||||
// connection's BytesMut write buffer keeps its peak capacity for the life of
|
||||
// the connection, so larger pipelines cause higher steady-state RSS.
|
||||
const PIPELINE_CHUNK_SIZE: usize = 25;
|
||||
// Bound how many keys we send in a single MGET. Each MGET response must fit
|
||||
// into the connection's read buffer, which also retains its peak capacity. At
|
||||
// ~1 MB per cached value, 32 keys caps any single response at ~32 MB.
|
||||
const MGET_CHUNK_SIZE: usize = 32;
|
||||
// How long a pooled Redis connection lives before being recycled, regardless
|
||||
// of activity. Forced recycling is the only way to release the per-connection
|
||||
// BytesMut peak capacity that builds up under steady load.
|
||||
const REDIS_MAX_CONN_AGE: Duration = Duration::from_secs(120);
|
||||
|
||||
#[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,
|
||||
Postcard,
|
||||
}
|
||||
|
||||
#[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),
|
||||
"postcard" => Ok(Self::Postcard),
|
||||
_ => Err(InvalidEncodingFormat),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_value<T: Serialize>(value: &T) -> Result<Vec<u8>, DatabaseError> {
|
||||
let mut value = match ENV.REDIS_ENCODING_FORMAT {
|
||||
EncodingFormat::Json => serde_json::to_vec(value)?,
|
||||
EncodingFormat::Postcard => postcard::to_allocvec(value)?,
|
||||
};
|
||||
|
||||
if ENV.REDIS_COMPRESSION_LEVEL > 0
|
||||
&& ENV.REDIS_COMPRESSION_ALGORITHM == Codec::Lz4
|
||||
&& value.len() >= ENV.REDIS_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 >= ENV.REDIS_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)
|
||||
}
|
||||
|
||||
fn decode_value<T>(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 ENV.REDIS_ENCODING_FORMAT {
|
||||
EncodingFormat::Json => serde_json::from_slice(&value).ok(),
|
||||
EncodingFormat::Postcard => postcard::from_bytes(&value).ok(),
|
||||
}
|
||||
}
|
||||
|
||||
fn cache_expiries(namespace: &str) -> (i64, i64) {
|
||||
// Namespaces may embed a version suffix like `:v1`, so split it out.
|
||||
match namespace.split_once(':').map(|t| t.0).unwrap_or(namespace) {
|
||||
"versions" | "versions_files" => (
|
||||
ENV.REDIS_VERSION_DEFAULT_EXPIRY,
|
||||
ENV.REDIS_VERSION_ACTUAL_EXPIRY,
|
||||
),
|
||||
_ => (ENV.REDIS_DEFAULT_EXPIRY, ENV.REDIS_ACTUAL_EXPIRY),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RedisPool {
|
||||
pub url: String,
|
||||
pub pool: deadpool_redis::Pool,
|
||||
cache_list: Arc<DashMap<String, util::CacheSubscriber>>,
|
||||
meta_namespace: Arc<str>,
|
||||
}
|
||||
|
||||
pub struct RedisConnection {
|
||||
pub connection: deadpool_redis::Connection,
|
||||
meta_namespace: Arc<str>,
|
||||
}
|
||||
|
||||
impl RedisPool {
|
||||
// initiate a new redis pool
|
||||
// testing pool uses a hashmap to mimic redis behaviour for very small data sizes (ie: tests)
|
||||
// PANICS: production pool will panic if redis url is not set
|
||||
pub fn new(meta_namespace: impl Into<Arc<str>>) -> Self {
|
||||
let wait_timeout = Duration::from_millis(ENV.REDIS_WAIT_TIMEOUT_MS);
|
||||
|
||||
let url = &ENV.REDIS_URL;
|
||||
let pool = Config::from_url(url.clone())
|
||||
.builder()
|
||||
.expect("Error building Redis pool")
|
||||
.max_size(ENV.REDIS_MAX_CONNECTIONS as usize)
|
||||
.wait_timeout(Some(wait_timeout))
|
||||
.runtime(Runtime::Tokio1)
|
||||
.build()
|
||||
.expect("Redis connection failed");
|
||||
|
||||
let pool = RedisPool {
|
||||
url: url.clone(),
|
||||
pool,
|
||||
cache_list: Arc::new(DashMap::with_capacity(2048)),
|
||||
meta_namespace: meta_namespace.into(),
|
||||
};
|
||||
|
||||
let redis_min_connections = ENV.REDIS_MIN_CONNECTIONS;
|
||||
let spawn_min_connections = (0..redis_min_connections)
|
||||
.map(|_| {
|
||||
let pool = pool.clone();
|
||||
tokio::spawn(async move { pool.pool.get().await })
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>();
|
||||
tokio::spawn({
|
||||
let pool = pool.clone();
|
||||
async move {
|
||||
// collect the connections into a buffer while we're spawning them,
|
||||
// to make sure that we're not `get`ing any connections we previously took
|
||||
let _connections =
|
||||
spawn_min_connections.try_collect::<Vec<_>>().await;
|
||||
info!(
|
||||
pool_status = ?pool.pool.status(),
|
||||
"Finished getting {redis_min_connections} initial Redis connections"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let interval = Duration::from_secs(30);
|
||||
let max_idle = Duration::from_secs(5 * 60); // 5 minutes
|
||||
let pool_ref = pool.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(interval).await;
|
||||
pool_ref.pool.retain(|_, metrics| {
|
||||
// Drop connections that have been idle too long, OR that
|
||||
// are older than REDIS_MAX_CONN_AGE regardless of use.
|
||||
// The age-based recycle is what releases the per-connection
|
||||
// BytesMut peak capacity under steady traffic.
|
||||
metrics.last_used() < max_idle
|
||||
&& metrics.created.elapsed() < REDIS_MAX_CONN_AGE
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
pool
|
||||
}
|
||||
|
||||
pub async fn register_and_set_metrics(
|
||||
&self,
|
||||
registry: &Registry,
|
||||
) -> Result<(), prometheus::Error> {
|
||||
let redis_max_size = IntGauge::new(
|
||||
"labrinth_redis_pool_max_size",
|
||||
"Maximum size of Redis pool",
|
||||
)?;
|
||||
let redis_size = IntGauge::new(
|
||||
"labrinth_redis_pool_size",
|
||||
"Current size of Redis pool",
|
||||
)?;
|
||||
let redis_available = IntGauge::new(
|
||||
"labrinth_redis_pool_available",
|
||||
"Available connections in Redis pool",
|
||||
)?;
|
||||
let redis_waiting = IntGauge::new(
|
||||
"labrinth_redis_pool_waiting",
|
||||
"Number of futures waiting for a Redis connection",
|
||||
)?;
|
||||
|
||||
registry.register(Box::new(redis_max_size.clone()))?;
|
||||
registry.register(Box::new(redis_size.clone()))?;
|
||||
registry.register(Box::new(redis_available.clone()))?;
|
||||
registry.register(Box::new(redis_waiting.clone()))?;
|
||||
|
||||
let redis_pool_ref = self.pool.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let status = redis_pool_ref.status();
|
||||
redis_max_size.set(status.max_size as i64);
|
||||
redis_size.set(status.size as i64);
|
||||
redis_available.set(status.available as i64);
|
||||
redis_waiting.set(status.waiting as i64);
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn connect(&self) -> Result<RedisConnection, DatabaseError> {
|
||||
Ok(RedisConnection {
|
||||
connection: self.pool.get().await?,
|
||||
meta_namespace: self.meta_namespace.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, closure))]
|
||||
pub async fn get_cached_keys<F, Fut, T, K>(
|
||||
&self,
|
||||
namespace: &str,
|
||||
keys: &[K],
|
||||
closure: F,
|
||||
) -> Result<Vec<T>, DatabaseError>
|
||||
where
|
||||
F: FnOnce(Vec<K>) -> Fut,
|
||||
Fut: Future<Output = Result<DashMap<K, T>, DatabaseError>>,
|
||||
T: Serialize + DeserializeOwned,
|
||||
K: Display
|
||||
+ Hash
|
||||
+ Eq
|
||||
+ PartialEq
|
||||
+ Clone
|
||||
+ DeserializeOwned
|
||||
+ Serialize
|
||||
+ Debug,
|
||||
{
|
||||
Ok(self
|
||||
.get_cached_keys_raw(namespace, keys, closure)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| x.1)
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, closure))]
|
||||
pub async fn get_cached_keys_raw<F, Fut, T, K>(
|
||||
&self,
|
||||
namespace: &str,
|
||||
keys: &[K],
|
||||
closure: F,
|
||||
) -> Result<HashMap<K, T>, DatabaseError>
|
||||
where
|
||||
F: FnOnce(Vec<K>) -> Fut,
|
||||
Fut: Future<Output = Result<DashMap<K, T>, DatabaseError>>,
|
||||
T: Serialize + DeserializeOwned,
|
||||
K: Display
|
||||
+ Hash
|
||||
+ Eq
|
||||
+ PartialEq
|
||||
+ Clone
|
||||
+ DeserializeOwned
|
||||
+ Serialize
|
||||
+ Debug,
|
||||
{
|
||||
self.get_cached_keys_raw_with_slug(
|
||||
namespace,
|
||||
None,
|
||||
false,
|
||||
keys,
|
||||
|ids| async move {
|
||||
Ok(closure(ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|(key, val)| (key, (None::<String>, val)))
|
||||
.collect())
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, closure))]
|
||||
pub async fn get_cached_keys_with_slug<F, Fut, T, I, K, S>(
|
||||
&self,
|
||||
namespace: &str,
|
||||
slug_namespace: &str,
|
||||
case_sensitive: bool,
|
||||
keys: &[I],
|
||||
closure: F,
|
||||
) -> Result<Vec<T>, DatabaseError>
|
||||
where
|
||||
F: FnOnce(Vec<I>) -> Fut,
|
||||
Fut: Future<Output = Result<DashMap<K, (Option<S>, T)>, DatabaseError>>,
|
||||
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(
|
||||
namespace,
|
||||
Some(slug_namespace),
|
||||
case_sensitive,
|
||||
keys,
|
||||
closure,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| x.1)
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, closure))]
|
||||
pub async fn get_cached_keys_raw_with_slug<F, Fut, T, I, K, S>(
|
||||
&self,
|
||||
namespace: &str,
|
||||
slug_namespace: Option<&str>,
|
||||
case_sensitive: bool,
|
||||
keys: &[I],
|
||||
closure: F,
|
||||
) -> Result<HashMap<K, T>, DatabaseError>
|
||||
where
|
||||
F: FnOnce(Vec<I>) -> Fut,
|
||||
Fut: Future<Output = Result<DashMap<K, (Option<S>, T)>, DatabaseError>>,
|
||||
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(|x| (x.to_string(), x.clone()))
|
||||
.collect::<DashMap<String, I>>();
|
||||
|
||||
if ids.is_empty() {
|
||||
return Ok(HashMap::new());
|
||||
}
|
||||
|
||||
let get_cached_values = |ids: DashMap<String, I>| {
|
||||
async move {
|
||||
let slug_ids = if let Some(slug_namespace) = slug_namespace {
|
||||
async {
|
||||
let mut connection = self.pool.get().await?;
|
||||
|
||||
let args = ids
|
||||
.iter()
|
||||
.map(|x| {
|
||||
format!(
|
||||
"{}_{slug_namespace}:{}",
|
||||
self.meta_namespace,
|
||||
if case_sensitive {
|
||||
x.value().to_string()
|
||||
} else {
|
||||
x.value().to_string().to_lowercase()
|
||||
}
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut v = Vec::new();
|
||||
for chunk in args.chunks(MGET_CHUNK_SIZE) {
|
||||
let part = cmd("MGET")
|
||||
.arg(chunk)
|
||||
.query_async::<Vec<Option<String>>>(
|
||||
&mut connection,
|
||||
)
|
||||
.await?;
|
||||
v.extend(part.into_iter().flatten());
|
||||
}
|
||||
Ok::<_, DatabaseError>(v)
|
||||
}
|
||||
.instrument(info_span!("get slug ids"))
|
||||
.await?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let mut connection = self.pool.get().await?;
|
||||
let args = ids
|
||||
.iter()
|
||||
.map(|x| x.value().to_string())
|
||||
.chain(ids.iter().filter_map(|x| {
|
||||
parse_base62(&x.value().to_string())
|
||||
.ok()
|
||||
.map(|x| x.to_string())
|
||||
}))
|
||||
.chain(slug_ids)
|
||||
.map(|x| format!("{}_{namespace}:{x}", self.meta_namespace))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut cached_values = HashMap::new();
|
||||
for chunk in args.chunks(MGET_CHUNK_SIZE) {
|
||||
let part = cmd("MGET")
|
||||
.arg(chunk)
|
||||
.query_async::<Vec<Option<Vec<u8>>>>(&mut connection)
|
||||
.await?;
|
||||
cached_values.extend(part.into_iter().filter_map(|x| {
|
||||
x.and_then(|val| {
|
||||
decode_value::<RedisValue<T, K, S>>(&val)
|
||||
})
|
||||
.map(|val| (val.key.clone(), val))
|
||||
}));
|
||||
}
|
||||
|
||||
Ok::<_, DatabaseError>((cached_values, ids))
|
||||
}
|
||||
.instrument(info_span!("get cached values"))
|
||||
};
|
||||
|
||||
let (default_expiry, actual_expiry) = cache_expiries(namespace);
|
||||
let current_time = Utc::now();
|
||||
let mut expired_values = HashMap::new();
|
||||
|
||||
let (cached_values_raw, ids) = get_cached_values(ids).await?;
|
||||
let mut cached_values = cached_values_raw
|
||||
.into_iter()
|
||||
.filter_map(|(key, val)| {
|
||||
if Utc.timestamp_opt(val.iat + actual_expiry, 0).unwrap()
|
||||
< current_time
|
||||
{
|
||||
expired_values.insert(val.key.to_string(), val);
|
||||
|
||||
None
|
||||
} else {
|
||||
let key_str = val.key.to_string();
|
||||
ids.remove(&key_str);
|
||||
|
||||
if let Ok(value) = key_str.parse::<u64>() {
|
||||
let base62 = to_base62(value);
|
||||
ids.remove(&base62);
|
||||
}
|
||||
|
||||
if let Some(ref alias) = val.alias {
|
||||
ids.remove(&alias.to_string());
|
||||
}
|
||||
|
||||
Some((key, val))
|
||||
}
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let subscribe_ids = DashMap::new();
|
||||
let mut cache_writers = HashMap::new();
|
||||
|
||||
if !ids.is_empty() {
|
||||
let fetch_ids =
|
||||
ids.iter().map(|x| x.key().clone()).collect::<Vec<_>>();
|
||||
|
||||
fetch_ids.into_iter().for_each(|key| {
|
||||
let ns_key_value = if case_sensitive {
|
||||
key.to_lowercase()
|
||||
} else {
|
||||
key.clone()
|
||||
};
|
||||
let namespaced_key = format!(
|
||||
"{}_{namespace}:{ns_key_value}",
|
||||
self.meta_namespace,
|
||||
);
|
||||
let either = self.acquire_lock(namespaced_key);
|
||||
|
||||
match either {
|
||||
Either::Left(sentinel) => {
|
||||
cache_writers.insert(key, sentinel);
|
||||
}
|
||||
|
||||
Either::Right(subscriber) => {
|
||||
if let Some((key, raw_key)) = ids.remove(&key) {
|
||||
if let Some(val) = expired_values.remove(&key) {
|
||||
if let Some(ref alias) = val.alias {
|
||||
ids.remove(&alias.to_string());
|
||||
}
|
||||
|
||||
if let Ok(value) =
|
||||
val.key.to_string().parse::<u64>()
|
||||
{
|
||||
let base62 = to_base62(value);
|
||||
ids.remove(&base62);
|
||||
}
|
||||
|
||||
cached_values.insert(val.key.clone(), val);
|
||||
} else {
|
||||
subscribe_ids.insert(raw_key, subscriber);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let mut fetch_tasks = Vec::new();
|
||||
|
||||
if !ids.is_empty() {
|
||||
fetch_tasks.push(Either::Left(async {
|
||||
let fetch_ids =
|
||||
ids.iter().map(|x| x.value().clone()).collect::<Vec<_>>();
|
||||
|
||||
let vals = closure(fetch_ids).await?;
|
||||
let mut return_values = HashMap::new();
|
||||
|
||||
let mut pipe = redis_pipe();
|
||||
let mut pipe_cmds: usize = 0;
|
||||
let mut connection = self.pool.get().await?;
|
||||
// Doesn't need to be atomic
|
||||
|
||||
if !vals.is_empty() {
|
||||
for (key, (slug, value)) in vals {
|
||||
let value = RedisValue {
|
||||
key: key.clone(),
|
||||
iat: Utc::now().timestamp(),
|
||||
val: value,
|
||||
alias: slug.clone(),
|
||||
};
|
||||
|
||||
pipe.set_ex(
|
||||
format!(
|
||||
"{}_{namespace}:{key}",
|
||||
self.meta_namespace
|
||||
),
|
||||
encode_value(&value)?,
|
||||
default_expiry as u64,
|
||||
);
|
||||
pipe_cmds += 1;
|
||||
|
||||
if let Some(slug) = slug {
|
||||
ids.remove(&slug.to_string());
|
||||
|
||||
if let Some(slug_namespace) = slug_namespace {
|
||||
let actual_slug = if case_sensitive {
|
||||
slug.to_string()
|
||||
} else {
|
||||
slug.to_string().to_lowercase()
|
||||
};
|
||||
|
||||
pipe.set_ex(
|
||||
format!(
|
||||
"{}_{slug_namespace}:{}",
|
||||
self.meta_namespace, actual_slug
|
||||
),
|
||||
key.to_string(),
|
||||
default_expiry as u64,
|
||||
);
|
||||
pipe_cmds += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let key_str = key.to_string();
|
||||
ids.remove(&key_str);
|
||||
|
||||
if let Ok(value) = key_str.parse::<u64>() {
|
||||
let base62 = to_base62(value);
|
||||
ids.remove(&base62);
|
||||
}
|
||||
|
||||
return_values.insert(key, value);
|
||||
|
||||
if pipe_cmds >= PIPELINE_CHUNK_SIZE {
|
||||
pipe.query_async::<()>(&mut connection).await?;
|
||||
pipe = redis_pipe();
|
||||
pipe_cmds = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if pipe_cmds > 0 {
|
||||
pipe.query_async::<()>(&mut connection).await?;
|
||||
}
|
||||
|
||||
drop(cache_writers);
|
||||
|
||||
Result::<_, DatabaseError>::Ok(return_values)
|
||||
}));
|
||||
}
|
||||
|
||||
if !subscribe_ids.is_empty() {
|
||||
fetch_tasks.push(Either::Right(async move {
|
||||
let mut futures = FuturesUnordered::new();
|
||||
let len = subscribe_ids.len();
|
||||
|
||||
for (key, subscriber) in subscribe_ids {
|
||||
futures.push(async move {
|
||||
(
|
||||
key,
|
||||
subscriber
|
||||
.wait_timeout(Duration::from_secs(5))
|
||||
.await,
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
let fetch_ids = DashMap::with_capacity(len);
|
||||
while let Some((key, result)) = futures.next().await {
|
||||
result?;
|
||||
fetch_ids.insert(key.to_string(), key);
|
||||
}
|
||||
|
||||
let (return_values, _) = get_cached_values(fetch_ids).await?;
|
||||
Ok(return_values)
|
||||
}));
|
||||
}
|
||||
|
||||
if !fetch_tasks.is_empty() {
|
||||
for map in futures::future::try_join_all(fetch_tasks).await? {
|
||||
for (key, value) in map {
|
||||
cached_values.insert(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(cached_values.into_iter().map(|x| (x.0, x.1.val)).collect())
|
||||
}
|
||||
|
||||
/// Acquire or create a cache lock onto the given key.
|
||||
fn acquire_lock(
|
||||
&self,
|
||||
key: String,
|
||||
) -> Either<LockSentinel<'_>, util::CacheSubscriber> {
|
||||
let mut out_writer = None;
|
||||
let subscriber =
|
||||
self.cache_list.entry(key.clone()).or_insert_with(|| {
|
||||
let (writer, subscriber) = util::cache();
|
||||
out_writer = Some(writer);
|
||||
subscriber
|
||||
});
|
||||
|
||||
match out_writer {
|
||||
Some(writer) => Either::Left(LockSentinel {
|
||||
pool: self,
|
||||
key,
|
||||
writer,
|
||||
}),
|
||||
None => Either::Right(subscriber.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LockSentinel<'a> {
|
||||
pool: &'a RedisPool,
|
||||
key: String,
|
||||
writer: util::CacheWriter,
|
||||
}
|
||||
|
||||
impl<'a> Drop for LockSentinel<'a> {
|
||||
fn drop(&mut self) {
|
||||
self.writer.write();
|
||||
self.pool.cache_list.remove(&self.key);
|
||||
}
|
||||
}
|
||||
|
||||
impl RedisConnection {
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn set<D>(
|
||||
&mut self,
|
||||
namespace: &str,
|
||||
id: &str,
|
||||
data: D,
|
||||
expiry: Option<i64>,
|
||||
) -> Result<(), DatabaseError>
|
||||
where
|
||||
D: ToRedisArgs + Send + Sync + Debug,
|
||||
{
|
||||
let mut cmd = cmd("SET");
|
||||
cmd.arg(format!("{}_{}:{}", self.meta_namespace, namespace, id))
|
||||
.arg(data)
|
||||
.arg("EX")
|
||||
.arg(expiry.unwrap_or(ENV.REDIS_DEFAULT_EXPIRY));
|
||||
redis_execute::<()>(&mut cmd, &mut self.connection).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, id, data))]
|
||||
pub async fn set_serialized<Id, D>(
|
||||
&mut self,
|
||||
namespace: &str,
|
||||
id: Id,
|
||||
data: D,
|
||||
expiry: Option<i64>,
|
||||
) -> Result<(), DatabaseError>
|
||||
where
|
||||
Id: Display,
|
||||
D: serde::Serialize,
|
||||
{
|
||||
self.set(namespace, &id.to_string(), encode_value(&data)?, expiry)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn get(
|
||||
&mut self,
|
||||
namespace: &str,
|
||||
id: &str,
|
||||
) -> Result<Option<String>, DatabaseError> {
|
||||
let mut cmd = cmd("GET");
|
||||
redis_args(
|
||||
&mut cmd,
|
||||
vec![format!("{}_{}:{}", self.meta_namespace, namespace, id)]
|
||||
.as_slice(),
|
||||
);
|
||||
let res = redis_execute(&mut cmd, &mut self.connection).await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn get_many(
|
||||
&mut self,
|
||||
namespace: &str,
|
||||
ids: &[String],
|
||||
) -> Result<Vec<Option<Vec<u8>>>, DatabaseError> {
|
||||
let mut cmd = cmd("MGET");
|
||||
redis_args(
|
||||
&mut cmd,
|
||||
ids.iter()
|
||||
.map(|x| format!("{}_{}:{}", self.meta_namespace, namespace, x))
|
||||
.collect::<Vec<_>>()
|
||||
.as_slice(),
|
||||
);
|
||||
let res = redis_execute(&mut cmd, &mut self.connection).await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn get_deserialized<R>(
|
||||
&mut self,
|
||||
namespace: &str,
|
||||
id: &str,
|
||||
) -> Result<Option<R>, DatabaseError>
|
||||
where
|
||||
R: for<'a> serde::Deserialize<'a>,
|
||||
{
|
||||
let mut cmd = cmd("GET");
|
||||
redis_args(
|
||||
&mut cmd,
|
||||
vec![format!("{}_{}:{}", self.meta_namespace, namespace, id)]
|
||||
.as_slice(),
|
||||
);
|
||||
let value: Option<Vec<u8>> =
|
||||
redis_execute(&mut cmd, &mut self.connection).await?;
|
||||
Ok(value.and_then(|value| decode_value(&value)))
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn get_many_deserialized<R>(
|
||||
&mut self,
|
||||
namespace: &str,
|
||||
ids: &[String],
|
||||
) -> Result<Vec<Option<R>>, DatabaseError>
|
||||
where
|
||||
R: for<'a> serde::Deserialize<'a>,
|
||||
{
|
||||
Ok(self
|
||||
.get_many(namespace, ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|value| value.and_then(|value| decode_value::<R>(&value)))
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, id))]
|
||||
pub async fn delete<T1>(
|
||||
&mut self,
|
||||
namespace: &str,
|
||||
id: T1,
|
||||
) -> Result<(), DatabaseError>
|
||||
where
|
||||
T1: Display,
|
||||
{
|
||||
let mut cmd = cmd("DEL");
|
||||
redis_args(
|
||||
&mut cmd,
|
||||
vec![format!("{}_{}:{}", self.meta_namespace, namespace, id)]
|
||||
.as_slice(),
|
||||
);
|
||||
redis_execute::<()>(&mut cmd, &mut self.connection).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, iter))]
|
||||
pub async fn delete_many(
|
||||
&mut self,
|
||||
iter: impl IntoIterator<Item = (&str, Option<String>)>,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut cmd = cmd("DEL");
|
||||
let mut any = false;
|
||||
for (namespace, id) in iter {
|
||||
if let Some(id) = id {
|
||||
redis_args(
|
||||
&mut cmd,
|
||||
[format!("{}_{}:{}", self.meta_namespace, namespace, id)]
|
||||
.as_slice(),
|
||||
);
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
|
||||
if any {
|
||||
redis_execute::<()>(&mut cmd, &mut self.connection).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, value))]
|
||||
pub async fn lpush(
|
||||
&mut self,
|
||||
namespace: &str,
|
||||
key: &str,
|
||||
value: impl ToRedisArgs + Send + Sync + Debug,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let key = format!("{}_{namespace}:{key}", self.meta_namespace);
|
||||
cmd("LPUSH")
|
||||
.arg(key)
|
||||
.arg(value)
|
||||
.query_async::<()>(&mut self.connection)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn brpop(
|
||||
&mut self,
|
||||
namespace: &str,
|
||||
key: &str,
|
||||
timeout: Option<f64>,
|
||||
) -> Result<Option<[Vec<u8>; 2]>, DatabaseError> {
|
||||
let key = format!("{}_{namespace}:{key}", self.meta_namespace);
|
||||
// a timeout of 0 is infinite
|
||||
let timeout = timeout.unwrap_or(0.0);
|
||||
let values = cmd("BRPOP")
|
||||
.arg(key)
|
||||
.arg(timeout)
|
||||
.query_async(&mut self.connection)
|
||||
.await?;
|
||||
Ok(values)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn incr(
|
||||
&mut self,
|
||||
namespace: &str,
|
||||
id: &str,
|
||||
) -> Result<Option<u64>, DatabaseError> {
|
||||
let key = format!("{}_{namespace}:{id}", self.meta_namespace);
|
||||
let value = cmd("INCR")
|
||||
.arg(key)
|
||||
.query_async(&mut self.connection)
|
||||
.await?;
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[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
|
||||
}
|
||||
}
|
||||
|
||||
pub fn redis_args(cmd: &mut util::InstrumentedCmd, args: &[String]) {
|
||||
for arg in args {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn redis_execute<T>(
|
||||
cmd: &mut util::InstrumentedCmd,
|
||||
redis: &mut deadpool_redis::Connection,
|
||||
) -> Result<T, deadpool_redis::PoolError>
|
||||
where
|
||||
T: redis::FromRedisValue,
|
||||
{
|
||||
let res = cmd.query_async::<T>(redis).await?;
|
||||
Ok(res)
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use redis::{FromRedisValue, RedisResult, ToRedisArgs};
|
||||
use tokio::sync::Notify;
|
||||
use tokio::time::{Duration, timeout};
|
||||
use tracing::{Instrument, info_span};
|
||||
|
||||
use crate::database::models::DatabaseError;
|
||||
|
||||
pub fn redis_pipe() -> InstrumentedPipeline {
|
||||
InstrumentedPipeline {
|
||||
inner: redis::pipe(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deref, DerefMut)]
|
||||
pub struct InstrumentedPipeline {
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
inner: redis::Pipeline,
|
||||
}
|
||||
|
||||
impl InstrumentedPipeline {
|
||||
pub fn atomic(&mut self) -> &mut Self {
|
||||
self.inner.atomic();
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn query_async<T: FromRedisValue>(
|
||||
&self,
|
||||
con: &mut impl redis::aio::ConnectionLike,
|
||||
) -> RedisResult<T> {
|
||||
self.inner
|
||||
.query_async(con)
|
||||
.instrument(info_span!("pipeline.query_async"))
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cmd(name: &str) -> InstrumentedCmd {
|
||||
InstrumentedCmd {
|
||||
inner: redis::cmd(name),
|
||||
name: name.to_string(),
|
||||
args: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InstrumentedCmd {
|
||||
inner: redis::Cmd,
|
||||
name: String,
|
||||
args: Vec<String>,
|
||||
}
|
||||
|
||||
impl InstrumentedCmd {
|
||||
#[inline]
|
||||
pub fn arg<T: ToRedisArgs + Debug>(&mut self, arg: T) -> &mut Self {
|
||||
self.args.push(format!("{arg:?}"));
|
||||
self.inner.arg(arg);
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn query_async<T: FromRedisValue>(
|
||||
&self,
|
||||
con: &mut impl redis::aio::ConnectionLike,
|
||||
) -> RedisResult<T> {
|
||||
let span = info_span!(
|
||||
"cmd.query_async",
|
||||
// <https://opentelemetry.io/docs/specs/semconv/db/redis/>
|
||||
db.system.name = "redis",
|
||||
db.operation.name = self.name,
|
||||
db.query.text = format!("{} {}", self.name, self.args.join(" ")),
|
||||
);
|
||||
self.inner.query_async(con).instrument(span).await
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cache() -> (CacheWriter, CacheSubscriber) {
|
||||
let shared = Arc::new(Shared::new());
|
||||
(
|
||||
CacheWriter {
|
||||
shared: shared.clone(),
|
||||
},
|
||||
CacheSubscriber { shared },
|
||||
)
|
||||
}
|
||||
|
||||
pub struct CacheWriter {
|
||||
shared: Arc<Shared>,
|
||||
}
|
||||
|
||||
impl CacheWriter {
|
||||
pub fn write(&self) {
|
||||
self.shared.make_ready();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CacheSubscriber {
|
||||
shared: Arc<Shared>,
|
||||
}
|
||||
|
||||
impl CacheSubscriber {
|
||||
pub async fn wait_timeout(
|
||||
self,
|
||||
duration: Duration,
|
||||
) -> Result<(), DatabaseError> {
|
||||
timeout(duration, self.shared.wait()).await.map_err(|_| {
|
||||
DatabaseError::LocalCacheTimeout {
|
||||
released: 0,
|
||||
total: 1,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct Shared {
|
||||
ready: AtomicBool,
|
||||
// With this implementation's intrusive linked lists, the waiters are stored inline in the future
|
||||
// so there's no heap allocation per waiter.
|
||||
wakers: Notify,
|
||||
}
|
||||
|
||||
impl Shared {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
ready: AtomicBool::new(false),
|
||||
wakers: Notify::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_ready(&self) {
|
||||
self.ready.store(true, Ordering::Release);
|
||||
self.wakers.notify_waiters();
|
||||
}
|
||||
|
||||
async fn wait(&self) {
|
||||
let ready = self.ready.load(Ordering::Acquire);
|
||||
|
||||
if ready {
|
||||
return;
|
||||
}
|
||||
|
||||
let notification = self.wakers.notified();
|
||||
// Don't need to call `enable` as we use notify_waiters
|
||||
|
||||
// Prevent race where the writer set the ready bit and notified waiters between the load and registering the waiter
|
||||
let ready = self.ready.load(Ordering::SeqCst);
|
||||
|
||||
if ready {
|
||||
return;
|
||||
}
|
||||
|
||||
notification.await;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user