Files
RustProxy/src/cache/model/entry.rs
T
2026-11-07 08:45:14 +01:00

589 lines
19 KiB
Rust

impl Settings {
pub fn validate(&self) -> Result<()> {
ensure!(
self.max_bytes >= 65_536,
"cache.max_bytes must be at least 64 KiB"
);
ensure!(
self.max_object_bytes >= 1024 && self.max_object_bytes <= self.max_bytes,
"cache.max_object_bytes must be at least 1 KiB and no larger than max_bytes"
);
ensure!(
(1..=16_384).contains(&self.max_entries),
"cache.max_entries must be 1..16384"
);
ensure!(
self.transient_max_bytes >= 65_536,
"cache.transient_max_bytes must be at least 64 KiB"
);
ensure!(
self.cold_queue_max_bytes >= 65_536
&& self.cold_queue_max_bytes <= self.transient_max_bytes,
"cache.cold_queue_max_bytes must be at least 64 KiB and no larger than transient_max_bytes"
);
ensure!(
(1..=4096).contains(&self.hot_eviction_max_objects),
"cache.hot_eviction_max_objects must be 1..4096"
);
ensure!(
(5..=86_400).contains(&self.hot_auto_trim_after_seconds),
"cache.hot_auto_trim_after_seconds must be 5..86400"
);
ensure!(
(10..=95).contains(&self.hot_auto_trim_target_percent),
"cache.hot_auto_trim_target_percent must be 10..95"
);
ensure!(
(65_536..=1024_u64 * 1024 * 1024 * 1024).contains(&self.cold_max_bytes),
"cache.cold_max_bytes must be 64 KiB..1 TiB"
);
ensure!(
(1..=1_000_000).contains(&self.cold_max_entries),
"cache.cold_max_entries must be 1..1000000"
);
ensure!(
(5..=86_400).contains(&self.cold_gc_interval_seconds),
"cache.cold_gc_interval_seconds must be 5..86400"
);
ensure!(
(1..=10_000).contains(&self.cold_gc_max_delete),
"cache.cold_gc_max_delete must be 1..10000"
);
ensure!(
(5..=604_800).contains(&self.cold_after_seconds),
"cache.cold_after_seconds must be 5..604800"
);
ensure!(
(5..=2_592_000).contains(&self.cold_max_hot_seconds),
"cache.cold_max_hot_seconds must be 5..2592000"
);
ensure!(
valid_relative_cache_path(&self.cold_path),
"cache.cold_path must be a simple relative path inside the data directory"
);
ensure!(
self.varnish_grace_seconds <= 604_800,
"cache.varnish_grace_seconds must be 0..604800"
);
ensure!(
(1..=604_800).contains(&self.max_ttl_seconds)
&& self.default_ttl_seconds <= self.max_ttl_seconds,
"cache TTL must be at most seven days; default TTL cannot exceed maximum"
);
ensure!(
self.bypass_prefixes.len() <= 64,
"at most 64 cache bypass prefixes"
);
for prefix in &self.bypass_prefixes {
ensure!(
prefix.len() <= 512
&& prefix.starts_with('/')
&& crate::acl::routing_path(prefix)? == *prefix,
"cache bypass prefixes must be normalized absolute paths"
);
}
self.validate_http_invalidation_allowlist()?;
ensure!(
self.static_extensions.len() <= 64,
"at most 64 static cache extensions"
);
let mut extensions = std::collections::HashSet::new();
for extension in &self.static_extensions {
ensure!(
!extension.is_empty()
&& extension.len() <= 16
&& extension.bytes().all(|b| b.is_ascii_alphanumeric()),
"static cache extensions must be 1..16 ASCII letters or numbers without a dot"
);
ensure!(
extensions.insert(extension.to_ascii_lowercase()),
"duplicate static cache extension"
);
}
ensure!(
(10..=60_000).contains(&self.collapse_wait_timeout_ms),
"cache.collapse_wait_timeout_ms must be 10..60000"
);
ensure!(
self.stale_if_error_statuses.len() <= 32,
"cache.stale_if_error_statuses supports at most 32 statuses"
);
let mut statuses = std::collections::HashSet::new();
for status in &self.stale_if_error_statuses {
ensure!(
(500..=599).contains(status),
"cache.stale_if_error_statuses must contain HTTP 5xx status codes"
);
ensure!(
statuses.insert(*status),
"duplicate stale-if-error HTTP status"
);
}
ensure!(
(1..=604_800).contains(&self.proactive_refresh_before_expiry_seconds),
"cache.proactive_refresh_before_expiry_seconds must be 1..604800"
);
ensure!(
self.proactive_refresh_percent <= 100,
"cache.proactive_refresh_percent must be 0..100"
);
ensure!(
(1..=64).contains(&self.proactive_refresh_max_inflight),
"cache.proactive_refresh_max_inflight must be 1..64"
);
if let Some(value) = &self.via_header {
ensure!(
!value.is_empty()
&& value.len() <= 256
&& value.bytes().all(|byte| byte >= 0x20 && byte != 0x7f)
&& value.parse::<HeaderValue>().is_ok(),
"cache.via_header must be a valid 1..256 byte HTTP header value"
);
}
Ok(())
}
pub fn active_for(&self, route: Option<bool>) -> bool {
self.enabled && route != Some(false)
}
pub fn allows_stale_error_class(&self, class: StaleErrorClass) -> bool {
match class {
StaleErrorClass::Timeout => self.stale_if_error_timeout,
StaleErrorClass::Connection => self.stale_if_error_connection,
StaleErrorClass::Dns => self.stale_if_error_dns,
StaleErrorClass::Tls => self.stale_if_error_tls,
}
}
pub fn allows_stale_error_status(&self, status: StatusCode) -> bool {
self.stale_if_error_statuses.contains(&status.as_u16())
}
pub fn ignore_cookie_for_path(&self, path: &str) -> bool {
if !self.static_ignore_cookies {
return false;
}
let segment = path.rsplit('/').next().unwrap_or(path);
let Some((_, extension)) = segment.rsplit_once('.') else {
return false;
};
!extension.is_empty()
&& self
.static_extensions
.iter()
.any(|item| item.eq_ignore_ascii_case(extension))
}
pub fn annotate(&self, headers: &mut HeaderMap, outcome: &'static str, hits: u64, ttl: u64) {
self.annotate_internal(headers, outcome, hits, ttl as i64, None, false, None);
}
pub fn annotate_reason(
&self,
headers: &mut HeaderMap,
outcome: &'static str,
hits: u64,
ttl: u64,
reason: &'static str,
) {
self.annotate_internal(
headers,
outcome,
hits,
ttl as i64,
Some(reason),
false,
None,
);
}
pub fn annotate_reason_transformed(
&self,
headers: &mut HeaderMap,
outcome: &'static str,
hits: u64,
ttl: u64,
reason: &'static str,
transformed: bool,
) {
self.annotate_internal(
headers,
outcome,
hits,
ttl as i64,
Some(reason),
transformed,
None,
);
}
pub fn annotate_reason_transformed_freshness(
&self,
headers: &mut HeaderMap,
outcome: &'static str,
hits: u64,
freshness_ttl: i64,
reason: &'static str,
transformed: bool,
) {
self.annotate_internal(
headers,
outcome,
hits,
freshness_ttl,
Some(reason),
transformed,
None,
);
}
pub fn annotate_reason_transformed_forwarded(
&self,
headers: &mut HeaderMap,
outcome: &'static str,
hits: u64,
ttl: u64,
reason: &'static str,
transformed: bool,
forward: &'static str,
forward_status: Option<StatusCode>,
) {
self.annotate_internal(
headers,
outcome,
hits,
ttl as i64,
Some(reason),
transformed,
Some((forward, forward_status)),
);
}
fn annotate_internal(
&self,
headers: &mut HeaderMap,
outcome: &'static str,
hits: u64,
ttl: i64,
reason: Option<&'static str>,
transformed: bool,
forwarded: Option<(&'static str, Option<StatusCode>)>,
) {
if !self.enabled {
return;
}
if let Some(value) = self
.via_header
.as_deref()
.and_then(crate::settings::cached_header_value)
{
headers.append(http::header::VIA, value);
}
if self.expose_only_hits && outcome != "HIT" {
return;
}
if self.expose_status {
insert_cache_diagnostic(
headers,
"x-cache",
"x-upstream-cache",
HeaderValue::from_static(outcome),
);
if let Some(reason) = reason {
insert_cache_diagnostic(
headers,
"x-cache-reason",
"x-upstream-cache-reason",
HeaderValue::from_static(reason),
);
}
}
if self.expose_hits {
insert_cache_diagnostic(
headers,
"x-cache-hits",
"x-upstream-cache-hits",
HeaderValue::from(hits),
);
}
if self.expose_cache_status {
let forwarded = forwarded.or_else(|| {
matches!(
reason,
Some("cache_revalidated" | "cache_revalidated_uncached")
)
.then_some(("stale", Some(StatusCode::NOT_MODIFIED)))
});
use std::fmt::Write as _;
let mut value = String::with_capacity(96);
value.push_str("RustProxy; ");
if let Some((why, status)) = forwarded {
let _ = write!(value, "fwd={why}");
if let Some(status) = status {
let _ = write!(value, "; fwd-status={}", status.as_u16());
}
} else {
match outcome {
"HIT" => {
let _ = write!(value, "hit; ttl={ttl}");
}
"MISS" => value.push_str("fwd=miss"),
"BYPASS" if reason == Some("request_method") => value.push_str("fwd=method"),
_ => value.push_str("fwd=bypass"),
}
}
if let Some(reason) = reason {
let _ = write!(value, "; detail=\"{reason}\"");
}
if transformed {
value.push_str("; transformed");
}
headers.append(
::http::header::CACHE_STATUS,
HeaderValue::from_maybe_shared(bytes::Bytes::from(value))
.expect("generated Cache-Status member"),
);
}
}
pub fn annotate_resident_age(&self, headers: &mut HeaderMap, resident_age: u64) {
if self.enabled && self.expose_resident_age {
insert_cache_diagnostic(
headers,
"x-cache-resident-age",
"x-upstream-cache-resident-age",
HeaderValue::from(resident_age),
);
}
}
}
fn insert_cache_diagnostic(
headers: &mut HeaderMap,
name: &'static str,
upstream_name: &'static str,
value: HeaderValue,
) {
let name = http::HeaderName::from_static(name);
if headers.contains_key(&name) {
let upstream_values: Vec<HeaderValue> = headers.get_all(&name).iter().cloned().collect();
let upstream_name = http::HeaderName::from_static(upstream_name);
for upstream_value in upstream_values {
headers.append(&upstream_name, upstream_value);
}
}
headers.insert(name, value);
}
fn valid_relative_cache_path(value: &str) -> bool {
if value.is_empty() || value.len() > 256 {
return false;
}
let path = Path::new(value);
!path.is_absolute()
&& path
.components()
.all(|part| matches!(part, Component::Normal(_)))
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct UnsafeForce {
pub enabled: bool,
pub targets: Vec<String>,
}
impl UnsafeForce {
pub fn validate(&self) -> Result<()> {
ensure!(
self.targets.len() <= 32,
"forced cache supports at most 32 target prefixes"
);
for target in &self.targets {
ensure!(
!target.is_empty()
&& target.len() <= 2048
&& target.starts_with('/')
&& !target.contains('#')
&& !target.chars().any(char::is_control),
"forced cache targets must be 1..2048 byte origin-form path/query prefixes"
);
let path = target.split('?').next().unwrap_or(target);
ensure!(
crate::acl::routing_path(path)? == path,
"forced cache target path must be normalized"
);
}
Ok(())
}
pub fn matches(&self, path: &str, target: &str) -> bool {
if !self.enabled {
return false;
}
self.targets.iter().any(|configured| {
if configured.contains('?') {
target.starts_with(configured)
} else {
crate::config::prefix_matches(configured, path)
}
})
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Overrides {
pub enabled: Option<bool>,
pub default_ttl_seconds: Option<u64>,
pub max_ttl_seconds: Option<u64>,
pub max_object_bytes: Option<usize>,
pub bypass_query: Option<bool>,
pub bypass_prefixes: Option<Vec<String>>,
pub static_ignore_cookies: Option<bool>,
pub static_extensions: Option<Vec<String>>,
pub expose_status: Option<bool>,
pub expose_hits: Option<bool>,
pub expose_cache_status: Option<bool>,
pub expose_resident_age: Option<bool>,
pub expose_only_hits: Option<bool>,
pub unsafe_force: UnsafeForce,
}
impl Overrides {
pub fn resolve(&self, global: &Settings) -> Result<Settings> {
self.unsafe_force.validate()?;
let mut result = global.clone();
result.enabled = global.enabled && self.enabled.unwrap_or(true);
if let Some(value) = self.default_ttl_seconds {
result.default_ttl_seconds = value;
}
if let Some(value) = self.max_ttl_seconds {
result.max_ttl_seconds = value;
}
if let Some(value) = self.max_object_bytes {
ensure!(
value <= global.max_object_bytes,
"vhost object limit cannot exceed the global ceiling"
);
result.max_object_bytes = value;
}
if let Some(value) = self.bypass_query {
result.bypass_query = value;
}
if let Some(value) = &self.bypass_prefixes {
result.bypass_prefixes = value.clone();
}
if let Some(value) = self.static_ignore_cookies {
result.static_ignore_cookies = value;
}
if let Some(value) = &self.static_extensions {
result.static_extensions = value.clone();
}
if let Some(value) = self.expose_status {
result.expose_status = value;
}
if let Some(value) = self.expose_hits {
result.expose_hits = value;
}
if let Some(value) = self.expose_cache_status {
result.expose_cache_status = value;
}
if let Some(value) = self.expose_resident_age {
result.expose_resident_age = value;
}
if let Some(value) = self.expose_only_hits {
result.expose_only_hits = value;
}
result.validate()?;
Ok(result)
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ScopeView {
pub entries: usize,
pub resident_bytes: usize,
pub cold_entries: usize,
pub cold_bytes: u64,
}
#[derive(Default)]
struct Counters {
hits: AtomicU64,
misses: AtomicU64,
bypasses: AtomicU64,
stored: AtomicU64,
evictions: AtomicU64,
expired: AtomicU64,
rejected_fills: AtomicU64,
memory_pressure_rejections: AtomicU64,
hot_eviction_limited: AtomicU64,
purges: AtomicU64,
cold_hits: AtomicU64,
cold_promotions: AtomicU64,
hot_demotions: AtomicU64,
cold_evictions: AtomicU64,
cold_errors: AtomicU64,
cold_queue_dropped: AtomicU64,
collapsed_fills: AtomicU64,
collapse_timeouts: AtomicU64,
purge_entries: AtomicU64,
bans_created: AtomicU64,
bans_removed: AtomicU64,
ban_matches: AtomicU64,
proactive_scheduled: AtomicU64,
proactive_started: AtomicU64,
proactive_skipped: AtomicU64,
}
#[derive(Debug, Clone, Serialize)]
pub struct View {
pub hits: u64,
pub misses: u64,
pub bypasses: u64,
pub stored: u64,
pub evictions: u64,
pub expired: u64,
pub rejected_fills: u64,
pub memory_pressure_rejections: u64,
pub hot_eviction_limited: u64,
pub purges: u64,
pub entries: usize,
pub resident_bytes: usize,
pub hot_logical_bytes: usize,
pub hot_allocated_bytes: usize,
pub hot_limit_bytes: usize,
pub transient_limit_bytes: usize,
pub cold_queue_limit_bytes: usize,
pub cold_entries: usize,
pub cold_bytes: u64,
pub cold_limit_bytes: u64,
pub cold_limit_entries: usize,
pub cold_hits: u64,
pub cold_promotions: u64,
pub hot_demotions: u64,
pub cold_evictions: u64,
pub cold_errors: u64,
pub cold_queue_dropped: u64,
pub collapsed_fills: u64,
pub collapse_timeouts: u64,
pub collapse_waiters: u64,
pub purge_entries: u64,
pub bans_created: u64,
pub bans_removed: u64,
pub ban_matches: u64,
pub active_bans: usize,
pub proactive_scheduled: u64,
pub proactive_started: u64,
pub proactive_skipped: u64,
pub proactive_inflight: u64,
pub fills_active: usize,
pub fill_bytes_active: u64,
pub cold_queue_items: u64,
pub cold_queue_bytes: u64,
pub cold_write_bytes_active: u64,
pub cold_read_bytes_active: u64,
pub transient_bytes: u64,
pub hit_ratio: f64,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct DomainCounters {
pub hits: u64,
pub misses: u64,
pub bypasses: u64,
pub stored: u64,
pub saved_body_bytes: u64,
pub reasons: BTreeMap<String, u64>,
}