mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 17:38:10 +00:00
Merge pull request #37 from storytold/cache-profiles-redis
cache user profiles with redis
This commit is contained in:
6 files changed
+123
-54
No files matched your search
Generated
+1
@@ -7745,6 +7745,7 @@ dependencies = [
|
||||
"log",
|
||||
"md-5",
|
||||
"once_cell",
|
||||
"r2d2_redis",
|
||||
"regex",
|
||||
"reusable_types",
|
||||
"serde",
|
||||
|
||||
@@ -5,7 +5,7 @@ use log::{warn, info};
|
||||
use sqlx::{MySql, MySqlPool};
|
||||
use sqlx::pool::PoolConnection;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct UserBadgeForList {
|
||||
pub slug: String,
|
||||
pub title: String,
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@ use std::sync::Arc;
|
||||
use tokens::users::user::UserToken;
|
||||
|
||||
// TODO: Make non-`Serialize` and make the HTTP endpoints do the work
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct UserProfileResult {
|
||||
pub user_token: UserToken,
|
||||
pub username: String,
|
||||
@@ -49,7 +49,7 @@ pub struct UserProfileResult {
|
||||
}
|
||||
|
||||
// TODO: Make non-`Serialize` and make the HTTP endpoints do the work
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct UserProfileModeratorFields {
|
||||
pub is_banned: bool,
|
||||
pub maybe_mod_comments: Option<String>,
|
||||
|
||||
@@ -55,3 +55,7 @@ toml = "0.5.8"
|
||||
# "rustls" doesn't appear to work with IP addresses, per:
|
||||
# https://github.com/launchbadge/sqlx/issues/1095#issuecomment-798050828
|
||||
sqlx = { version = "0.6.2", features = [ "mysql", "runtime-actix-native-tls", "offline", "chrono" ] }
|
||||
|
||||
# TODO: Shouldn't really be a dependency, but need to add caching for the DDoS attack.
|
||||
# Redis Database
|
||||
r2d2_redis = "0.14.0"
|
||||
@@ -11,18 +11,20 @@ use chrono::{DateTime, Utc};
|
||||
use crate::utils::session_checker::SessionChecker;
|
||||
use database_queries::queries::users::user_badges::list_user_badges::UserBadgeForList;
|
||||
use database_queries::queries::users::user_badges::list_user_badges::list_user_badges;
|
||||
use database_queries::queries::users::user_profiles::get_user_profile_by_username::get_user_profile_by_username_from_connection;
|
||||
use database_queries::queries::users::user_profiles::get_user_profile_by_username::{get_user_profile_by_username_from_connection, UserProfileResult};
|
||||
use enums::common::visibility::Visibility;
|
||||
use http_server_common::request::get_request_header_optional::get_request_header_optional;
|
||||
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
|
||||
use http_server_common::util::timer::MultiBenchmarkingTimer;
|
||||
use log::warn;
|
||||
use log::{error, warn};
|
||||
use sqlx::MySqlPool;
|
||||
use std::fmt;
|
||||
use r2d2_redis::{r2d2, RedisConnectionManager};
|
||||
use r2d2_redis::r2d2::PooledConnection;
|
||||
use r2d2_redis::redis::Commands;
|
||||
use tokens::users::user::UserToken;
|
||||
|
||||
// TODO: This is duplicated in query_user_profile
|
||||
// TODO: This handler has embedded queries.
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct UserProfileRecordForResponse {
|
||||
@@ -97,6 +99,7 @@ pub async fn get_profile_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<GetProfilePathInfo>,
|
||||
mysql_pool: web::Data<MySqlPool>,
|
||||
redis_pool: web::Data<r2d2::Pool<RedisConnectionManager>>,
|
||||
session_checker: web::Data<SessionChecker>,
|
||||
) -> Result<HttpResponse, ProfileError>
|
||||
{
|
||||
@@ -130,25 +133,65 @@ pub async fn get_profile_handler(
|
||||
is_mod = user_session.can_ban_users;
|
||||
}
|
||||
|
||||
let (pool_connection, maybe_user_profile) =
|
||||
benchmark.time_async_section_moving_args("profile query", pool_connection, |mut pc| async {
|
||||
let ret = get_user_profile_by_username_from_connection(&path.username, &mut pc).await;
|
||||
(pc, ret)
|
||||
}).await;
|
||||
|
||||
let user_profile = match maybe_user_profile {
|
||||
Ok(Some(user_profile)) => user_profile,
|
||||
Ok(None) => {
|
||||
warn!("Invalid user");
|
||||
return Err(ProfileError::NotFound);
|
||||
},
|
||||
Err(err) => {
|
||||
warn!("User profile query error: {:?}", err);
|
||||
return Err(ProfileError::ServerError);
|
||||
}
|
||||
// TODO: Standard cache key
|
||||
let cache_key = format!("cache:userProfile:{}", path.username);
|
||||
let mut maybe_redis = redis_pool.get().ok();
|
||||
|
||||
let mut maybe_user_data = None;
|
||||
let mut store_in_cache = false;
|
||||
|
||||
if let Some(redis) = maybe_redis.as_mut() {
|
||||
maybe_user_data = try_get_user_from_redis_cache(&cache_key, redis);
|
||||
}
|
||||
|
||||
if maybe_user_data.is_some() {
|
||||
warn!("User pulled from Redis cache key: {}", cache_key);
|
||||
}
|
||||
|
||||
if maybe_user_data.is_none() {
|
||||
let (pool_connection, maybe_user_profile) =
|
||||
benchmark.time_async_section_moving_args("profile query", pool_connection, |mut pc| async {
|
||||
let ret = get_user_profile_by_username_from_connection(&path.username, &mut pc).await;
|
||||
(pc, ret)
|
||||
}).await;
|
||||
|
||||
let user_profile = match maybe_user_profile {
|
||||
Ok(Some(user_profile)) => user_profile,
|
||||
Ok(None) => return Err(ProfileError::NotFound),
|
||||
Err(err) => {
|
||||
error!("User profile query error: {:?}", err);
|
||||
return Err(ProfileError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
let (_pool_connection, maybe_badges) =
|
||||
benchmark.time_async_section_moving_args("badges query", pool_connection, |mut pc| async {
|
||||
let ret = list_user_badges(&mut pc, &user_profile.user_token.0)
|
||||
.await;
|
||||
(pc, ret)
|
||||
}).await;
|
||||
|
||||
let badges = maybe_badges
|
||||
.unwrap_or_else(|err| {
|
||||
warn!("Error querying badges: {:?}", err);
|
||||
return Vec::new(); // NB: Fine if this fails. Not sure why it would.
|
||||
});
|
||||
|
||||
maybe_user_data = Some(RedisCacheData {
|
||||
user_profile: user_profile.clone(),
|
||||
badges: badges.clone(),
|
||||
});
|
||||
|
||||
store_in_cache = true;
|
||||
}
|
||||
|
||||
let user_data = match maybe_user_data {
|
||||
None => return Err(ProfileError::NotFound),
|
||||
Some(user_data) => user_data,
|
||||
};
|
||||
|
||||
let is_banned = user_profile.maybe_moderator_fields
|
||||
let is_banned = user_data.user_profile.maybe_moderator_fields
|
||||
.as_ref()
|
||||
.map(|mod_fields| mod_fields.is_banned)
|
||||
.unwrap_or(false);
|
||||
@@ -158,46 +201,43 @@ pub async fn get_profile_handler(
|
||||
return Err(ProfileError::NotFound);
|
||||
}
|
||||
|
||||
let (_pool_connection, maybe_badges) =
|
||||
benchmark.time_async_section_moving_args("badges query", pool_connection, |mut pc| async {
|
||||
let ret = list_user_badges(&mut pc, &user_profile.user_token.0)
|
||||
.await;
|
||||
(pc, ret)
|
||||
}).await;
|
||||
|
||||
let badges = maybe_badges
|
||||
.unwrap_or_else(|err| {
|
||||
warn!("Error querying badges: {:?}", err);
|
||||
return Vec::new(); // NB: Fine if this fails. Not sure why it would.
|
||||
});
|
||||
if store_in_cache {
|
||||
if let Some(redis) = maybe_redis.as_mut() {
|
||||
if let Ok(redis_payload) = serde_json::to_string(&user_data) {
|
||||
const SECONDS : usize = 60;
|
||||
// NB: Compiler can't figure out the throwaway result type
|
||||
let _r : Option<String> = redis.set_ex(&cache_key, redis_payload, SECONDS).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut profile_for_response = UserProfileRecordForResponse {
|
||||
user_token: user_profile.user_token,
|
||||
username: user_profile.username,
|
||||
display_name: user_profile.display_name,
|
||||
email_gravatar_hash: user_profile.email_gravatar_hash,
|
||||
profile_markdown: user_profile.profile_markdown,
|
||||
profile_rendered_html: user_profile.profile_rendered_html,
|
||||
user_role_slug: user_profile.user_role_slug,
|
||||
disable_gravatar: user_profile.disable_gravatar,
|
||||
preferred_tts_result_visibility: user_profile.preferred_tts_result_visibility,
|
||||
preferred_w2l_result_visibility: user_profile.preferred_w2l_result_visibility,
|
||||
discord_username: user_profile.discord_username,
|
||||
twitch_username: user_profile.twitch_username,
|
||||
twitter_username: user_profile.twitter_username,
|
||||
patreon_username: user_profile.patreon_username,
|
||||
github_username: user_profile.github_username,
|
||||
cashapp_username: user_profile.cashapp_username,
|
||||
website_url: user_profile.website_url,
|
||||
created_at: user_profile.created_at,
|
||||
maybe_moderator_fields: user_profile.maybe_moderator_fields.map(|mod_fields| {
|
||||
user_token: user_data.user_profile.user_token,
|
||||
username: user_data.user_profile.username,
|
||||
display_name: user_data.user_profile.display_name,
|
||||
email_gravatar_hash: user_data.user_profile.email_gravatar_hash,
|
||||
profile_markdown: user_data.user_profile.profile_markdown,
|
||||
profile_rendered_html: user_data.user_profile.profile_rendered_html,
|
||||
user_role_slug: user_data.user_profile.user_role_slug,
|
||||
disable_gravatar: user_data.user_profile.disable_gravatar,
|
||||
preferred_tts_result_visibility: user_data.user_profile.preferred_tts_result_visibility,
|
||||
preferred_w2l_result_visibility: user_data.user_profile.preferred_w2l_result_visibility,
|
||||
discord_username: user_data.user_profile.discord_username,
|
||||
twitch_username: user_data.user_profile.twitch_username,
|
||||
twitter_username: user_data.user_profile.twitter_username,
|
||||
patreon_username: user_data.user_profile.patreon_username,
|
||||
github_username: user_data.user_profile.github_username,
|
||||
cashapp_username: user_data.user_profile.cashapp_username,
|
||||
website_url: user_data.user_profile.website_url,
|
||||
created_at: user_data.user_profile.created_at,
|
||||
maybe_moderator_fields: user_data.user_profile.maybe_moderator_fields.map(|mod_fields| {
|
||||
UserProfileModeratorFields {
|
||||
is_banned: mod_fields.is_banned,
|
||||
maybe_mod_comments: mod_fields.maybe_mod_comments,
|
||||
maybe_mod_user_token: mod_fields.maybe_mod_user_token,
|
||||
}
|
||||
}),
|
||||
badges,
|
||||
badges: user_data.badges,
|
||||
};
|
||||
|
||||
if !is_mod {
|
||||
@@ -231,3 +271,26 @@ pub async fn get_profile_handler(
|
||||
|
||||
Ok(http_response)
|
||||
}
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub (crate) struct RedisCacheData {
|
||||
pub user_profile: UserProfileResult,
|
||||
pub badges: Vec<UserBadgeForList>,
|
||||
}
|
||||
|
||||
// TODO: Async
|
||||
pub (crate) fn try_get_user_from_redis_cache(
|
||||
cache_key: &str,
|
||||
redis: &mut PooledConnection<RedisConnectionManager>,
|
||||
) -> Option<RedisCacheData> {
|
||||
|
||||
let results : Option<String> = redis.get(cache_key).ok().flatten();
|
||||
|
||||
let redis_data = match results {
|
||||
None => return None,
|
||||
Some(redis_data) => redis_data,
|
||||
};
|
||||
|
||||
serde_json::from_str(&redis_data).ok()
|
||||
}
|
||||
@@ -506,6 +506,7 @@ pub async fn serve(server_state: ServerState) -> AnyhowResult<()>
|
||||
let app = App::new()
|
||||
.app_data(web::Data::new(server_state_arc.firehose_publisher.clone()))
|
||||
.app_data(web::Data::new(server_state_arc.mysql_pool.clone()))
|
||||
.app_data(web::Data::new(server_state_arc.redis_pool.clone()))
|
||||
.app_data(web::Data::new(server_state_arc.session_checker.clone()))
|
||||
.app_data(web::Data::new(server_state_arc.cookie_manager.clone()))
|
||||
.app_data(web::Data::new(server_state_arc.stripe.clone().config.clone()))
|
||||
|
||||
Reference in new issue
Block a user