feat: add caches
This commit is contained in:
@@ -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<Build> {
|
||||
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<Build> {
|
||||
AdHoc::config::<DefaultPerson>()
|
||||
)
|
||||
.attach(prometheus.clone())
|
||||
.manage( build_cache())
|
||||
.mount("/", routes![
|
||||
routes::pdf::render_pdf_cv,
|
||||
routes::pdf::render_html_cv
|
||||
|
||||
@@ -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/<username>/<language>/output.pdf")]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[get("/cv/<username>/<language>/output.pdf?<force_rebuild>")]
|
||||
pub async fn render_pdf_cv(username: &str, tera: NanoTera, tracing: TracingSpan, request_client: OtelReqwestClient,
|
||||
cv_config: &State<CVBackendConfig>, language: RequestLanguage, browser: BrowserHolder) -> Result<PdfStream, Status> {
|
||||
cv_config: &State<CVBackendConfig>, language: RequestLanguage, browser: BrowserHolder, cache: &State<NanoCache<String, PdfStream>>,
|
||||
force_rebuild: Option<bool>) -> Result<PdfStream, Status> {
|
||||
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);
|
||||
|
||||
81
src/tools/cache.rs
Normal file
81
src/tools/cache.rs
Normal file
@@ -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<IntCounterVec> = 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<IntCounterVec> = 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<IntCounterVec> = 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<String, PdfStream> {
|
||||
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<K, V> {
|
||||
inner: Cache<K, V>,
|
||||
}
|
||||
|
||||
impl<K, V> NanoCache<K, V>
|
||||
where
|
||||
K: Hash + Eq + Send + Sync + 'static,
|
||||
V: Clone + Send + Sync + 'static
|
||||
{
|
||||
pub fn new(cache: Cache<K, V>) -> Self {
|
||||
Self {
|
||||
inner: cache
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get(&self, key: &K, force_invalidate: Option<bool>) -> Option<V> {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod tera;
|
||||
pub mod pdf;
|
||||
pub(crate) mod rocket;
|
||||
pub(crate) mod rocket;
|
||||
pub(crate) mod cache;
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::io::Cursor;
|
||||
|
||||
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PdfStream {
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user