diff --git a/Cargo.lock b/Cargo.lock index e22ecaf..f319dc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,6 +47,15 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener", +] + [[package]] name = "async-mutex" version = "1.4.0" @@ -233,6 +242,12 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +[[package]] +name = "bytecount" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205" + [[package]] name = "bytemuck" version = "1.14.0" @@ -251,6 +266,37 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +[[package]] +name = "camino" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34637b3140142bdf929fb439e8aa4ebad7651ebf7b1080b3930aa16ac1459ff" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", +] + [[package]] name = "cc" version = "1.0.83" @@ -593,6 +639,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "version_check", +] + [[package]] name = "event-listener" version = "2.5.3" @@ -1325,6 +1380,15 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "mach2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8" +dependencies = [ + "libc", +] + [[package]] name = "matchers" version = "0.1.0" @@ -1382,6 +1446,31 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "moka" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8017ec3548ffe7d4cef7ac0e12b044c01164a74c0f3119420faeaf13490ad8b" +dependencies = [ + "async-lock", + "async-trait", + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "futures-util", + "log", + "once_cell", + "parking_lot", + "quanta", + "rustc_version", + "skeptic", + "smallvec", + "tagptr", + "thiserror", + "triomphe", + "uuid", +] + [[package]] name = "multer" version = "2.1.0" @@ -1531,9 +1620,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openssl" @@ -1724,8 +1813,10 @@ dependencies = [ "fern", "headless_chrome", "log", + "moka", "nanobyte_opentelemetry", "nanobyte_tera", + "once_cell", "ovlach_data", "ovlach_tera", "phf", @@ -2020,6 +2111,33 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "pulldown-cmark" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a1a2f1f0a7ecff9c31abbe177637be0e97a0aef46cf8738ece09327985d998" +dependencies = [ + "bitflags 1.3.2", + "memchr", + "unicase", +] + +[[package]] +name = "quanta" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17e662a7a8291a865152364c20c7abc5e60486ab2001e8ec10b24862de0b9ab" +dependencies = [ + "crossbeam-utils", + "libc", + "mach2", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + [[package]] name = "quote" version = "1.0.33" @@ -2059,6 +2177,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "raw-cpuid" +version = "10.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.3.5" @@ -2310,6 +2437,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.26" @@ -2435,6 +2571,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e388332cd64eb80cd595a00941baf513caffae8dce9cfd0467fc9c66397dade6" +[[package]] +name = "semver" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +dependencies = [ + "serde", +] + [[package]] name = "serde" version = "1.0.193" @@ -2559,6 +2704,21 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "skeptic" +version = "0.13.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d23b015676c90a0f01c197bfdc786c20342c73a0afdda9025adb0bc42940a8" +dependencies = [ + "bytecount", + "cargo_metadata", + "error-chain", + "glob", + "pulldown-cmark", + "tempfile", + "walkdir", +] + [[package]] name = "slab" version = "0.4.9" @@ -2700,6 +2860,12 @@ dependencies = [ "libc", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "tempfile" version = "3.8.1" @@ -3090,6 +3256,12 @@ dependencies = [ "tracing-serde", ] +[[package]] +name = "triomphe" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3" + [[package]] name = "try-lock" version = "0.2.4" @@ -3223,6 +3395,15 @@ dependencies = [ "unic-common", ] +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.13" diff --git a/Cargo.toml b/Cargo.toml index be6de39..e09dbb4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,8 @@ phf = { version = "0.11.2", features = ["macros"] } nanobyte_tera = { version = "0.2.0", registry = "gitea_nanobyte" } ovlach_tera = { version="0.2.0", registry="gitea_ovlach" } ovlach_data = { version = "0.1.3", registry = "gitea_ovlach"} +moka = { version = "0.12.1", features = ["future", 'log'] } +once_cell = "1.19.0" [dev-dependencies] serde_json = "1.0.108" diff --git a/src/lib.rs b/src/lib.rs index da09273..9a3c64a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ use nanobyte_opentelemetry::rocket::TracingFairing; use rocket::{Rocket, Build, routes, fairing::AdHoc}; use rocket_prometheus::PrometheusMetrics; use serde::Deserialize; +use tools::cache::{build_cache, cache_prometheus_metrics}; pub mod routes; mod chromium; mod tools; @@ -26,6 +27,7 @@ pub struct DefaultPerson { pub fn rocket_builder() -> Rocket { let prometheus = PrometheusMetrics::new(); + cache_prometheus_metrics(&prometheus).expect("Failed to register prometheus cache metrics"); rocket::build() .attach(TracingFairing::ignite()) @@ -37,6 +39,7 @@ pub fn rocket_builder() -> Rocket { AdHoc::config::() ) .attach(prometheus.clone()) + .manage( build_cache()) .mount("/", routes![ routes::pdf::render_pdf_cv, routes::pdf::render_html_cv diff --git a/src/routes/pdf.rs b/src/routes/pdf.rs index 3e905c3..ec2b091 100644 --- a/src/routes/pdf.rs +++ b/src/routes/pdf.rs @@ -4,11 +4,13 @@ use nanobyte_opentelemetry::rocket::{TracingSpan, OtelReqwestClient}; use nanobyte_tera::l18n::LanguageDescription; use ovlach_data::cv::data::CV; use rocket::fs::NamedFile; +use rocket::serde::json::json; use ::rocket::{State, http::Status}; use ::rocket::get; use tempfile::NamedTempFile; use tera::Context; use tracing::{info_span, error, debug, Instrument}; +use crate::tools::cache::NanoCache; use crate::{chromium::rocket::BrowserHolder, tools::{tera::NanoTera, pdf::PdfStream, rocket::RequestLanguage}, services::cv::fetch_cv_data_from_backend, CVBackendConfig}; // TODO: request-id @@ -56,19 +58,32 @@ fn render_template(template_name: &str, file: &NamedTempFile, tera: NanoTera, cv } } -#[get("/cv///output.pdf")] +#[allow(clippy::too_many_arguments)] +#[get("/cv///output.pdf?")] pub async fn render_pdf_cv(username: &str, tera: NanoTera, tracing: TracingSpan, request_client: OtelReqwestClient, - cv_config: &State, language: RequestLanguage, browser: BrowserHolder) -> Result { + cv_config: &State, language: RequestLanguage, browser: BrowserHolder, cache: &State>, + force_rebuild: Option) -> Result { async move { match fetch_cv_data_from_backend(&cv_config.cv_backend_path, &username.to_string(), &request_client.0).await { Ok(cv_data) => { - let file = tempfile::Builder::new().suffix(".html").tempfile().unwrap(); - render_template("two_column", &file, tera, cv_data, language.language); - let span = info_span!("render_pdf", username = username); - let pdf = span.in_scope(||{ - generate_pdf(browser.browser, &file) - }); - Ok(PdfStream::new(pdf)) + // TODO: CV hasher + let data = json!(cv_data).to_string(); + match cache.get(&data, force_rebuild).await { + Some(data) => { + Ok(data.clone()) + } + None => { + let file = tempfile::Builder::new().suffix(".html").tempfile().unwrap(); + render_template("two_column", &file, tera, cv_data, language.language); + let span = info_span!("render_pdf", username = username); + let pdf = span.in_scope(||{ + generate_pdf(browser.browser, &file) + }); + let result = PdfStream::new(pdf); + cache.insert(data, result.clone()).await; + Ok(result) + } + } }, Err(e) => { error!("Error fetching cv data: {:?}", e); diff --git a/src/tools/cache.rs b/src/tools/cache.rs new file mode 100644 index 0000000..e81952c --- /dev/null +++ b/src/tools/cache.rs @@ -0,0 +1,81 @@ +use std::time::Duration; + +use moka::{future::Cache, notification::RemovalCause}; +use once_cell::sync::Lazy; +use rocket_prometheus::{prometheus::{IntCounterVec, opts, Error}, PrometheusMetrics}; +use std::hash::Hash; + +use super::pdf::PdfStream; + +static CACHE_EVICTION_COUNT: Lazy = Lazy::new(|| + IntCounterVec::new(opts!(format!("{}_{}", std::env::var("ROCKET_PROMETHEUS_NAMESPACE").unwrap_or("app".to_string()), "cache_eviction"), "Count of cache eviction"), &["cause"]) + .expect("Could not create NAME_COUNTER") +); +static CACHE_HITS: Lazy = Lazy::new(|| + IntCounterVec::new(opts!(format!("{}_{}", std::env::var("ROCKET_PROMETHEUS_NAMESPACE").unwrap_or("app".to_string()), "cache_hists"), "Count of cache eviction"), &[]).expect("Could not create NAME_COUNTER") +); +static CACHE_MISSES: Lazy = Lazy::new(|| + IntCounterVec::new(opts!(format!("{}_{}", std::env::var("ROCKET_PROMETHEUS_NAMESPACE").unwrap_or("app".to_string()), "cache_misses"), "Count of cache eviction"), &[]).expect("Could not create NAME_COUNTER") +); + +pub fn cache_prometheus_metrics(metrics: &PrometheusMetrics) -> Result<(), Error> { + metrics.registry().register(Box::new(CACHE_EVICTION_COUNT.clone()))?; + metrics.registry().register(Box::new(CACHE_HITS.clone()))?; + metrics.registry().register(Box::new(CACHE_MISSES.clone()))?; + Ok(()) +} + +pub fn build_cache() -> NanoCache { + let cache = Cache::builder() + .time_to_live(Duration::from_secs(60 * 60)) + .eviction_listener(|_k, _v, cause| { + let cause_string = match cause { + RemovalCause::Expired => "expired", + RemovalCause::Explicit => "explicit", + RemovalCause::Replaced => "replaced", + RemovalCause::Size => "size", + }; + + CACHE_EVICTION_COUNT.with_label_values(&[cause_string]).inc(); + }) + .build(); + + NanoCache::new(cache) + +} + +pub struct NanoCache { + inner: Cache, +} + +impl NanoCache +where + K: Hash + Eq + Send + Sync + 'static, + V: Clone + Send + Sync + 'static +{ + pub fn new(cache: Cache) -> Self { + Self { + inner: cache + } + } + + pub async fn get(&self, key: &K, force_invalidate: Option) -> Option { + if force_invalidate == Some(true) { + self.inner.invalidate(key).await; + CACHE_MISSES.with_label_values(&[]).inc(); + return None; + } + let result = self.inner.get(key).await; + + match result { + Some(_) => CACHE_HITS.with_label_values(&[]).inc(), + None => CACHE_MISSES.with_label_values(&[]).inc(), + } + + result + } + + pub async fn insert(&self, key: K, value: V) { + self.inner.insert(key, value).await + } +} diff --git a/src/tools/mod.rs b/src/tools/mod.rs index 5dfe3ff..6883b0c 100644 --- a/src/tools/mod.rs +++ b/src/tools/mod.rs @@ -1,3 +1,4 @@ pub mod tera; pub mod pdf; -pub(crate) mod rocket; \ No newline at end of file +pub(crate) mod rocket; +pub(crate) mod cache; diff --git a/src/tools/pdf.rs b/src/tools/pdf.rs index 84bdc60..c8876da 100644 --- a/src/tools/pdf.rs +++ b/src/tools/pdf.rs @@ -3,6 +3,7 @@ use std::io::Cursor; +#[derive(Clone)] pub struct PdfStream { data: Vec, }