diff --git a/src/cache.rs b/src/cache.rs new file mode 100644 index 0000000..b4ff271 --- /dev/null +++ b/src/cache.rs @@ -0,0 +1,116 @@ +use std::collections::HashMap; + +use rocket::tokio::sync::RwLock; + +#[macro_export] +macro_rules! use_api_cache { + ($route:literal, $id:ident, $cache:ident) => { + if let Some(ret) = $cache.get_only($route, $id).await { + return Ok(serde_json::from_str(&ret).unwrap()); + } + }; +} + +pub struct RouteCache { + inner: RwLock>>>, +} + +impl RouteCache { + pub fn new() -> Self { + Self { + inner: RwLock::new(HashMap::new()), + } + } + + pub async fn get(&self, route: &str, id: &str, generator: F) -> String + where + F: FnOnce() -> Fut, + Fut: std::future::Future, + { + { + // Try to get a read lock first. + let lock = self.inner.read().await; + if let Some(inner_map) = lock.get(route) { + if let Some(cached_value) = inner_map.get(id) { + log::trace!("Using cached value for {route} / {id}"); + return cached_value.clone().unwrap(); + } + } + } + + // If the value was not found, acquire a write lock to insert the computed value. + let mut lock = self.inner.write().await; + + log::trace!("Computing value for {route} / {id}"); + let computed = generator().await; + + lock.entry(route.to_string()) + .or_insert_with(HashMap::new) + .insert(id.to_string(), Some(computed.clone())); + + computed + } + + pub async fn get_only(&self, route: &str, id: &str) -> Option { + let lock = self.inner.read().await; + if let Some(inner_map) = lock.get(route) { + if let Some(cached_value) = inner_map.get(id) { + log::trace!("Using cached value for {route} / {id}"); + return cached_value.clone(); + } + } + + None + } + + pub async fn get_option(&self, route: &str, id: &str, generator: F) -> Option + where + F: FnOnce() -> Fut, + Fut: std::future::Future>, + { + { + // Try to get a read lock first. + let lock = self.inner.read().await; + if let Some(inner_map) = lock.get(route) { + if let Some(cached_value) = inner_map.get(id) { + log::trace!("Using cached value for {route} / {id}"); + return cached_value.clone(); + } + } + } + + // If the value was not found, acquire a write lock to insert the computed value. + let mut lock = self.inner.write().await; + + log::trace!("Computing value for {route} / {id}"); + let computed = generator().await; + + lock.entry(route.to_string()) + .or_insert_with(HashMap::new) + .insert(id.to_string(), computed.clone()); + + computed + } + + pub async fn insert(&self, route: &str, id: &str, value: String) { + let mut lock = self.inner.write().await; + + log::trace!("Inserting value for {route} / {id}"); + + lock.entry(route.to_string()) + .or_insert_with(HashMap::new) + .insert(id.to_string(), Some(value)); + } + + pub async fn invalidate(&self, route: &str, id: &str) { + let mut lock = self.inner.write().await; + if let Some(inner_map) = lock.get_mut(route) { + inner_map.remove(id); + + // If the inner map is empty, remove the route entry as well. + if inner_map.is_empty() { + lock.remove(route); + } + } + } +} diff --git a/src/main.rs b/src/main.rs index fab5cb7..90cb2b1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ use library::Libary; +mod cache; mod library; mod route; @@ -30,6 +31,8 @@ async fn rocket() -> _ { User::create("admin", "admin", UserRole::Admin).await; + let cache = cache::RouteCache::new(); + rocket::build() .mount( "/", @@ -49,5 +52,6 @@ async fn rocket() -> _ { ], ) .manage(lib) + .manage(cache) .attach(cors) } diff --git a/src/route/album.rs b/src/route/album.rs index a385a26..8741d45 100644 --- a/src/route/album.rs +++ b/src/route/album.rs @@ -9,9 +9,11 @@ use rocket::fs::NamedFile; use rocket::*; use serde_json::json; +use crate::cache::RouteCache; use crate::library::album::Album; use crate::library::Libary; use crate::route::to_api; +use crate::use_api_cache; #[get("/artist//albums")] pub async fn albums_route(artist_id: &str, lib: &State) -> FallibleApiResponse { @@ -37,13 +39,31 @@ fn sort_by_tracknumber(a: &serde_json::Value, b: &serde_json::Value) -> Ordering } #[get("/album//cover")] -pub async fn album_cover_route(album_id: &str, lib: &State) -> Option { - let album = lib.get_album_by_id(album_id).await?; - NamedFile::open(album.get_cover().await?).await.ok() +pub async fn album_cover_route( + album_id: &str, + lib: &State, + cache: &State, +) -> Option { + NamedFile::open( + cache + .get_option("album_cover_route", album_id, || async { + let album = lib.get_album_by_id(album_id).await?; + album.get_cover().await + }) + .await?, + ) + .await + .ok() } #[get("/album/")] -pub async fn album_route(album_id: &str, lib: &State) -> FallibleApiResponse { +pub async fn album_route( + album_id: &str, + lib: &State, + cache: &State, +) -> FallibleApiResponse { + use_api_cache!("album_route", album_id, cache); + let album = lib .get_album_by_id(album_id) .await @@ -69,5 +89,13 @@ pub async fn album_route(album_id: &str, lib: &State) -> FallibleApiResp .unwrap() .insert("tracks".into(), tracks.into()); + cache + .insert( + "album_route", + album_id, + serde_json::to_string(&album).unwrap(), + ) + .await; + Ok(album) } diff --git a/src/route/artist.rs b/src/route/artist.rs index 9144fad..c64443c 100644 --- a/src/route/artist.rs +++ b/src/route/artist.rs @@ -7,6 +7,7 @@ use mongod::Model; use mongodb::bson::doc; use rocket::*; +use crate::cache::RouteCache; use crate::library::artist::Artist; use crate::library::Libary; @@ -18,9 +19,14 @@ pub async fn artists_route(lib: &State) -> FallibleApiResponse { } #[get("/artist//image")] -pub async fn artist_image_route(id: &str) -> Option { - let image = Artist::get_image_of(id).await?; - NamedFile::open(image).await.ok() +pub async fn artist_image_route(id: &str, cache: &State) -> Option { + let image = cache + .get_option("artist_image_route", id, || async { + Artist::get_image_of(id).await + }) + .await; + + NamedFile::open(image?).await.ok() } #[get("/artist/")]