v0.34.0_1

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-18 06:41:20 +02:00
1 parent 9cfc4588cc
commit f20c8da718
25 files changed
+694 -117

No files matched your search

+11 -32
View File
@@ -176,8 +176,7 @@ impl Cache {
if let Some(revision) = revision {
store.revision = revision;
self.revision.store(revision, Ordering::Relaxed);
store.domains.clear();
store.domain_count = 0;
self.domain_stats.clear();
}
drop(store);
self.cancel_all_fill_flights();
@@ -264,44 +263,24 @@ impl Cache {
view
}
pub fn record(&self, scope: &str, domain: &str, outcome: &str, reason: &'static str) {
let mut store = self.store.lock().unwrap_or_else(|e| e.into_inner());
if let Some(c) = store.domain(scope, domain) {
match outcome {
"HIT" => c.hits += 1,
"MISS" => c.misses += 1,
_ => c.bypasses += 1,
}
if let Some(count) = c.reasons.get_mut(reason) {
*count = count.saturating_add(1);
} else {
c.reasons.insert(reason.to_owned(), 1);
}
}
self.domain_stats.record(scope, domain, outcome, reason);
}
pub fn record_reason(&self, scope: &str, domain: &str, reason: &'static str) {
let mut store = self.store.lock().unwrap_or_else(|e| e.into_inner());
if let Some(c) = store.domain(scope, domain) {
if let Some(count) = c.reasons.get_mut(reason) {
*count = count.saturating_add(1);
} else {
c.reasons.insert(reason.to_owned(), 1);
}
}
self.domain_stats.record_reason(scope, domain, reason);
}
pub fn dashboard(&self, scope: Option<&str>, offset: usize, limit: usize) -> Dashboard {
let mut store = self.store.lock().unwrap_or_else(|e| e.into_inner());
let revision = store.revision;
let mut groups: BTreeMap<(String, String), DomainView> = store
.domains
.iter()
.filter(|(vhost, _)| scope.is_none_or(|s| s == vhost.as_str()))
.flat_map(|(vhost, domains)| domains.iter().map(move |(domain, counters)| (vhost, domain, counters)))
.map(|(vhost, domain, counters)| {
let mut groups: BTreeMap<(String, String), DomainView> = self
.domain_stats
.snapshot(scope)
.into_iter()
.map(|((vhost, domain), counters)| {
(
(vhost.clone(), domain.clone()),
DomainView {
vhost: vhost.clone(),
domain: domain.clone(),
vhost,
domain,
entries: 0,
logical_bytes: 0,
resident_bytes: 0,
@@ -313,7 +292,7 @@ impl Cache {
} else {
0.0
},
counters: counters.clone(),
counters,
},
)
})
+2
View File
@@ -6,6 +6,7 @@ impl Cache {
..Default::default()
})),
revision: AtomicU64::new(revision),
domain_stats: DomainStatsStore::default(),
cold: Arc::new(Mutex::new(ColdStore::default())),
tier_settings: Mutex::new(Settings::default()),
scope_revisions: Mutex::new(HashMap::new()),
@@ -57,6 +58,7 @@ impl Cache {
..Default::default()
})),
revision: AtomicU64::new(revision),
domain_stats: DomainStatsStore::default(),
cold: Arc::new(Mutex::new(ColdStore::default())),
tier_settings: Mutex::new(settings),
scope_revisions: Mutex::new(scope_revisions),
+2 -7
View File
@@ -61,7 +61,6 @@ impl Cache {
store.lru.insert(sequence, storage_key);
// Preserve the exact LRU and domain-byte counters, without
// allocating a replacement variant index or String keys.
store.saved_body_hit(&ticket.scope, &ticket.domain, body_bytes);
self.counters.hits.fetch_add(1, Ordering::Relaxed);
drop(store);
@@ -77,6 +76,7 @@ impl Cache {
"age",
HeaderValue::from_str(&age.to_string()).expect("integer header"),
);
self.domain_stats.saved_body_hit(&ticket.scope, &ticket.domain, body_bytes);
return Some(Hit {
response,
body,
@@ -274,12 +274,7 @@ impl Cache {
"age",
HeaderValue::from_str(&age.to_string()).expect("integer header"),
);
{
let mut store = self.store.lock().unwrap_or_else(|e| e.into_inner());
if let Some(c) = store.domain(&ticket.scope, &ticket.domain) {
c.saved_body_bytes = c.saved_body_bytes.saturating_add(entry.body_bytes as u64);
}
}
self.domain_stats.saved_body_hit(&ticket.scope, &ticket.domain, entry.body_bytes);
self.counters.hits.fetch_add(1, Ordering::Relaxed);
self.counters.cold_hits.fetch_add(1, Ordering::Relaxed);
self.counters
+1 -3
View File
@@ -108,9 +108,7 @@ impl Cache {
return false;
}
if count_stored {
if let Some(c) = store.domain(&entry.scope, &entry.domain) {
c.stored += 1;
}
self.domain_stats.stored(&entry.scope, &entry.domain);
}
entry.last_access = Instant::now();
entry.sequence = store.next_sequence();
+1
View File
@@ -5,6 +5,7 @@
//! policy and streaming-fill code can be reviewed independently.
include!("model.rs");
include!("stats.rs");
include!("lifecycle.rs");
include!("cold.rs");
include!("dashboard.rs");
-22
View File
@@ -336,8 +336,6 @@ struct Store {
control_revision: u64,
scope_generations: HashMap<String, u64>,
key_generations: HashMap<Key, u64>,
domains: BTreeMap<String, BTreeMap<String, DomainCounters>>,
domain_count: usize,
}
impl Store {
fn index_variant(&mut self, base_key: Key, key: Key) {
@@ -362,26 +360,6 @@ impl Store {
}
Some(entry)
}
fn domain(&mut self, scope: &str, domain: &str) -> Option<&mut DomainCounters> {
// The common path is a borrowed lookup: no allocation of scope/domain
// strings for every cache hit or log-reason counter update.
if self.domains.get(scope).is_some_and(|d| d.contains_key(domain)) {
return self.domains.get_mut(scope)?.get_mut(domain);
}
if self.domain_count >= 4096 {
return None;
}
self.domain_count += 1;
Some(self.domains.entry(scope.to_owned()).or_default()
.entry(domain.to_owned()).or_default())
}
fn saved_body_hit(&mut self, scope: &str, domain: &str, bytes: usize) {
// Every admitted HOT insertion registered its domain. If the bounded
// stats table is full, skip accounting for an unregistered domain.
if let Some(c) = self.domains.get_mut(scope).and_then(|d| d.get_mut(domain)) {
c.saved_body_bytes = c.saved_body_bytes.saturating_add(bytes as u64);
}
}
fn next_sequence(&mut self) -> u64 {
// No practical wrapping, but a wrap must not corrupt LRU bookkeeping.
if self.sequence == u64::MAX {
+1
View File
@@ -52,6 +52,7 @@ impl Drop for TransientLeaseInner {
pub struct Cache {
store: Arc<Mutex<Store>>,
domain_stats: DomainStatsStore,
revision: AtomicU64,
cold: Arc<Mutex<ColdStore>>,
tier_settings: Mutex<Settings>,
+187
View File
@@ -0,0 +1,187 @@
// Lock-free read path for bounded, per-domain cache dashboard counters.
//
// Cache-disabled proxy requests still contribute to BYPASS and reason totals,
// but must never lock the HOT object/LRU store. Published domain/reason indexes
// are immutable; individual counters are atomics. Only first sight of a new
// domain/reason acquires the registration mutex.
use arc_swap::ArcSwap;
const DOMAIN_STATS_LIMIT: usize = 4096;
const FAST_REASONS: [&str; 4] = ["cache_disabled", "cache_hit", "cache_miss", "request_cookie"];
type ReasonIndex = BTreeMap<&'static str, Arc<AtomicU64>>;
type DomainIndex = BTreeMap<String, BTreeMap<String, Arc<DomainStats>>>;
#[derive(Clone, Default)]
struct PublishedDomains {
index: DomainIndex,
count: usize,
}
struct DomainStats {
hits: AtomicU64,
misses: AtomicU64,
bypasses: AtomicU64,
stored: AtomicU64,
saved_body_bytes: AtomicU64,
fast_reasons: [AtomicU64; FAST_REASONS.len()],
reasons: ArcSwap<ReasonIndex>,
register_reason: Mutex<()>,
}
impl Default for DomainStats {
fn default() -> Self {
Self {
hits: AtomicU64::new(0),
misses: AtomicU64::new(0),
bypasses: AtomicU64::new(0),
stored: AtomicU64::new(0),
saved_body_bytes: AtomicU64::new(0),
fast_reasons: std::array::from_fn(|_| AtomicU64::new(0)),
reasons: ArcSwap::from_pointee(ReasonIndex::new()),
register_reason: Mutex::new(()),
}
}
}
impl DomainStats {
fn record(&self, outcome: &str, reason: &'static str) {
match outcome {
"HIT" => &self.hits,
"MISS" => &self.misses,
_ => &self.bypasses,
}.fetch_add(1, Ordering::Relaxed);
self.record_reason(reason);
}
fn record_reason(&self, reason: &'static str) {
// Cache OFF is the highest-volume case; avoid reason map lookup and
// any shared lock/allocator activity for its dashboard reason.
if let Some(index) = FAST_REASONS.iter().position(|label| *label == reason) {
self.fast_reasons[index].fetch_add(1, Ordering::Relaxed);
return;
}
let published = self.reasons.load();
if let Some(counter) = published.get(reason) {
counter.fetch_add(1, Ordering::Relaxed);
return;
}
drop(published);
let _guard = self.register_reason.lock().unwrap_or_else(|e| e.into_inner());
let published = self.reasons.load_full();
if let Some(counter) = published.get(reason) {
counter.fetch_add(1, Ordering::Relaxed);
return;
}
let mut next = (*published).clone();
next.insert(reason, Arc::new(AtomicU64::new(1)));
self.reasons.store(Arc::new(next));
}
fn snapshot(&self) -> DomainCounters {
let load = |value: &AtomicU64| value.load(Ordering::Relaxed);
let mut reasons = BTreeMap::new();
for (label, counter) in FAST_REASONS.iter().zip(&self.fast_reasons) {
let value = load(counter);
if value > 0 {
reasons.insert((*label).to_owned(), value);
}
}
for (label, counter) in self.reasons.load().iter() {
reasons.insert((*label).to_owned(), load(counter));
}
DomainCounters {
hits: load(&self.hits),
misses: load(&self.misses),
bypasses: load(&self.bypasses),
stored: load(&self.stored),
saved_body_bytes: load(&self.saved_body_bytes),
reasons,
}
}
}
struct DomainStatsStore {
domains: ArcSwap<PublishedDomains>,
register: Mutex<()>,
}
impl Default for DomainStatsStore {
fn default() -> Self {
Self {
domains: ArcSwap::from_pointee(PublishedDomains::default()),
register: Mutex::new(()),
}
}
}
impl DomainStatsStore {
fn with(&self, scope: &str, domain: &str, action: impl FnOnce(&DomainStats)) {
let published = self.domains.load();
if let Some(counters) = published.index.get(scope).and_then(|hosts| hosts.get(domain)) {
action(counters);
return;
}
drop(published);
// Rare path: register only 4096 distinct (scope, domain) pairs. The
// published map is cloned on first insertion, never on a request hit.
let _guard = self.register.lock().unwrap_or_else(|e| e.into_inner());
let published = self.domains.load_full();
if let Some(counters) = published.index.get(scope).and_then(|hosts| hosts.get(domain)) {
action(counters);
return;
}
if published.count >= DOMAIN_STATS_LIMIT {
return;
}
let mut next = (*published).clone();
let counters = Arc::new(DomainStats::default());
next.index.entry(scope.to_owned()).or_default()
.insert(domain.to_owned(), Arc::clone(&counters));
next.count += 1;
self.domains.store(Arc::new(next));
action(&counters);
}
fn record(&self, scope: &str, domain: &str, outcome: &str, reason: &'static str) {
self.with(scope, domain, |counters| counters.record(outcome, reason));
}
fn record_reason(&self, scope: &str, domain: &str, reason: &'static str) {
self.with(scope, domain, |counters| counters.record_reason(reason));
}
fn stored(&self, scope: &str, domain: &str) {
self.with(scope, domain, |counters| {
counters.stored.fetch_add(1, Ordering::Relaxed);
});
}
fn saved_body_hit(&self, scope: &str, domain: &str, bytes: usize) {
// COLD entries loaded from persisted metadata may be served before a
// domain is registered. Preserve first-hit byte accounting as well.
self.with(scope, domain, |counters| {
counters.saved_body_bytes.fetch_add(bytes as u64, Ordering::Relaxed);
});
}
fn clear(&self) {
let _guard = self.register.lock().unwrap_or_else(|e| e.into_inner());
self.domains.store(Arc::new(PublishedDomains::default()));
}
fn snapshot(&self, scope: Option<&str>) -> BTreeMap<(String, String), DomainCounters> {
let published = self.domains.load();
let mut counters = BTreeMap::new();
for (vhost, domains) in &published.index {
if !scope.is_none_or(|selected| selected == vhost) {
continue;
}
for (domain, metric) in domains {
counters.insert((vhost.clone(), domain.clone()), metric.snapshot());
}
}
counters
}
}
+51
View File
@@ -215,6 +215,57 @@ mod dashboard_tests {
}
}
#[test]
fn cache_domain_metrics_do_not_lock_hot_store_and_preserve_reasons() {
let cache = Cache::new(7);
// A BYPASS must be recordable even while the HOT/LRU mutex is held.
let store = cache.store.lock().unwrap();
cache.record("alpha", "a.example", "BYPASS", "cache_disabled");
cache.record_reason("alpha", "a.example", "background_refresh_failure");
drop(store);
let workers = 8;
let each = 2000;
let mut tasks = Vec::new();
for _ in 0..workers {
let cache = Arc::clone(&cache);
tasks.push(std::thread::spawn(move || {
for _ in 0..each {
cache.record("alpha", "a.example", "BYPASS", "cache_disabled");
cache.record("alpha", "a.example", "HIT", "cache_hit");
}
}));
}
for task in tasks {
task.join().expect("domain counter worker");
}
let scoped = cache.dashboard(Some("alpha"), 0, 1);
assert_eq!(scoped.domains.len(), 1);
let counters = &scoped.domains[0].counters;
assert_eq!(counters.bypasses, 1 + workers * each);
assert_eq!(counters.hits, workers * each);
assert_eq!(counters.reasons["cache_disabled"], 1 + workers * each);
assert_eq!(counters.reasons["cache_hit"], workers * each);
assert_eq!(counters.reasons["background_refresh_failure"], 1);
assert_eq!(cache.purge_scope("alpha"), 0);
assert_eq!(cache.dashboard(Some("alpha"), 0, 1).domains[0].counters.hits, workers * each);
cache.purge(Some(8));
assert!(cache.dashboard(None, 0, 1).domains.is_empty());
cache.record("alpha", "a.example", "MISS", "cache_miss");
let after_reset = cache.dashboard(Some("alpha"), 0, 1);
assert_eq!(after_reset.domains[0].counters.misses, 1);
assert_eq!(after_reset.domains[0].counters.hits, 0);
assert_eq!(after_reset.statistics_revision, 8);
}
#[test]
fn first_hit_saved_bytes_registers_domain_even_without_prior_record() {
let cache = Cache::new(7);
cache.domain_stats.saved_body_hit("cold", "restored.example", 1040);
let result = cache.dashboard(Some("cold"), 0, 1);
assert_eq!(result.domains.len(), 1);
assert_eq!(result.domains[0].counters.saved_body_bytes, 1040);
}
#[test]
fn per_domain_counter_cardinality_is_bounded() {
let cache = Cache::new(7);