use std::sync::Arc;
use std::time::Duration;
use martin_tile_utils::{Format, TileCoord};
use moka::future::Cache;
use tracing::{info, trace};
#[cfg(feature = "metrics")]
use crate::metrics::{TILE_CACHE_REQUESTS_TOTAL, ZOOM_LABELS};
use crate::tiles::Tile;
#[derive(Clone, Debug)]
pub struct TileCache(Cache<TileCacheKey, Tile>);
impl TileCache {
#[must_use]
pub fn new(
max_size_bytes: u64,
expiry: Option<Duration>,
idle_timeout: Option<Duration>,
) -> Self {
let mut builder = Cache::builder()
.name("tile_cache")
.weigher(|_key: &TileCacheKey, value: &Tile| -> u32 {
value.data.len().try_into().unwrap_or(u32::MAX)
})
.max_capacity(max_size_bytes)
.support_invalidation_closures();
if let Some(ttl) = expiry {
builder = builder.time_to_live(ttl);
}
if let Some(tti) = idle_timeout {
builder = builder.time_to_idle(tti);
}
Self(builder.build())
}
pub async fn get_or_insert<F, Fut, E>(
&self,
source_id: String,
xyz: TileCoord,
query: Option<String>,
format: Option<Format>,
compute: F,
) -> Result<Tile, Arc<E>>
where
F: FnOnce() -> Fut,
Fut: Future<Output = Result<Tile, E>>,
E: Send + Sync + 'static,
{
let key = TileCacheKey::new(source_id, xyz, query, format);
let entry = self
.0
.entry(key.clone())
.or_try_insert_with(async move { compute().await })
.await?;
if entry.is_fresh() {
#[cfg(feature = "metrics")]
TILE_CACHE_REQUESTS_TOTAL
.with_label_values(&["tile", "miss", ZOOM_LABELS[key.xyz.z as usize]])
.inc();
hotpath::gauge!("tile_cache_misses").inc(1.0);
trace!("Tile cache MISS for {key:?}");
} else {
#[cfg(feature = "metrics")]
TILE_CACHE_REQUESTS_TOTAL
.with_label_values(&["tile", "hit", ZOOM_LABELS[key.xyz.z as usize]])
.inc();
hotpath::gauge!("tile_cache_hits").inc(1.0);
trace!(
"Tile cache HIT for {key:?} (entries={entries}, size={size}B)",
entries = self.0.entry_count(),
size = self.0.weighted_size()
);
}
Ok(entry.into_value())
}
pub fn invalidate_source(&self, source_id: &str) {
let source_id_owned = source_id.to_string();
self.0
.invalidate_entries_if(move |key, _| key.source_id == source_id_owned)
.expect("invalidate_entries_if predicate should not error");
info!("Invalidated tile cache for source: {source_id}");
}
pub fn invalidate_all(&self) {
self.0.invalidate_all();
info!("Invalidated all tile cache entries");
}
#[must_use]
pub fn entry_count(&self) -> u64 {
self.0.entry_count()
}
#[must_use]
pub fn weighted_size(&self) -> u64 {
self.0.weighted_size()
}
pub async fn run_pending_tasks(&self) {
self.0.run_pending_tasks().await;
}
}
pub type OptTileCache = Option<TileCache>;
pub const NO_TILE_CACHE: OptTileCache = None;
#[derive(Debug, Hash, PartialEq, Eq, Clone)]
struct TileCacheKey {
source_id: String,
xyz: TileCoord,
query: Option<String>,
format: Option<Format>,
}
impl TileCacheKey {
fn new(
source_id: String,
xyz: TileCoord,
query: Option<String>,
format: Option<Format>,
) -> Self {
Self {
source_id,
xyz,
query,
format,
}
}
}