use std::sync::Arc;
use std::time::Duration;
use moka::future::Cache;
use tracing::{info, trace};
#[cfg(feature = "metrics")]
use crate::metrics::CACHE_REQUESTS_TOTAL;
pub type OptFontCache = Option<FontCache>;
pub const NO_FONT_CACHE: OptFontCache = None;
#[derive(Clone, Debug)]
pub struct FontCache {
cache: Cache<FontCacheKey, Vec<u8>>,
}
impl FontCache {
#[must_use]
pub fn new(
max_size_bytes: u64,
expiry: Option<Duration>,
idle_timeout: Option<Duration>,
) -> Self {
let mut builder = Cache::builder()
.name("font_cache")
.weigher(|_key: &FontCacheKey, value: &Vec<u8>| -> u32 {
value.len().try_into().unwrap_or(u32::MAX)
})
.max_capacity(max_size_bytes);
if let Some(ttl) = expiry {
builder = builder.time_to_live(ttl);
}
if let Some(tti) = idle_timeout {
builder = builder.time_to_idle(tti);
}
Self {
cache: builder.build(),
}
}
pub async fn get_or_insert<F, E>(
&self,
ids: String,
start: u32,
end: u32,
compute: F,
) -> Result<Vec<u8>, Arc<E>>
where
F: FnOnce() -> Result<Vec<u8>, E>,
E: Send + Sync + 'static,
{
let key = FontCacheKey::new(ids, start, end);
let entry = self
.cache
.entry(key.clone())
.or_try_insert_with(async move { compute() })
.await?;
if entry.is_fresh() {
#[cfg(feature = "metrics")]
CACHE_REQUESTS_TOTAL
.with_label_values(&["font", "miss"])
.inc();
hotpath::gauge!("font_cache_misses").inc(1.0);
trace!("Font cache MISS for {key:?}");
} else {
#[cfg(feature = "metrics")]
CACHE_REQUESTS_TOTAL
.with_label_values(&["font", "hit"])
.inc();
hotpath::gauge!("font_cache_hits").inc(1.0);
trace!(
"Font cache HIT for {key:?} (entries={}, size={})",
self.cache.entry_count(),
self.cache.weighted_size()
);
}
Ok(entry.into_value())
}
pub fn invalidate_font(&self, font_id: &str) {
let font_id_owned = font_id.to_string();
self.cache
.invalidate_entries_if(move |key, _| key.ids.contains(&font_id_owned))
.expect("invalidate_entries_if predicate should not error");
info!("Invalidated font cache for font: {font_id}");
}
pub fn invalidate_all(&self) {
self.cache.invalidate_all();
info!("Invalidated all font cache entries");
}
#[must_use]
pub fn entry_count(&self) -> u64 {
self.cache.entry_count()
}
#[must_use]
pub fn weighted_size(&self) -> u64 {
self.cache.weighted_size()
}
pub async fn run_pending_tasks(&self) {
self.cache.run_pending_tasks().await;
}
}
#[derive(Debug, Hash, PartialEq, Eq, Clone)]
struct FontCacheKey {
ids: String,
start: u32,
end: u32,
}
impl FontCacheKey {
fn new(ids: String, start: u32, end: u32) -> Self {
Self { ids, start, end }
}
}