This commit is contained in:
Mateusz Gruszczyński committed 2026-10-04 11:15:58 +02:00
1 parent 87e571c170
commit fa7abc9e53
308 files changed
+49746 -50237

No files matched your search

+396
View File
@@ -0,0 +1,396 @@
#[derive(Debug, Clone, Serialize)]
pub struct DomainView {
pub vhost: String,
pub domain: String,
pub entries: usize,
pub logical_bytes: usize,
pub resident_bytes: usize,
pub hot_entries: usize,
pub cold_entries: usize,
pub cold_bytes: u64,
pub hit_ratio: f64,
#[serde(flatten)]
pub counters: DomainCounters,
}
#[derive(Debug, Clone, Serialize)]
pub struct ObjectView {
pub vhost: String,
pub scheme: String,
pub authority: String,
pub domain: String,
pub path: String,
pub target: String,
pub cache_key: String,
pub tags: Vec<String>,
pub transformed: bool,
pub body_bytes: usize,
pub resident_bytes: usize,
pub storage_bytes: u64,
pub tier: &'static str,
pub hits: u64,
pub age_seconds: u64,
pub remaining_ttl_seconds: u64,
}
#[derive(Debug, Clone, Serialize)]
pub struct Dashboard {
pub domains: Vec<DomainView>,
pub objects: Vec<ObjectView>,
pub matching_objects: usize,
pub matching_body_bytes: usize,
pub matching_resident_bytes: usize,
pub matching_cold_bytes: u64,
pub offset: usize,
pub limit: usize,
pub statistics_revision: u64,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ColdCleanupRequest {
/// Remove objects not used for at least this many seconds.
pub unused_for_seconds: Option<u64>,
/// Additionally remove up to N oldest matching objects.
pub oldest: Option<usize>,
/// Limit the selection to objects which were never served.
pub hits_zero: bool,
}
impl Default for ColdCleanupRequest {
fn default() -> Self {
Self { unused_for_seconds: None, oldest: Some(100), hits_zero: false }
}
}
impl ColdCleanupRequest {
fn validate(&self) -> Result<()> {
ensure!(
self.unused_for_seconds.is_some() || self.oldest.is_some() || self.hits_zero,
"at least one cold-cache cleanup criterion is required"
);
if let Some(value) = self.unused_for_seconds {
ensure!((1..=31_536_000).contains(&value), "unused_for_seconds must be 1..31536000");
}
if let Some(value) = self.oldest {
ensure!((1..=10_000).contains(&value), "oldest must be 1..10000");
}
Ok(())
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct ColdCleanupResult {
pub removed_entries: usize,
pub freed_bytes: u64,
}
#[derive(Debug, Clone, Serialize)]
pub struct ColdCleanupJob {
pub id: u64,
pub status: &'static str,
pub removed_entries: usize,
pub freed_bytes: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
struct Entry {
base_key: Key,
vary: Vec<String>,
scope: String,
scheme: String,
authority: String,
domain: String,
path: String,
target: String,
tags: Vec<String>,
created_at: SystemTime,
headers: HeaderMap,
body: Bytes,
/// Backing allocation retained by `body`; can exceed `body.len()`.
body_allocated: usize,
received: Instant,
last_access: Instant,
resident_since: Instant,
resident_age_base: Duration,
initial_age: Duration,
ttl: u64,
stale_while_revalidate: u64,
stale_if_error: u64,
must_revalidate: bool,
proxy_revalidate: bool,
origin_etag: Option<HeaderValue>,
origin_last_modified: Option<HeaderValue>,
cost: usize,
sequence: u64,
hits: u64,
transformed: bool,
skip_compression: bool,
requires_revalidation: bool,
}
#[allow(clippy::ptr_arg)] // String/Vec capacity is intentionally part of HOT accounting.
fn hot_entry_cost(
body_allocated: usize,
headers: &HeaderMap,
vary: &Vec<String>,
scope: &String,
scheme: &String,
authority: &String,
domain: &String,
path: &String,
target: &String,
tags: &Vec<String>,
origin_etag: Option<&HeaderValue>,
origin_last_modified: Option<&HeaderValue>,
) -> usize {
// The public containers do not expose an exact per-entry allocator charge.
// Account their retained index memory conservatively: HOT HashMap key/control,
// the LRU BTreeMap item/node share and the variants index (including spare
// Vec<Key> capacity). This keeps `max_bytes` closer to the real retained
// allocation instead of pretending Entry/body are the only HOT owners.
let index_heap = std::mem::size_of::<Key>()
.saturating_add(std::mem::size_of::<(u64, Key)>())
.saturating_add(32)
.saturating_add(2usize.saturating_mul(std::mem::size_of::<Key>()))
.saturating_add(32)
.saturating_add(32);
let string_heap = |value: &String| value.capacity();
let string_vec_heap = |values: &Vec<String>| {
values
.capacity()
.saturating_mul(std::mem::size_of::<String>())
.saturating_add(values.iter().map(String::capacity).sum::<usize>())
};
let header_heap = headers
.iter()
.map(|(name, value)| {
// HeaderMap does not expose its bucket capacity. Charge a conservative
// per-field bucket/node allowance in addition to visible name/value bytes.
name.as_str()
.len()
.saturating_add(value.as_bytes().len())
.saturating_add(64)
})
.sum::<usize>();
body_allocated
.saturating_add(std::mem::size_of::<Entry>())
.saturating_add(index_heap)
.saturating_add(header_heap)
.saturating_add(string_vec_heap(vary))
.saturating_add(string_heap(scope))
.saturating_add(string_heap(scheme))
.saturating_add(string_heap(authority))
.saturating_add(string_heap(domain))
.saturating_add(string_heap(path))
.saturating_add(string_heap(target))
.saturating_add(string_vec_heap(tags))
.saturating_add(origin_etag.map_or(0, |value| value.as_bytes().len()))
.saturating_add(origin_last_modified.map_or(0, |value| value.as_bytes().len()))
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ColdFile {
format: u8,
key: String,
#[serde(default)]
base_key: String,
#[serde(default)]
vary: Vec<String>,
revision: u64,
scope: String,
#[serde(default = "default_cache_scheme")]
scheme: String,
#[serde(default)]
authority: String,
domain: String,
path: String,
#[serde(default)]
target: String,
#[serde(default)]
tags: Vec<String>,
#[serde(default)]
created_at_ms: u64,
headers: Vec<(String, String)>,
body_file: String,
body_bytes: usize,
body_sha256: String,
stored_at_ms: u64,
age_at_store_seconds: u64,
#[serde(default)]
resident_age_at_store_seconds: u64,
ttl: u64,
#[serde(default)]
stale_while_revalidate: u64,
#[serde(default)]
stale_if_error: u64,
#[serde(default)]
must_revalidate: bool,
#[serde(default)]
proxy_revalidate: bool,
#[serde(default)]
origin_etag: Option<String>,
#[serde(default)]
origin_last_modified: Option<String>,
#[serde(default)]
last_access_ms: u64,
hits: u64,
#[serde(default)]
transformed: bool,
skip_compression: bool,
requires_revalidation: bool,
}
#[derive(Clone)]
struct ColdEntry {
key: Key,
base_key: Key,
vary: Vec<String>,
revision: u64,
scope: String,
scheme: String,
authority: String,
domain: String,
path: String,
target: String,
tags: Vec<String>,
created_at: SystemTime,
headers: HeaderMap,
body_path: PathBuf,
meta_path: PathBuf,
body_bytes: usize,
body_sha256: String,
disk_bytes: u64,
stored_at: SystemTime,
last_access: SystemTime,
age_at_store_seconds: u64,
resident_age_at_store_seconds: u64,
ttl: u64,
stale_while_revalidate: u64,
stale_if_error: u64,
must_revalidate: bool,
proxy_revalidate: bool,
origin_etag: Option<HeaderValue>,
origin_last_modified: Option<HeaderValue>,
hits: u64,
transformed: bool,
sequence: u64,
// Stable identity of this in-memory cold entry. Unlike `sequence`, this
// never changes on access, so an old async reader cannot unpin a newer
// replacement stored under the same cache key.
generation: u64,
skip_compression: bool,
requires_revalidation: bool,
pinned_reads: usize,
}
#[derive(Default)]
struct ColdStore {
entries: HashMap<Key, ColdEntry>,
variants: HashMap<Key, Vec<Key>>,
lru: BTreeMap<u64, Key>,
bytes: u64,
sequence: u64,
}
struct ColdDraft {
key: Key,
revision: u64,
generation: u64,
scope_generation: u64,
key_generation: u64,
entry: Entry,
root: PathBuf,
/// Keeps this full in-memory object charged to the bounded transient pool
/// until the COLD write completes and the payload is dropped.
_memory_lease: TransientLease,
}
enum ColdCommand {
Store(ColdDraft),
Delete(Vec<PathBuf>),
PurgeDirectory(PathBuf),
}
#[derive(Default)]
struct Store {
entries: HashMap<Key, Entry>,
variants: HashMap<Key, Vec<Key>>,
lru: BTreeMap<u64, Key>,
bytes: usize,
logical_bytes: usize,
sequence: u64,
generation: u64,
revision: u64,
scope_generations: HashMap<String, u64>,
key_generations: HashMap<Key, u64>,
domains: BTreeMap<(String, String), DomainCounters>,
last_sweep: Option<Instant>,
}
impl Store {
fn index_variant(&mut self, base_key: Key, key: Key) {
let keys = self.variants.entry(base_key).or_default();
if !keys.contains(&key) {
keys.push(key);
}
}
fn remove(&mut self, key: &Key) -> Option<Entry> {
let entry = self.entries.remove(key)?;
self.lru.remove(&entry.sequence);
self.bytes = self.bytes.saturating_sub(entry.cost);
self.logical_bytes = self.logical_bytes.saturating_sub(entry.body.len());
let empty = if let Some(keys) = self.variants.get_mut(&entry.base_key) {
keys.retain(|candidate| candidate != key);
keys.is_empty()
} else {
false
};
if empty {
self.variants.remove(&entry.base_key);
}
Some(entry)
}
fn domain(&mut self, scope: &str, domain: &str) -> Option<&mut DomainCounters> {
let key = (scope.to_owned(), domain.to_owned());
// Cardinality is bounded independently of resident cache capacity.
if self.domains.len() >= 4096 && !self.domains.contains_key(&key) {
return None;
}
Some(self.domains.entry(key).or_default())
}
fn sweep(&mut self) -> usize {
if self
.last_sweep
.is_some_and(|v| v.elapsed() < Duration::from_secs(1))
{
return 0;
}
self.last_sweep = Some(Instant::now());
let stale: Vec<_> = self
.entries
.iter()
.filter(|(_, e)| {
let age = e.initial_age.saturating_add(e.received.elapsed()).as_secs();
age >= retention_deadline(
e.ttl,
e.stale_while_revalidate,
e.stale_if_error,
e.origin_etag.is_some() || e.origin_last_modified.is_some(),
)
})
.map(|(k, _)| *k)
.collect();
for key in &stale {
self.remove(key);
}
stale.len()
}
fn next_sequence(&mut self) -> u64 {
// No practical wrapping, but a wrap must not corrupt LRU bookkeeping.
if self.sequence == u64::MAX {
self.entries.clear();
self.variants.clear();
self.lru.clear();
self.bytes = 0;
self.logical_bytes = 0;
self.sequence = 0;
}
self.sequence += 1;
self.sequence
}
}