diff --git a/martin/benches/bench.rs b/martin/benches/bench.rs index 53ddff479..08da0d8b8 100644 --- a/martin/benches/bench.rs +++ b/martin/benches/bench.rs @@ -45,8 +45,8 @@ impl Source for NullSource { async fn get_tile( &self, - _xyz: &TileCoord, - _query: &Option, + _xyz: TileCoord, + _url_query: Option<&UrlQuery>, ) -> MartinResult { Ok(Vec::new()) } diff --git a/martin/src/args/srv.rs b/martin/src/args/srv.rs index b436151b3..f4b27dcca 100644 --- a/martin/src/args/srv.rs +++ b/martin/src/args/srv.rs @@ -10,6 +10,9 @@ pub struct SrvArgs { /// Number of web server workers #[arg(short = 'W', long)] pub workers: Option, + /// Cache size (in MB) for the tile cache + #[arg(short = 'c', long)] + pub cache_size: Option, } impl SrvArgs { @@ -24,5 +27,8 @@ impl SrvArgs { if self.workers.is_some() { srv_config.worker_processes = self.workers; } + if self.cache_size.is_some() { + srv_config.cache_size_mb = self.cache_size; + } } } diff --git a/martin/src/bin/martin-cp.rs b/martin/src/bin/martin-cp.rs index 2a2077a6a..037997246 100644 --- a/martin/src/bin/martin-cp.rs +++ b/martin/src/bin/martin-cp.rs @@ -311,7 +311,8 @@ async fn run_tile_copy(args: CopyArgs, state: ServerState) -> MartinCpResult<()> .try_for_each_concurrent(concurrency, |xyz| { let tx = tx.clone(); async move { - let tile = get_tile_content(sources, info, &xyz, query, encodings).await?; + let tile = + get_tile_content(sources, info, xyz, query, encodings, None).await?; let data = tile.data; tx.send(TileXyz { xyz, data }) .await diff --git a/martin/src/mbtiles/mod.rs b/martin/src/mbtiles/mod.rs index 756046b47..3029fed04 100644 --- a/martin/src/mbtiles/mod.rs +++ b/martin/src/mbtiles/mod.rs @@ -105,8 +105,8 @@ impl Source for MbtSource { async fn get_tile( &self, - xyz: &TileCoord, - _url_query: &Option, + xyz: TileCoord, + _url_query: Option<&UrlQuery>, ) -> MartinResult { if let Some(tile) = self .mbtiles diff --git a/martin/src/pg/configurator.rs b/martin/src/pg/configurator.rs index bbd8384c2..79d86ccdb 100644 --- a/martin/src/pg/configurator.rs +++ b/martin/src/pg/configurator.rs @@ -214,7 +214,7 @@ impl PgBuilder { continue; } Ok((id, pg_sql, src_inf)) => { - debug!("{id} query: {}", pg_sql.query); + debug!("{id} query: {}", pg_sql.sql_query); self.add_func_src(&mut res, id.clone(), &src_inf, pg_sql.clone()); info_map.insert(id, src_inf); } @@ -252,7 +252,7 @@ impl PgBuilder { warn_on_rename(id, &id2, "Function"); let signature = &pg_sql.signature; info!("Configured {dup}source {id2} from the function {signature}"); - debug!("{id2} query: {}", pg_sql.query); + debug!("{id2} query: {}", pg_sql.sql_query); info_map.insert(id2, merged_inf); } @@ -285,7 +285,7 @@ impl PgBuilder { let id2 = self.resolve_id(&source_id, &db_inf); self.add_func_src(&mut res, id2.clone(), &db_inf, pg_sql.clone()); info!("Discovered source {id2} from function {}", pg_sql.signature); - debug!("{id2} query: {}", pg_sql.query); + debug!("{id2} query: {}", pg_sql.sql_query); info_map.insert(id2, db_inf); } } @@ -302,11 +302,11 @@ impl PgBuilder { &self, sources: &mut TileInfoSources, id: String, - info: &impl PgInfo, - sql: PgSqlInfo, + pg_info: &impl PgInfo, + sql_info: PgSqlInfo, ) { - let tilejson = info.to_tilejson(id.clone()); - let source = PgSource::new(id, sql, tilejson, self.pool.clone()); + let tilejson = pg_info.to_tilejson(id.clone()); + let source = PgSource::new(id, sql_info, tilejson, self.pool.clone()); sources.push(Box::new(source)); } } diff --git a/martin/src/pg/errors.rs b/martin/src/pg/errors.rs index cd26417c8..2b49bc7ef 100644 --- a/martin/src/pg/errors.rs +++ b/martin/src/pg/errors.rs @@ -61,6 +61,6 @@ pub enum PgError { #[error(r#"Unable to get tile {2:#} from {1}: {0}"#)] GetTileError(#[source] TokioPgError, String, TileCoord), - #[error(r#"Unable to get tile {2:#} with {:?} params from {1}: {0}"#, query_to_json(.3))] - GetTileWithQueryError(#[source] TokioPgError, String, TileCoord, UrlQuery), + #[error(r#"Unable to get tile {2:#} with {:?} params from {1}: {0}"#, query_to_json(.3.as_ref()))] + GetTileWithQueryError(#[source] TokioPgError, String, TileCoord, Option), } diff --git a/martin/src/pg/pg_source.rs b/martin/src/pg/pg_source.rs index dc2cc1198..f639e3d5b 100644 --- a/martin/src/pg/pg_source.rs +++ b/martin/src/pg/pg_source.rs @@ -1,5 +1,3 @@ -use std::collections::HashMap; - use async_trait::async_trait; use deadpool_postgres::tokio_postgres::types::{ToSql, Type}; use log::debug; @@ -58,35 +56,32 @@ impl Source for PgSource { async fn get_tile( &self, - xyz: &TileCoord, - url_query: &Option, + xyz: TileCoord, + url_query: Option<&UrlQuery>, ) -> MartinResult { - let empty_query = HashMap::new(); - let url_query = url_query.as_ref().unwrap_or(&empty_query); let conn = self.pool.get().await?; - let param_types: &[Type] = if self.support_url_query() { &[Type::INT2, Type::INT8, Type::INT8, Type::JSON] } else { &[Type::INT2, Type::INT8, Type::INT8] }; - let query = &self.info.query; + let sql = &self.info.sql_query; let prep_query = conn - .prepare_typed_cached(query, param_types) + .prepare_typed_cached(sql, param_types) .await .map_err(|e| { PrepareQueryError( e, self.id.to_string(), self.info.signature.to_string(), - self.info.query.to_string(), + self.info.sql_query.to_string(), ) })?; let tile = if self.support_url_query() { let json = query_to_json(url_query); - debug!("SQL: {query} [{xyz}, {json:?}]"); + debug!("SQL: {sql} [{xyz}, {json:?}]"); let params: &[&(dyn ToSql + Sync)] = &[ &i16::from(xyz.z), &i64::from(xyz.x), @@ -95,7 +90,7 @@ impl Source for PgSource { ]; conn.query_opt(&prep_query, params).await } else { - debug!("SQL: {query} [{xyz}]"); + debug!("SQL: {sql} [{xyz}]"); conn.query_opt( &prep_query, &[&i16::from(xyz.z), &i64::from(xyz.x), &i64::from(xyz.y)], @@ -107,9 +102,9 @@ impl Source for PgSource { .map(|row| row.and_then(|r| r.get::<_, Option>(0))) .map_err(|e| { if self.support_url_query() { - GetTileWithQueryError(e, self.id.to_string(), *xyz, url_query.clone()) + GetTileWithQueryError(e, self.id.to_string(), xyz, url_query.cloned()) } else { - GetTileError(e, self.id.to_string(), *xyz) + GetTileError(e, self.id.to_string(), xyz) } })? .unwrap_or_default(); @@ -120,7 +115,7 @@ impl Source for PgSource { #[derive(Clone, Debug)] pub struct PgSqlInfo { - pub query: String, + pub sql_query: String, pub use_url_query: bool, pub signature: String, } @@ -129,7 +124,7 @@ impl PgSqlInfo { #[must_use] pub fn new(query: String, has_query_params: bool, signature: String) -> Self { Self { - query, + sql_query: query, use_url_query: has_query_params, signature, } diff --git a/martin/src/pg/utils.rs b/martin/src/pg/utils.rs index 7733d9b61..1e6a8ec87 100755 --- a/martin/src/pg/utils.rs +++ b/martin/src/pg/utils.rs @@ -85,13 +85,15 @@ pub fn patch_json(target: TileJSON, patch: &Option) -> TileJS } #[must_use] -pub fn query_to_json(query: &UrlQuery) -> Json> { +pub fn query_to_json(query: Option<&UrlQuery>) -> Json> { let mut query_as_json = HashMap::new(); - for (k, v) in query { - let json_value: serde_json::Value = - serde_json::from_str(v).unwrap_or_else(|_| serde_json::Value::String(v.clone())); + if let Some(query) = query { + for (k, v) in query { + let json_value: serde_json::Value = + serde_json::from_str(v).unwrap_or_else(|_| serde_json::Value::String(v.clone())); - query_as_json.insert(k.clone(), json_value); + query_as_json.insert(k.clone(), json_value); + } } Json(query_as_json) diff --git a/martin/src/pmtiles/mod.rs b/martin/src/pmtiles/mod.rs index 04acdd3ab..cb1fa263a 100644 --- a/martin/src/pmtiles/mod.rs +++ b/martin/src/pmtiles/mod.rs @@ -264,8 +264,8 @@ macro_rules! impl_pmtiles_source { async fn get_tile( &self, - xyz: &TileCoord, - _url_query: &Option, + xyz: TileCoord, + _url_query: Option<&UrlQuery>, ) -> MartinResult { // TODO: optimize to return Bytes if let Some(t) = self diff --git a/martin/src/source.rs b/martin/src/source.rs index 7a35bf0df..01c2bc2b5 100644 --- a/martin/src/source.rs +++ b/martin/src/source.rs @@ -113,7 +113,11 @@ pub trait Source: Send + Debug { false } - async fn get_tile(&self, xyz: &TileCoord, query: &Option) -> MartinResult; + async fn get_tile( + &self, + xyz: TileCoord, + url_query: Option<&UrlQuery>, + ) -> MartinResult; fn is_valid_zoom(&self, zoom: u8) -> bool { let tj = self.get_tilejson(); diff --git a/martin/src/srv/config.rs b/martin/src/srv/config.rs index 0db8db587..0c47e5098 100644 --- a/martin/src/srv/config.rs +++ b/martin/src/srv/config.rs @@ -9,6 +9,7 @@ pub struct SrvConfig { pub keep_alive: Option, pub listen_addresses: Option, pub worker_processes: Option, + pub cache_size_mb: Option, } #[cfg(test)] @@ -25,12 +26,14 @@ mod tests { keep_alive: 75 listen_addresses: '0.0.0.0:3000' worker_processes: 8 + tile_cache_size_mb: 100 "}) .unwrap(), SrvConfig { keep_alive: Some(75), listen_addresses: some("0.0.0.0:3000"), worker_processes: Some(8), + cache_size_mb: Some(100), } ); } diff --git a/martin/src/srv/server.rs b/martin/src/srv/server.rs index ea173a38e..b26fdf6bd 100755 --- a/martin/src/srv/server.rs +++ b/martin/src/srv/server.rs @@ -19,6 +19,7 @@ use futures::future::try_join_all; use itertools::Itertools as _; use log::error; use martin_tile_utils::{Encoding, Format, TileInfo}; +use moka::future::Cache; use serde::{Deserialize, Serialize}; use tilejson::{tilejson, TileJSON}; @@ -31,7 +32,7 @@ use crate::sprites::{SpriteCatalog, SpriteError, SpriteSources}; use crate::srv::config::{SrvConfig, KEEP_ALIVE_DEFAULT, LISTEN_ADDRESSES_DEFAULT}; use crate::utils::{decode_brotli, decode_gzip, encode_brotli, encode_gzip}; use crate::MartinError::BindingError; -use crate::{MartinResult, Tile, TileCoord}; +use crate::{MartinResult, Tile, TileCoord, TileData}; /// List of keywords that cannot be used as source IDs. Some of these are reserved for future use. /// Reserved keywords must never end in a "dot number" (e.g. ".1"). @@ -47,6 +48,18 @@ static SUPPORTED_ENCODINGS: &[HeaderEnc] = &[ HeaderEnc::identity(), ]; +#[derive(Debug, Hash, PartialEq, Eq)] +pub enum CacheItemKey { + TileInfo(String), + Tile(String, TileCoord), + TileWithQueryInfo(String, String), + TileWithQuery(String, TileCoord, String), + Sprite(String), + Font(String), +} + +type ItemCache = Cache; + #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] pub struct Catalog { pub tiles: TileCatalog, @@ -337,6 +350,7 @@ async fn get_tile( req: HttpRequest, path: Path, sources: Data, + cache: Data>, ) -> ActixResult { let xyz = TileCoord { z: path.z, @@ -347,8 +361,8 @@ async fn get_tile( let source_ids = &path.source_ids; let query = req.query_string(); let encodings = req.get_header::(); - - get_tile_response(sources.as_ref(), xyz, source_ids, query, encodings).await + let cache = cache.as_ref().as_ref(); + get_tile_response(sources.as_ref(), xyz, source_ids, query, encodings, cache).await } pub async fn get_tile_response( @@ -357,11 +371,20 @@ pub async fn get_tile_response( source_ids: &str, query: &str, encodings: Option, + cache: Option<&ItemCache>, ) -> ActixResult { let (sources, use_url_query, info) = sources.get_sources(source_ids, Some(xyz.z))?; let query = use_url_query.then_some(query); - let tile = get_tile_content(sources.as_slice(), info, &xyz, query, encodings.as_ref()).await?; + let tile = get_tile_content( + sources.as_slice(), + info, + xyz, + query, + encodings.as_ref(), + cache, + ) + .await?; Ok(if tile.data.is_empty() { HttpResponse::NoContent().finish() @@ -378,21 +401,27 @@ pub async fn get_tile_response( pub async fn get_tile_content( sources: &[&dyn Source], info: TileInfo, - xyz: &TileCoord, + xyz: TileCoord, query: Option<&str>, encodings: Option<&AcceptEncoding>, + cache: Option<&ItemCache>, ) -> ActixResult { if sources.is_empty() { return Err(ErrorNotFound("No valid sources found")); } - let query = match query { - Some(v) if !v.is_empty() => Some(Query::::from_query(v)?.into_inner()), - _ => None, + let query_str = query.filter(|v| !v.is_empty()); + let query = match query_str { + Some(v) => Some(Query::::from_query(v)?.into_inner()), + None => None, }; - let mut tiles = try_join_all(sources.iter().map(|s| s.get_tile(xyz, &query))) - .await - .map_err(map_internal_error)?; + let mut tiles = try_join_all( + sources + .iter() + .map(|s| get_cached_tile(cache, *s, xyz, query_str, query.as_ref())), + ) + .await + .map_err(map_internal_error)?; let mut layer_count = 0; let mut last_non_empty_layer = 0; @@ -429,6 +458,33 @@ pub async fn get_tile_content( Ok(tile) } +async fn get_cached_tile( + cache: Option<&ItemCache>, + source: &dyn Source, + xyz: TileCoord, + query_str: Option<&str>, + query: Option<&UrlQuery>, +) -> MartinResult { + if let Some(cache) = cache { + let id = source.get_id().to_owned(); + let key = if let Some(query_str) = query_str { + CacheItemKey::TileWithQuery(id, xyz, query_str.to_owned()) + } else { + CacheItemKey::Tile(id, xyz) + }; + if let Some(data) = cache.get(&key).await { + Ok(data) + } else { + let data = source.get_tile(xyz, query).await?; + cache.insert(key, data.clone()).await; + Ok(data) + } + } else { + // cache is disabled + source.get_tile(xyz, query).await + } +} + fn recompress(mut tile: Tile, accept_enc: Option<&AcceptEncoding>) -> ActixResult { if let Some(accept_enc) = accept_enc { if tile.info.encoding.is_encoded() { @@ -530,7 +586,22 @@ pub fn new_server(config: SrvConfig, state: ServerState) -> MartinResult<(Server .allow_any_origin() .allowed_methods(vec!["GET"]); - let app = App::new().app_data(Data::new(state.tiles.clone())); + let cache_size = config.cache_size_mb.unwrap_or(100) * 1024 * 1024; + let cache = if cache_size > 0 { + Some( + ItemCache::builder() + .weigher(|_key, value: &Vec| -> u32 { + value.len().try_into().unwrap_or(u32::MAX) + }) + .max_capacity(cache_size) + .build(), + ) + } else { + None + }; + let app = App::new() + .app_data(Data::new(state.tiles.clone())) + .app_data(Data::new(cache)); #[cfg(feature = "sprites")] let app = app.app_data(Data::new(state.sprites.clone())); @@ -591,8 +662,8 @@ mod tests { async fn get_tile( &self, - _xyz: &TileCoord, - _url_query: &Option, + _xyz: TileCoord, + _url_query: Option<&UrlQuery>, ) -> MartinResult { Ok(self.data.clone()) } @@ -697,7 +768,7 @@ mod tests { let xyz = TileCoord { z: 0, x: 0, y: 0 }; assert_eq!( expected, - &get_tile_content(src.as_slice(), info, &xyz, None, None) + &get_tile_content(src.as_slice(), info, &xyz, None, None, &None) .await .unwrap() .data diff --git a/martin/src/utils/xyz.rs b/martin/src/utils/xyz.rs index 421ec6df6..1a968207e 100644 --- a/martin/src/utils/xyz.rs +++ b/martin/src/utils/xyz.rs @@ -1,6 +1,6 @@ use std::fmt::{Display, Formatter}; -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] pub struct TileCoord { pub z: u8, pub x: u32,