use axum::http::{HeaderMap, HeaderValue};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HttpCacheConfig {
#[serde(default = "default_enabled")]
pub enabled: bool,
#[serde(default = "default_enabled")]
dynamic: bool,
#[serde(default = "default_get_tip_info_cache_control")]
pub get_tip_info: String,
#[serde(default = "default_get_header_by_height_cache_control")]
pub get_header_by_height: String,
#[serde(default = "default_get_utxos_by_block_cache_control")]
pub get_utxos_by_block: String,
#[serde(default = "default_sync_utxos_by_block_cache_control")]
pub sync_utxos_by_block: String,
#[serde(default = "default_cache_control")]
pub get_height_at_time: String,
#[serde(default = "default_cache_control")]
pub transaction_query: String,
#[serde(default = "default_cache_control")]
pub get_utxos_deleted_info: String,
#[serde(default = "default_cache_control")]
pub get_utxos_mined_info: String,
}
fn default_enabled() -> bool {
true
}
fn default_get_tip_info_cache_control() -> String {
"public, max-age=15, s-maxage=15, stale-while-revalidate=15".to_string()
}
fn default_get_header_by_height_cache_control() -> String {
"public, max-age=120, s-maxage=60, stale-while-revalidate=15".to_string()
}
fn default_get_utxos_by_block_cache_control() -> String {
"public, max-age=3600, s-maxage=1800, stale-while-revalidate=60".to_string()
}
fn default_sync_utxos_by_block_cache_control() -> String {
"public, max-age=3600, s-maxage=1800, stale-while-revalidate=60".to_string()
}
fn default_cache_control() -> String {
"public, max-age=60, s-maxage=30, stale-while-revalidate=15".to_string()
}
impl Default for HttpCacheConfig {
fn default() -> Self {
Self {
enabled: true,
dynamic: true,
get_tip_info: default_get_tip_info_cache_control(),
get_header_by_height: default_get_header_by_height_cache_control(),
get_utxos_by_block: default_get_utxos_by_block_cache_control(),
sync_utxos_by_block: default_sync_utxos_by_block_cache_control(),
get_height_at_time: default_cache_control(),
transaction_query: default_cache_control(),
get_utxos_deleted_info: default_cache_control(),
get_utxos_mined_info: default_cache_control(),
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum RouteKey {
GetTipInfo,
GetHeaderByHeight,
GetUtxosByBlock,
SyncUtxosByBlock,
GetHeightAtTime,
TransactionQuery,
GetUtxosDeletedInfo,
GetUtxosMinedInfo,
}
impl HttpCacheConfig {
pub fn cache_control_for(&self, key: RouteKey, tip_height: u64, height: u64) -> String {
let default = match key {
RouteKey::GetTipInfo => self.get_tip_info.clone(),
RouteKey::GetHeaderByHeight => self.get_header_by_height.clone(),
RouteKey::GetUtxosByBlock => self.get_utxos_by_block.clone(),
RouteKey::SyncUtxosByBlock => self.sync_utxos_by_block.clone(),
RouteKey::GetHeightAtTime => self.get_height_at_time.clone(),
RouteKey::TransactionQuery => self.transaction_query.clone(),
RouteKey::GetUtxosDeletedInfo => self.get_utxos_deleted_info.clone(),
RouteKey::GetUtxosMinedInfo => self.get_utxos_mined_info.clone(),
};
if !self.dynamic {
return default;
}
let (max_age, s_maxage, stale_while_revalidate) = match tip_height.saturating_sub(height) {
0..=10 => (30, 30, 15), 11..=100 => (300, 60 * 5, 60), 101..=1000 => (360, 60 * 20, 60), 1001..=2000 => (360, 60 * 30, 60), 2001..=10000 => (360, 60 * 60 * 24, 60), _ => (360, 60 * 60 * 24 * 30, 60),
};
match key {
RouteKey::GetTipInfo => default,
RouteKey::GetHeaderByHeight => format!(
"public, max-age={max_age}, s-maxage={s_maxage}, stale-while-revalidate={stale_while_revalidate}"
),
RouteKey::GetUtxosByBlock => format!(
"public, max-age={max_age}, s-maxage={s_maxage}, stale-while-revalidate={stale_while_revalidate}"
),
RouteKey::SyncUtxosByBlock => format!(
"public, max-age={max_age}, s-maxage={s_maxage}, stale-while-revalidate={stale_while_revalidate}"
),
RouteKey::GetHeightAtTime => default,
RouteKey::TransactionQuery => default,
RouteKey::GetUtxosDeletedInfo => default,
RouteKey::GetUtxosMinedInfo => default,
}
}
}
pub fn apply_cache_control(
headers: &mut HeaderMap,
cfg: &HttpCacheConfig,
key: RouteKey,
tip_height: u64,
height: u64,
) {
if !cfg.enabled {
return;
}
let value = cfg.cache_control_for(key, tip_height, height);
if let Ok(hv) = HeaderValue::from_str(&value) {
headers.insert("Cache-Control", hv);
}
}
#[cfg(test)]
mod tests {
use axum::http::HeaderMap;
use super::*;
#[test]
fn test_apply_cache_control_disabled() {
let cfg = HttpCacheConfig {
enabled: false,
..Default::default()
};
let mut headers = HeaderMap::new();
apply_cache_control(&mut headers, &cfg, RouteKey::GetTipInfo, 0, 0);
assert!(headers.get("Cache-Control").is_none());
}
#[test]
fn test_apply_cache_control_all_keys() {
let cfg = HttpCacheConfig {
dynamic: false,
..Default::default()
};
let mut headers = HeaderMap::new();
for key in [
RouteKey::GetTipInfo,
RouteKey::GetHeaderByHeight,
RouteKey::GetUtxosByBlock,
RouteKey::SyncUtxosByBlock,
RouteKey::GetHeightAtTime,
RouteKey::TransactionQuery,
RouteKey::GetUtxosDeletedInfo,
RouteKey::GetUtxosMinedInfo,
] {
headers.clear();
apply_cache_control(&mut headers, &cfg, key, 0, 0);
match key {
RouteKey::GetTipInfo => {
assert_eq!(
headers.get("Cache-Control").unwrap(),
&HeaderValue::from_static("public, max-age=15, s-maxage=15, stale-while-revalidate=15")
);
},
RouteKey::GetHeaderByHeight => {
assert_eq!(
headers.get("Cache-Control").unwrap(),
&HeaderValue::from_static("public, max-age=120, s-maxage=60, stale-while-revalidate=15")
);
},
RouteKey::GetUtxosByBlock => {
assert_eq!(
headers.get("Cache-Control").unwrap(),
&HeaderValue::from_static("public, max-age=3600, s-maxage=1800, stale-while-revalidate=60")
);
},
RouteKey::SyncUtxosByBlock => {
assert_eq!(
headers.get("Cache-Control").unwrap(),
&HeaderValue::from_static("public, max-age=3600, s-maxage=1800, stale-while-revalidate=60")
);
},
RouteKey::GetHeightAtTime => {
assert_eq!(
headers.get("Cache-Control").unwrap(),
&HeaderValue::from_static("public, max-age=60, s-maxage=30, stale-while-revalidate=15")
);
},
RouteKey::TransactionQuery => {
assert_eq!(
headers.get("Cache-Control").unwrap(),
&HeaderValue::from_static("public, max-age=60, s-maxage=30, stale-while-revalidate=15")
);
},
RouteKey::GetUtxosDeletedInfo => {
assert_eq!(
headers.get("Cache-Control").unwrap(),
&HeaderValue::from_static("public, max-age=60, s-maxage=30, stale-while-revalidate=15")
);
},
RouteKey::GetUtxosMinedInfo => {
assert_eq!(
headers.get("Cache-Control").unwrap(),
&HeaderValue::from_static("public, max-age=60, s-maxage=30, stale-while-revalidate=15")
);
},
}
}
}
#[test]
fn test_cache_control_for_dynamic_buckets_boundaries() {
let cfg = HttpCacheConfig {
dynamic: true,
..Default::default()
};
let cases = [
(0u64, 30, 30, 15),
(10, 30, 30, 15),
(11, 300, 300, 60),
(100, 300, 300, 60),
(101, 360, 1200, 60),
(1000, 360, 1200, 60),
(1001, 360, 1800, 60),
(2000, 360, 1800, 60),
(2001, 360, 86400, 60),
(10000, 360, 86400, 60),
(10001, 360, 2592000, 60),
];
let tip = 50_000u64;
for (delta, max_age, s_maxage, swr) in cases {
let height = tip.saturating_sub(delta);
for key in [
RouteKey::GetHeaderByHeight,
RouteKey::GetUtxosByBlock,
RouteKey::SyncUtxosByBlock,
] {
let got = cfg.cache_control_for(key, tip, height);
let expected = format!(
"public, max-age={}, s-maxage={}, stale-while-revalidate={}",
max_age, s_maxage, swr
);
assert_eq!(got, expected, "key={:?} delta={}", key, delta);
}
}
}
#[test]
fn test_cache_control_for_height_greater_than_tip_uses_lowest_bucket() {
let cfg = HttpCacheConfig {
dynamic: true,
..Default::default()
};
let tip = 100;
let height = 200; let expected = "public, max-age=30, s-maxage=30, stale-while-revalidate=15";
let got = cfg.cache_control_for(RouteKey::GetHeaderByHeight, tip, height);
assert_eq!(got, expected);
}
#[test]
fn test_cache_control_for_dynamic_keeps_default_for_non_dynamic_routes() {
let cfg = HttpCacheConfig {
dynamic: true,
..Default::default()
};
let tip = 10_000u64;
let height = 1;
assert_eq!(
cfg.cache_control_for(RouteKey::GetTipInfo, tip, height),
cfg.get_tip_info
);
assert_eq!(
cfg.cache_control_for(RouteKey::GetHeightAtTime, tip, height),
cfg.get_height_at_time
);
assert_eq!(
cfg.cache_control_for(RouteKey::TransactionQuery, tip, height),
cfg.transaction_query
);
assert_eq!(
cfg.cache_control_for(RouteKey::GetUtxosDeletedInfo, tip, height),
cfg.get_utxos_deleted_info
);
assert_eq!(
cfg.cache_control_for(RouteKey::GetUtxosMinedInfo, tip, height),
cfg.get_utxos_mined_info
);
}
}