Files
brk/crates/brk_server/src/cache/params.rs
2026-04-28 18:46:37 +02:00

167 lines
5.2 KiB
Rust

use axum::http::HeaderMap;
use brk_types::{BlockHashPrefix, Version};
use crate::{VERSION, etag::Etag, extended::HeaderMapExtended};
use super::{
mode::{CDN_LIVE, cdn_cached},
strategy::CacheStrategy,
};
// Browser-facing: always revalidate via ETag. `no-cache` means "cache it but
// check before use" (not "don't cache"); ETag makes the check cheap.
const CC: &str = "public, no-cache, stale-if-error=86400";
// Errors: short, must-revalidate, no `stale-if-error` (we don't want a 24h-old
// error served when origin recovers). Same string for browser and CDN.
const CC_ERROR: &str = "public, max-age=1, must-revalidate";
/// Resolved cache parameters: an ETag plus the two Cache-Control directives.
pub struct CacheParams {
pub etag: Etag,
cache_control: &'static str,
cdn_cache_control: &'static str,
}
impl CacheParams {
fn tip(tip: BlockHashPrefix) -> Self {
Self {
etag: format!("t{:x}", *tip).into(),
cache_control: CC,
cdn_cache_control: CDN_LIVE,
}
}
fn immutable(version: Version) -> Self {
Self {
etag: format!("i{version}").into(),
cache_control: CC,
cdn_cache_control: cdn_cached(),
}
}
fn block_bound(version: Version, prefix: BlockHashPrefix) -> Self {
Self {
etag: format!("b{version}-{:x}", *prefix).into(),
cache_control: CC,
cdn_cache_control: cdn_cached(),
}
}
/// Deploy-tied response: etag from the build version. Used directly
/// by static handlers (OpenAPI spec, scalar bundle) that don't have
/// a [`CacheStrategy`] context.
pub fn deploy() -> Self {
Self {
etag: format!("d{VERSION}").into(),
cache_control: CC,
cdn_cache_control: cdn_cached(),
}
}
fn mempool_hash(hash: u64) -> Self {
Self {
etag: format!("m{hash:x}").into(),
cache_control: CC,
cdn_cache_control: CDN_LIVE,
}
}
/// Series query: tail-bound (`end >= total`) gets LIVE, historical gets CACHED.
/// Etag distinguishes the two: tail uses tip hash (per-block + reorgs),
/// historical uses total length (only changes when new data is appended).
pub fn series(version: Version, total: usize, end: usize, hash: BlockHashPrefix) -> Self {
let v = u32::from(version);
if end >= total {
Self {
etag: format!("s{v}-{:x}", *hash).into(),
cache_control: CC,
cdn_cache_control: CDN_LIVE,
}
} else {
Self {
etag: format!("s{v}-{total}").into(),
cache_control: CC,
cdn_cache_control: cdn_cached(),
}
}
}
/// Error response: keeps the originating ETag (so retries can 304),
/// uses [`CC_ERROR`] for both browser and CDN.
pub fn error(etag: Etag) -> Self {
Self {
etag,
cache_control: CC_ERROR,
cdn_cache_control: CC_ERROR,
}
}
/// Apply error cache-control headers without an ETag. Used for synthesized
/// errors (panics, fallback handlers) that have no resource etag.
pub fn apply_error_cache_control(headers: &mut HeaderMap) {
headers.insert_cache_control(CC_ERROR);
headers.insert_cdn_cache_control(CC_ERROR);
}
pub fn matches_etag(&self, headers: &HeaderMap) -> bool {
headers.has_etag(self.etag.as_str())
}
/// Write this cache policy (etag + cache-control + cdn-cache-control) onto a response's headers.
pub fn apply_to(&self, headers: &mut HeaderMap) {
headers.insert_etag(self.etag.as_str());
headers.insert_cache_control(self.cache_control);
headers.insert_cdn_cache_control(self.cdn_cache_control);
}
pub fn resolve(strategy: &CacheStrategy, tip: BlockHashPrefix) -> Self {
match strategy {
CacheStrategy::Tip => Self::tip(tip),
CacheStrategy::Immutable(v) => Self::immutable(*v),
CacheStrategy::BlockBound(v, prefix) => Self::block_bound(*v, *prefix),
CacheStrategy::Deploy => Self::deploy(),
CacheStrategy::MempoolHash(hash) => Self::mempool_hash(*hash),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn v(n: u32) -> Version {
Version::new(n)
}
fn h(n: u64) -> BlockHashPrefix {
BlockHashPrefix::from(n)
}
#[test]
fn series_tail_uses_tip_hash() {
let p = CacheParams::series(v(3), 100, 100, h(0xabcd));
assert_eq!(p.etag.as_str(), "s3-abcd");
}
#[test]
fn series_historical_uses_total() {
let p = CacheParams::series(v(3), 100, 50, h(0xabcd));
assert_eq!(p.etag.as_str(), "s3-100");
}
#[test]
fn series_historical_ignores_tip_hash() {
let a = CacheParams::series(v(3), 100, 50, h(0xabcd));
let b = CacheParams::series(v(3), 100, 50, h(0xdead));
assert_eq!(a.etag.as_str(), b.etag.as_str());
}
#[test]
fn series_tail_changes_with_tip_hash() {
let a = CacheParams::series(v(3), 100, 100, h(0xabcd));
let b = CacheParams::series(v(3), 100, 100, h(0xdead));
assert_ne!(a.etag.as_str(), b.etag.as_str());
}
}