cache in memory
This commit is contained in:
498
src/cache.rs
498
src/cache.rs
@@ -2,213 +2,163 @@ use crate::api::{Api, CurrentUser};
|
||||
use crate::db_conn::Db;
|
||||
use crate::models::{Comment, Post, User};
|
||||
use crate::rds_conn::RdsConn;
|
||||
use crate::rds_models::{clear_all, init, BlockedUsers};
|
||||
use crate::rds_models::BlockedUsers;
|
||||
use diesel::result::{Error as DieselError, QueryResult};
|
||||
use moka::future::Cache;
|
||||
use rand::Rng;
|
||||
use redis::{AsyncCommands, RedisError, RedisResult};
|
||||
use rocket::serde::json::serde_json;
|
||||
// can use rocket::serde::json::to_string in master version
|
||||
use futures_util::stream::StreamExt;
|
||||
use redis::RedisResult;
|
||||
use rocket::futures::future;
|
||||
use rocket::tokio::sync::RwLock;
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
|
||||
const KEY_USER_COUNT: &str = "hole_v2:cache:user_count";
|
||||
const USER_COUNT_EXPIRE_TIME: u64 = 5 * 60;
|
||||
|
||||
const USER_COUNT_EXPIRE_TIME: u64 = 60;
|
||||
const INSTANCE_EXPIRE_TIME: u64 = 60 * 60;
|
||||
|
||||
const MIN_LENGTH: isize = 200;
|
||||
const MAX_LENGTH: isize = 900;
|
||||
const CUT_LENGTH: isize = 100;
|
||||
|
||||
macro_rules! post_cache_key {
|
||||
($id: expr) => {
|
||||
format!("hole_v2:cache:post:{}:v2", $id)
|
||||
};
|
||||
// Global cache getters using OnceLock
|
||||
fn post_cache() -> &'static Cache<i32, Post> {
|
||||
static CACHE: OnceLock<Cache<i32, Post>> = OnceLock::new();
|
||||
CACHE.get_or_init(|| Cache::builder().max_capacity(10_000).build())
|
||||
}
|
||||
|
||||
pub struct PostCache {
|
||||
rconn: RdsConn,
|
||||
fn post_comment_cache() -> &'static Cache<String, Vec<Comment>> {
|
||||
static CACHE: OnceLock<Cache<String, Vec<Comment>>> = OnceLock::new();
|
||||
CACHE.get_or_init(|| {
|
||||
Cache::builder()
|
||||
.time_to_idle(Duration::from_secs(INSTANCE_EXPIRE_TIME))
|
||||
.build()
|
||||
})
|
||||
}
|
||||
|
||||
// Each list in post_list_cache, keyed by room_id and mode, is a sorted list. The element is a pair of numbers. The first one is the weight used to sort, the second one is the post id.
|
||||
fn post_list_cache() -> &'static Cache<String, Arc<RwLock<Vec<(i64, i32)>>>> {
|
||||
static CACHE: OnceLock<Cache<String, Arc<RwLock<Vec<(i64, i32)>>>>> = OnceLock::new();
|
||||
CACHE.get_or_init(|| Cache::builder().build())
|
||||
}
|
||||
|
||||
fn user_cache() -> &'static Cache<String, User> {
|
||||
static CACHE: OnceLock<Cache<String, User>> = OnceLock::new();
|
||||
CACHE.get_or_init(|| {
|
||||
Cache::builder()
|
||||
.time_to_idle(Duration::from_secs(INSTANCE_EXPIRE_TIME))
|
||||
.build()
|
||||
})
|
||||
}
|
||||
|
||||
fn block_dict_cache() -> &'static Cache<String, Arc<RwLock<HashMap<String, bool>>>> {
|
||||
static CACHE: OnceLock<Cache<String, Arc<RwLock<HashMap<String, bool>>>>> = OnceLock::new();
|
||||
CACHE.get_or_init(|| {
|
||||
Cache::builder()
|
||||
.time_to_idle(Duration::from_secs(INSTANCE_EXPIRE_TIME))
|
||||
.build()
|
||||
})
|
||||
}
|
||||
|
||||
fn user_count_cache() -> &'static Cache<String, i64> {
|
||||
static CACHE: OnceLock<Cache<String, i64>> = OnceLock::new();
|
||||
CACHE.get_or_init(|| {
|
||||
Cache::builder()
|
||||
.time_to_live(Duration::from_secs(USER_COUNT_EXPIRE_TIME))
|
||||
.build()
|
||||
})
|
||||
}
|
||||
|
||||
fn map_shared_diesel_error(err: Arc<DieselError>) -> DieselError {
|
||||
match err.as_ref() {
|
||||
DieselError::NotFound => DieselError::NotFound,
|
||||
DieselError::RollbackTransaction => DieselError::RollbackTransaction,
|
||||
DieselError::AlreadyInTransaction => DieselError::AlreadyInTransaction,
|
||||
DieselError::NotInTransaction => DieselError::NotInTransaction,
|
||||
DieselError::BrokenTransactionManager => DieselError::BrokenTransactionManager,
|
||||
_ => DieselError::QueryBuilderError(Box::new(io::Error::other(err.to_string()))),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PostCache;
|
||||
|
||||
impl PostCache {
|
||||
init!();
|
||||
|
||||
clear_all!("hole_v2:cache::post:*:v2");
|
||||
|
||||
pub async fn sets(&mut self, ps: &[&Post]) {
|
||||
pub async fn sets(ps: &[&Post]) {
|
||||
if ps.is_empty() {
|
||||
return;
|
||||
}
|
||||
let kvs: Vec<(String, String)> = ps
|
||||
.iter()
|
||||
.map(|p| (post_cache_key!(p.id), serde_json::to_string(p).unwrap()))
|
||||
.collect();
|
||||
self.rconn.mset(&kvs).await.unwrap_or_else(|e| {
|
||||
warn!("set post cache failed: {}", e);
|
||||
dbg!(&kvs);
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn get(&mut self, pid: &i32) -> Option<Post> {
|
||||
let key = post_cache_key!(pid);
|
||||
let rds_result: Option<String> = self
|
||||
.rconn
|
||||
.get::<String, Option<String>>(key)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
warn!("try to get post cache, connect rds failed, {}", e);
|
||||
None
|
||||
});
|
||||
|
||||
rds_result.and_then(|s| {
|
||||
serde_json::from_str(&s).unwrap_or_else(|e| {
|
||||
warn!("get post cache, decode failed {}, {}", e, s);
|
||||
None
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn gets(&mut self, pids: &[i32]) -> Vec<Option<Post>> {
|
||||
// 长度为1时会走GET而非MGET,返回值格式不兼容。愚蠢的设计。
|
||||
match pids.len() {
|
||||
0 => vec![],
|
||||
1 => vec![self.get(&pids[0]).await],
|
||||
_ => {
|
||||
let ks: Vec<String> = pids.iter().map(|pid| post_cache_key!(pid)).collect();
|
||||
// dbg!(&ks);
|
||||
// Vec is single arg, while &Vec is not. Seems a bug.
|
||||
let rds_result: Vec<Option<String>> = self
|
||||
.rconn
|
||||
.get::<Vec<String>, Vec<Option<String>>>(ks)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
warn!("try to get posts cache, connect rds failed, {}", e);
|
||||
vec![None; pids.len()]
|
||||
});
|
||||
// dbg!(&rds_result);
|
||||
|
||||
// 定期热度衰减的时候会清空缓存,这里设不设置过期时间影响不大
|
||||
|
||||
rds_result
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
// dbg!(&x);
|
||||
x.and_then(|s| {
|
||||
serde_json::from_str(&s).unwrap_or_else(|e| {
|
||||
warn!("get post cache, decode failed {}, {}", e, s);
|
||||
None
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
for p in ps {
|
||||
post_cache().insert(p.id, (*p).clone()).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get(pid: &i32) -> Option<Post> {
|
||||
post_cache().get(pid).await
|
||||
}
|
||||
|
||||
pub async fn get_with<F>(pid: i32, init: F) -> QueryResult<Post>
|
||||
where
|
||||
F: Future<Output = QueryResult<Post>>,
|
||||
{
|
||||
post_cache()
|
||||
.try_get_with(pid, init)
|
||||
.await
|
||||
.map_err(map_shared_diesel_error)
|
||||
}
|
||||
|
||||
pub async fn gets(pids: &[i32]) -> Vec<Option<Post>> {
|
||||
future::join_all(pids.iter().map(Self::get)).await
|
||||
}
|
||||
|
||||
pub async fn clear_all() {
|
||||
post_cache().invalidate_all();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PostCommentCache {
|
||||
key: String,
|
||||
rconn: RdsConn,
|
||||
}
|
||||
|
||||
impl PostCommentCache {
|
||||
init!(i32, "hole_v2:cache:post_comments:{}");
|
||||
|
||||
pub async fn set(&mut self, cs: &[Comment]) {
|
||||
self.rconn
|
||||
.set_ex(
|
||||
&self.key,
|
||||
serde_json::to_string(cs).unwrap(),
|
||||
INSTANCE_EXPIRE_TIME,
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
warn!("set comments cache failed: {}", e);
|
||||
dbg!(cs);
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get(&mut self) -> Option<Vec<Comment>> {
|
||||
let rds_result = self.rconn.get::<&String, String>(&self.key).await;
|
||||
// dbg!(&rds_result);
|
||||
if let Ok(s) = rds_result {
|
||||
self.rconn
|
||||
.expire::<&String, bool>(&self.key, INSTANCE_EXPIRE_TIME as i64)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
warn!(
|
||||
"get comments cache, set new expire failed: {}, {}, {} ",
|
||||
e, &self.key, &s
|
||||
);
|
||||
false
|
||||
});
|
||||
serde_json::from_str(&s).unwrap_or_else(|e| {
|
||||
warn!("get comments cache, decode failed {}, {}", e, s);
|
||||
None
|
||||
})
|
||||
} else {
|
||||
None
|
||||
pub fn init(post_id: i32) -> Self {
|
||||
Self {
|
||||
key: format!("hole_v2:cache:post_comments:{}", post_id),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_with<F>(&self, init: F) -> QueryResult<Vec<Comment>>
|
||||
where
|
||||
F: Future<Output = QueryResult<Vec<Comment>>>,
|
||||
{
|
||||
post_comment_cache()
|
||||
.try_get_with(self.key.clone(), init)
|
||||
.await
|
||||
.map_err(map_shared_diesel_error)
|
||||
}
|
||||
|
||||
pub async fn clear(&mut self) {
|
||||
self.rconn.del(&self.key).await.unwrap_or_else(|e| {
|
||||
warn!("clear commenrs cache fail, {}", e);
|
||||
});
|
||||
post_comment_cache().invalidate(&self.key).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PostListCache {
|
||||
key: String,
|
||||
mode: u8,
|
||||
rconn: RdsConn,
|
||||
length: isize,
|
||||
}
|
||||
|
||||
impl PostListCache {
|
||||
pub fn init(room_id: Option<i32>, mode: u8, rconn: &RdsConn) -> Self {
|
||||
pub const MAX_LENGTH: usize = 900;
|
||||
// pub const MIN_LENGTH: usize = 200;
|
||||
pub const CUT_LENGTH: usize = 100;
|
||||
pub fn init(room_id: Option<i32>, mode: u8) -> Self {
|
||||
Self {
|
||||
key: format!(
|
||||
"hole_v2:cache:post_list:{}:{}",
|
||||
match room_id {
|
||||
Some(i) => i.to_string(),
|
||||
None => "".to_owned(),
|
||||
},
|
||||
room_id.map_or_else(String::new, |i| i.to_string()),
|
||||
&mode
|
||||
),
|
||||
mode,
|
||||
rconn: rconn.clone(),
|
||||
length: 0,
|
||||
}
|
||||
}
|
||||
|
||||
async fn set_and_check_length(&mut self) {
|
||||
let mut l = self.rconn.zcard(&self.key).await.unwrap();
|
||||
if l > MAX_LENGTH {
|
||||
self.rconn
|
||||
.zremrangebyrank::<&String, ()>(&self.key, MAX_LENGTH - CUT_LENGTH, -1)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
warn!("cut list cache failed, {}, {}", e, &self.key);
|
||||
});
|
||||
l = MIN_LENGTH;
|
||||
}
|
||||
self.length = l;
|
||||
}
|
||||
|
||||
pub async fn need_fill(&mut self) -> bool {
|
||||
self.set_and_check_length().await;
|
||||
self.length < MIN_LENGTH
|
||||
}
|
||||
|
||||
pub fn i64_len(&self) -> i64 {
|
||||
self.length.try_into().unwrap()
|
||||
}
|
||||
|
||||
pub fn i64_minlen(&self) -> i64 {
|
||||
MIN_LENGTH.try_into().unwrap()
|
||||
}
|
||||
|
||||
fn p2pair(&self, p: &Post) -> (i64, i32) {
|
||||
(
|
||||
match self.mode {
|
||||
@@ -223,163 +173,155 @@ impl PostListCache {
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn fill(&mut self, ps: &[Post]) {
|
||||
let items: Vec<(i64, i32)> = ps.iter().map(|p| self.p2pair(p)).collect();
|
||||
self.rconn
|
||||
.zadd_multiple(&self.key, &items)
|
||||
pub async fn fill_with<F>(&mut self, query_posts: F) -> QueryResult<usize>
|
||||
where
|
||||
F: Future<Output = QueryResult<Vec<Post>>>,
|
||||
{
|
||||
let list_ref = post_list_cache()
|
||||
.try_get_with(self.key.clone(), async {
|
||||
let mut items: Vec<(i64, i32)> =
|
||||
query_posts.await?.iter().map(|p| self.p2pair(&p)).collect();
|
||||
items.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
Ok(Arc::new(RwLock::new(items)))
|
||||
})
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
warn!("fill list cache failed, {} {}", e, &self.key);
|
||||
});
|
||||
.map_err(map_shared_diesel_error)?;
|
||||
let list = list_ref.read().await;
|
||||
|
||||
self.set_and_check_length().await;
|
||||
}
|
||||
// Double-Checked Locking
|
||||
if list.len() <= Self::MAX_LENGTH {
|
||||
return Ok(list.len());
|
||||
}
|
||||
drop(list);
|
||||
let mut list = list_ref.write().await;
|
||||
|
||||
pub async fn put(&mut self, p: &Post) {
|
||||
// 其他都是加到最前面的,但热榜不是。可能导致MIN_LENGTH到MAX_LENGTH之间的数据不可靠
|
||||
// 影响不大,先不管了
|
||||
if p.is_deleted || (self.mode > 0 && p.is_reported) {
|
||||
self.rconn.zrem(&self.key, p.id).await.unwrap_or_else(|e| {
|
||||
warn!(
|
||||
"remove from list cache failed, {} {} {}",
|
||||
e, &self.key, p.id
|
||||
);
|
||||
});
|
||||
if list.len() <= Self::MAX_LENGTH {
|
||||
Ok(list.len())
|
||||
} else {
|
||||
let (s, m) = self.p2pair(p);
|
||||
self.rconn.zadd(&self.key, m, s).await.unwrap_or_else(|e| {
|
||||
warn!(
|
||||
"put into list cache failed, {} {} {} {}",
|
||||
e, &self.key, m, s
|
||||
);
|
||||
});
|
||||
list.truncate(Self::MAX_LENGTH - Self::CUT_LENGTH);
|
||||
Ok(list.len())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_pids(&mut self, start: i64, limit: i64) -> Vec<i32> {
|
||||
self.rconn
|
||||
.zrange(
|
||||
&self.key,
|
||||
start.try_into().unwrap(),
|
||||
(start + limit - 1).try_into().unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
pub async fn put(&mut self, p: &Post) {
|
||||
// Don't put is there is no cache. Let fill_with handle it.
|
||||
if let Some(list_ref) = post_list_cache().get(&self.key).await {
|
||||
let mut list = list_ref.write().await;
|
||||
// Remove any existing entry for this post_id
|
||||
if let Some(pos) = list.iter().position(|(_, pid)| *pid == p.id) {
|
||||
list.remove(pos);
|
||||
}
|
||||
if p.is_deleted || (self.mode > 0 && p.is_reported) {
|
||||
return;
|
||||
}
|
||||
list.push(self.p2pair(p));
|
||||
list.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_pids(&mut self, start: usize, limit: usize) -> Vec<i32> {
|
||||
if let Some(list_ref) = post_list_cache().get(&self.key).await {
|
||||
let list = list_ref.read().await;
|
||||
list.iter()
|
||||
.skip(start)
|
||||
.take(limit)
|
||||
.map(|(_, pid)| *pid)
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn clear(&mut self) {
|
||||
self.rconn.del(&self.key).await.unwrap_or_else(|e| {
|
||||
warn!("clear post list cache failed, {}", e);
|
||||
});
|
||||
post_list_cache().invalidate(&self.key).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UserCache {
|
||||
key: String,
|
||||
rconn: RdsConn,
|
||||
}
|
||||
|
||||
impl UserCache {
|
||||
init!(&str, "hole_v2:cache:user:{}");
|
||||
|
||||
clear_all!("hole_v2:cache:user:*");
|
||||
|
||||
pub async fn set(&mut self, u: &User) {
|
||||
self.rconn
|
||||
.set_ex(
|
||||
&self.key,
|
||||
serde_json::to_string(u).unwrap(),
|
||||
INSTANCE_EXPIRE_TIME,
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
warn!("set user cache failed: {}", e);
|
||||
dbg!(u);
|
||||
})
|
||||
pub fn init(user_id: &str) -> Self {
|
||||
Self {
|
||||
key: format!("hole_v2:cache:user:{}", user_id),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get(&mut self) -> Option<User> {
|
||||
let rds_result = self.rconn.get::<&String, String>(&self.key).await;
|
||||
if let Ok(s) = rds_result {
|
||||
self.rconn
|
||||
.expire::<&String, bool>(&self.key, INSTANCE_EXPIRE_TIME as i64)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
warn!(
|
||||
"get user cache, set new expire failed: {}, {}, {} ",
|
||||
e, &self.key, &s
|
||||
);
|
||||
false
|
||||
});
|
||||
serde_json::from_str(&s).unwrap_or_else(|e| {
|
||||
warn!("get user cache, decode failed {}, {}", e, s);
|
||||
None
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
// No need to use get_with for User. Just check and set separately.
|
||||
pub async fn set(&self, u: &User) {
|
||||
user_cache().insert(self.key.clone(), u.clone()).await;
|
||||
}
|
||||
|
||||
pub async fn get(&self) -> Option<User> {
|
||||
user_cache().get(&self.key).await
|
||||
}
|
||||
|
||||
pub async fn clear_all() {
|
||||
user_cache().invalidate_all();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BlockDictCache {
|
||||
key: String,
|
||||
rconn: RdsConn,
|
||||
}
|
||||
|
||||
impl BlockDictCache {
|
||||
// namehash, pid
|
||||
init!(&str, i32, "hole_v2:cache:block_dict:{}:{}");
|
||||
pub fn init(namehash: &str, post_id: i32) -> Self {
|
||||
Self {
|
||||
key: format!("hole_v2:cache:block_dict:{}:{}", namehash, post_id),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_or_create(
|
||||
&mut self,
|
||||
user: &CurrentUser,
|
||||
hash_list: &[&String],
|
||||
rconn: &RdsConn,
|
||||
) -> RedisResult<HashMap<String, bool>> {
|
||||
let mut block_dict = self
|
||||
.rconn
|
||||
.hgetall::<&String, HashMap<String, bool>>(&self.key)
|
||||
.await?;
|
||||
let dict_ref = block_dict_cache()
|
||||
.get_with(self.key.clone(), async move {
|
||||
Arc::new(RwLock::new(HashMap::new()))
|
||||
})
|
||||
.await;
|
||||
|
||||
//dbg!(&self.key, &block_dict);
|
||||
|
||||
let missing: Vec<(String, bool)> =
|
||||
future::try_join_all(hash_list.iter().filter_map(|hash| {
|
||||
(!block_dict.contains_key(&hash.to_string())).then_some(async {
|
||||
Ok::<(String, bool), RedisError>((
|
||||
hash.to_string(),
|
||||
BlockedUsers::check_if_block(&self.rconn, user, hash).await?,
|
||||
))
|
||||
})
|
||||
}))
|
||||
.await?;
|
||||
|
||||
if !missing.is_empty() {
|
||||
self.rconn.hset_multiple(&self.key, &missing).await?;
|
||||
self.rconn
|
||||
.expire(&self.key, INSTANCE_EXPIRE_TIME as i64)
|
||||
.await?;
|
||||
block_dict.extend(missing.into_iter());
|
||||
// Find missing hashes
|
||||
let mut missing_keys: Vec<String> = Vec::new();
|
||||
{
|
||||
let block_dict = dict_ref.read().await;
|
||||
for hash in hash_list {
|
||||
if !block_dict.contains_key(hash.as_str()) {
|
||||
missing_keys.push((*hash).clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//dbg!(&block_dict);
|
||||
if !missing_keys.is_empty() {
|
||||
let mut missing: Vec<(String, bool)> = Vec::with_capacity(missing_keys.len());
|
||||
for hash in missing_keys {
|
||||
let is_blocked = BlockedUsers::check_if_block(rconn, user, &hash).await?;
|
||||
missing.push((hash, is_blocked));
|
||||
}
|
||||
|
||||
Ok(block_dict)
|
||||
let mut block_dict = dict_ref.write().await;
|
||||
for (hash, is_blocked) in missing {
|
||||
block_dict.entry(hash).or_insert(is_blocked);
|
||||
}
|
||||
}
|
||||
|
||||
let out = dict_ref.read().await.clone();
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub async fn clear(&mut self) -> RedisResult<()> {
|
||||
self.rconn.del(&self.key).await
|
||||
pub async fn clear(&mut self) {
|
||||
block_dict_cache().invalidate(&self.key).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn cached_user_count(db: &Db, rconn: &mut RdsConn) -> Api<i64> {
|
||||
let cnt: Option<i64> = rconn.get(KEY_USER_COUNT).await?;
|
||||
if let Some(x) = cnt {
|
||||
Ok(x)
|
||||
} else {
|
||||
let x = User::get_count(db).await?;
|
||||
rconn
|
||||
.set_ex(KEY_USER_COUNT, x, USER_COUNT_EXPIRE_TIME)
|
||||
.await?;
|
||||
Ok(x)
|
||||
}
|
||||
pub async fn cached_user_count(db: &Db) -> Api<i64> {
|
||||
let key = "hole_v2:cache:user_count";
|
||||
Ok(user_count_cache()
|
||||
.try_get_with(key.to_string(), async { User::get_count(db).await })
|
||||
.await
|
||||
.map_err(map_shared_diesel_error)?)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user