mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-05-19 14:24:47 -07:00
167 lines
5.2 KiB
Rust
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());
|
|
}
|
|
}
|