v0.25.1
This commit is contained in:
1 parent
87e571c170
commit
fa7abc9e53
308 files changed
+49746
-50237
No files matched your search
Vendored
+396
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user