Skip to content

Commit a06f887

Browse files
committed
Introduce basic caching mechanism
1 parent f14cbdb commit a06f887

File tree

8 files changed

+123
-24
lines changed

8 files changed

+123
-24
lines changed

engine/Cargo.lock

Lines changed: 20 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

engine/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,5 @@ rust-s3 = { version = "0.36.0-beta.2", default-features = false, features = [
5858
aws-sdk-s3 = "1.64.0"
5959
aws-config = "1.5.10"
6060
async_zip = { version = "0.0.17", features = ["bzip2", "lzma", "zstd", "xz", "deflate"] }
61+
dashmap = { version = "6.1.0", features = ["serde"] }
62+
serde_json = "1.0.138"

engine/src/cache/mod.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use chrono::{DateTime, Duration, Utc};
2+
use dashmap::DashMap;
3+
use futures::future::{BoxFuture, Shared};
4+
use tracing::info;
5+
6+
#[derive(Debug)]
7+
pub struct Cache {
8+
pub raw: DashMap<String, Shared<BoxFuture<'static, CachedValue<serde_json::Value>>>>,
9+
}
10+
11+
impl Cache {
12+
pub fn new() -> Self {
13+
Self {
14+
raw: DashMap::new(),
15+
}
16+
}
17+
18+
pub async fn has(&self, key: &str) -> Option<serde_json::Value> {
19+
if let Some(x) = self.raw.get(key) {
20+
let x = x.clone().await;
21+
info!("Cache hit: {}", key);
22+
23+
if x.is_expired() {
24+
self.raw.remove(key);
25+
return None;
26+
}
27+
28+
Some(x.value)
29+
} else {
30+
info!("Cache miss: {}", key);
31+
None
32+
}
33+
}
34+
}
35+
36+
#[derive(Debug, Clone, Copy)]
37+
pub struct CachedValue<T> {
38+
pub value: T,
39+
pub expires_at: DateTime<Utc>,
40+
}
41+
42+
impl<T> CachedValue<T> {
43+
pub fn new(value: T, expires_at: DateTime<Utc>) -> Self {
44+
Self { value, expires_at }
45+
}
46+
47+
pub fn new_with_ttl(value: T, ttl: Duration) -> Self {
48+
Self {
49+
value,
50+
expires_at: Utc::now() + ttl,
51+
}
52+
}
53+
}
54+
55+
impl<T> CachedValue<T> {
56+
pub fn is_expired(&self) -> bool {
57+
self.expires_at < Utc::now()
58+
}
59+
}

engine/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub mod routes;
88
pub mod state;
99
pub mod storage;
1010
pub mod utils;
11+
pub mod cache;
1112

1213
#[async_std::main]
1314
async fn main() {

engine/src/middlewares/auth.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ impl UserAuth {
143143
pub async fn required_member_of(&self, team_id: impl AsRef<str>) -> Result<(), HttpError> {
144144
match self {
145145
UserAuth::User(session, state) => {
146-
if !Team::is_member(&state.database, &team_id, &session.user_id)
146+
if !Team::is_member(&state, &team_id, &session.user_id)
147147
.await
148148
.map_err(HttpError::from)?
149149
{

engine/src/models/team/mod.rs

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
use chrono::{DateTime, Utc};
1+
use chrono::{DateTime, Duration, Utc};
2+
use futures::{future::Shared, FutureExt};
23
use poem_openapi::Object;
34
use serde::{Deserialize, Serialize};
45
use sqlx::{query, query_as, query_scalar};
56

67
use crate::{
7-
database::Database, middlewares::auth::AccessibleResource, models::user::User, routes::error::HttpError, state::State, utils::id::{generate_id, IdType}
8+
cache::CachedValue, database::Database, middlewares::auth::AccessibleResource, models::user::User, routes::error::HttpError, state::State, utils::id::{generate_id, IdType}
89
};
910

1011
pub mod invite;
@@ -61,16 +62,10 @@ impl Team {
6162
.await
6263
}
6364

64-
pub async fn delete_by_id(
65-
db: &Database,
66-
team_id: impl AsRef<str>,
67-
) -> Result<(), sqlx::Error> {
68-
query!(
69-
"DELETE FROM teams WHERE team_id = $1",
70-
team_id.as_ref()
71-
)
72-
.execute(&db.pool)
73-
.await?;
65+
pub async fn delete_by_id(db: &Database, team_id: impl AsRef<str>) -> Result<(), sqlx::Error> {
66+
query!("DELETE FROM teams WHERE team_id = $1", team_id.as_ref())
67+
.execute(&db.pool)
68+
.await?;
7469

7570
Ok(())
7671
}
@@ -92,18 +87,33 @@ impl Team {
9287
}
9388

9489
pub async fn is_member(
95-
db: &Database,
90+
state: &State,
9691
team_id: impl AsRef<str>,
9792
user_id: impl AsRef<str>,
9893
) -> Result<bool, sqlx::Error> {
99-
Ok(query_scalar!(
94+
let cache_key = format!("team:{}:member:{}", team_id.as_ref(), user_id.as_ref());
95+
96+
if let Some(x) = state.cache.has(&cache_key).await {
97+
return Ok(x.as_bool().unwrap_or(false));
98+
}
99+
100+
let x = query_scalar!(
100101
"SELECT EXISTS (SELECT 1 FROM user_teams WHERE team_id = $1 AND user_id = $2) OR EXISTS (SELECT 1 FROM teams WHERE team_id = $1 AND owner_id = $2)",
101102
team_id.as_ref(),
102103
user_id.as_ref()
103104
)
104-
.fetch_one(&db.pool)
105+
.fetch_one(&state.database.pool)
105106
.await?
106-
.unwrap_or(false))
107+
.unwrap_or(false);
108+
109+
state.cache.raw.insert(
110+
cache_key,
111+
async move {
112+
CachedValue::new_with_ttl(serde_json::Value::from(x), Duration::seconds(30))
113+
}.boxed().shared(),
114+
);
115+
116+
Ok(x)
107117
}
108118

109119
pub async fn get_members(
@@ -124,7 +134,9 @@ pub struct TeamId<'a>(pub &'a str);
124134

125135
impl<'a> AccessibleResource for TeamId<'a> {
126136
async fn has_access_to(&self, state: &State, user_id: &str) -> Result<bool, HttpError> {
127-
let x = Team::is_member(&state.database, self.0, user_id).await.map_err(HttpError::from)?;
137+
let x = Team::is_member(&state, self.0, user_id)
138+
.await
139+
.map_err(HttpError::from)?;
128140

129141
Ok(x)
130142
}

engine/src/state.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::sync::Arc;
33
use config::{Config, Environment};
44
use serde::Deserialize;
55

6-
use crate::{database::Database, storage::Storage};
6+
use crate::{cache::Cache, database::Database, storage::Storage};
77

88
pub type State = Arc<AppState>;
99

@@ -12,6 +12,7 @@ pub struct AppState {
1212
pub config: AppConfig,
1313
pub database: Database,
1414
pub storage: Storage,
15+
pub cache: Cache,
1516
}
1617

1718
#[derive(Deserialize, Debug)]
@@ -38,6 +39,13 @@ impl AppState {
3839

3940
let storage = Storage::from_config(&config);
4041

41-
Ok(Self { config, database, storage })
42+
let cache = Cache::new();
43+
44+
Ok(Self {
45+
config,
46+
database,
47+
storage,
48+
cache,
49+
})
4250
}
4351
}

web/src/util/query.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const queryClient = new QueryClient({
77
defaultOptions: {
88
queries: {
99
gcTime: 1000 * 60 * 60 * 24, // 24 hours
10-
staleTime: 1000 * 20, // 20 seconds
10+
staleTime: 1000 * 5, // 5 seconds
1111
retry: 0,
1212
},
1313
},

0 commit comments

Comments
 (0)