use std::sync::Arc;
use std::time::Duration;
use actix_web::web::Bytes;
use moka::future::Cache;
use tracing::{info, trace};
#[derive(Clone)]
pub struct SpriteCache {
cache: Cache<SpriteCacheKey, Bytes>,
}
impl std::fmt::Debug for SpriteCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SpriteCache")
.field("entry_count", &self.cache.entry_count())
.field("weighted_size", &self.cache.weighted_size())
.finish()
}
}
impl SpriteCache {
#[must_use]
pub fn new(
max_size_bytes: u64,
expiry: Option<Duration>,
idle_timeout: Option<Duration>,
) -> Self {
let mut builder = Cache::builder()
.name("sprite_cache")
.weigher(|key: &SpriteCacheKey, value: &Bytes| -> u32 {
size_of_val(key).try_into().unwrap_or(u32::MAX)
+ 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, Fut, E>(
&self,
ids: String,
as_sdf: bool,
as_json: bool,
compute: F,
) -> Result<Bytes, Arc<E>>
where
F: FnOnce() -> Fut,
Fut: Future<Output = Result<Bytes, E>>,
E: Send + Sync + 'static,
{
let key = SpriteCacheKey::new(ids, as_sdf, as_json);
let entry = self
.cache
.entry(key.clone())
.or_try_insert_with(async move { compute().await })
.await?;
if entry.is_fresh() {
hotpath::gauge!("sprite_cache_misses").inc(1.0);
trace!("Sprite cache MISS for {key:?}");
} else {
hotpath::gauge!("sprite_cache_hits").inc(1.0);
trace!(
"Sprite cache HIT for {key:?} (entries={}, size={})",
self.cache.entry_count(),
self.cache.weighted_size()
);
}
Ok(entry.into_value())
}
pub fn invalidate_source(&self, source_id: &str) {
let source_id_owned = source_id.to_string();
self.cache
.invalidate_entries_if(move |key, _| key.ids.contains(&source_id_owned))
.expect("invalidate_entries_if predicate should not error");
info!("Invalidated sprite cache for source: {source_id}");
}
pub fn invalidate_all(&self) {
self.cache.invalidate_all();
info!("Invalidated all sprite 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;
}
}
pub type OptSpriteCache = Option<SpriteCache>;
pub const NO_SPRITE_CACHE: OptSpriteCache = None;
#[derive(Debug, Hash, PartialEq, Eq, Clone)]
struct SpriteCacheKey {
ids: String,
as_sdf: bool,
as_json: bool,
}
impl SpriteCacheKey {
fn new(ids: String, as_sdf: bool, as_json: bool) -> Self {
Self {
ids,
as_sdf,
as_json,
}
}
}