v0.34.0_1
This commit is contained in:
1 parent
9cfc4588cc
commit
f20c8da718
25 files changed
+694
-117
No files matched your search
Vendored
+11
-32
@@ -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,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
Vendored
+2
@@ -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),
|
||||
|
||||
Vendored
+2
-7
@@ -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
|
||||
|
||||
Vendored
+1
-3
@@ -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();
|
||||
|
||||
Vendored
+1
@@ -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");
|
||||
|
||||
Vendored
-22
@@ -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 {
|
||||
|
||||
Vendored
+1
@@ -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>,
|
||||
|
||||
Vendored
+187
@@ -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
|
||||
}
|
||||
}
|
||||
Vendored
+51
@@ -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);
|
||||
|
||||
+17
-4
@@ -62,12 +62,25 @@ impl VhostMetrics {
|
||||
Err(observed) => current = observed,
|
||||
}
|
||||
}
|
||||
for (index, threshold) in DURATION_BUCKETS_US.iter().enumerate() {
|
||||
if micros <= *threshold {
|
||||
self.buckets[index].fetch_add(1, Ordering::Relaxed);
|
||||
// Write exactly one exclusive histogram bin per exchange. Prometheus
|
||||
// cumulative le=... buckets are reconstructed only when scraped.
|
||||
// This removes up to 11 contended atomics for short 1 KiB responses.
|
||||
let bucket = DURATION_BUCKETS_US.partition_point(|threshold| micros > *threshold);
|
||||
self.buckets[bucket].fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// A scrape-consistent cumulative representation of exclusive bins. The
|
||||
/// sum of atomic snapshots is monotonic and the final count includes +Inf.
|
||||
pub fn duration_histogram(&self) -> ([u64; DURATION_BUCKETS_US.len()], u64) {
|
||||
let mut cumulative = [0u64; DURATION_BUCKETS_US.len()];
|
||||
let mut count = 0u64;
|
||||
for (index, bin) in self.buckets.iter().enumerate() {
|
||||
count = count.saturating_add(bin.load(Ordering::Relaxed));
|
||||
if index < DURATION_BUCKETS_US.len() {
|
||||
cumulative[index] = count;
|
||||
}
|
||||
}
|
||||
self.buckets[DURATION_BUCKETS_US.len()].fetch_add(1, Ordering::Relaxed);
|
||||
(cumulative, count)
|
||||
}
|
||||
|
||||
pub fn view(&self) -> VhostMetricsView {
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
mod vhost_tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn exclusive_bins_reconstruct_exact_prometheus_cumulative_boundaries() {
|
||||
let m = VhostMetrics::default();
|
||||
m.active.store(5, Ordering::Relaxed);
|
||||
for micros in [1, 5_000, 5_001, 25_000, 5_000_001] {
|
||||
m.complete(200, false, std::time::Duration::from_micros(micros), 0, 1040);
|
||||
}
|
||||
let (cumulative, total) = m.duration_histogram();
|
||||
assert_eq!(total, 5);
|
||||
assert_eq!(cumulative, [2, 3, 4, 4, 4, 4, 4, 4, 4, 4]);
|
||||
assert_eq!(n(&m.buckets[0]), 2);
|
||||
assert_eq!(n(&m.buckets[1]), 1);
|
||||
assert_eq!(n(&m.buckets[2]), 1);
|
||||
assert_eq!(n(&m.buckets[10]), 1);
|
||||
assert_eq!(n(&m.completed), 5);
|
||||
assert_eq!(n(&m.active), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn streaming_completion_and_histogram() {
|
||||
let m = VhostMetrics::default();
|
||||
@@ -10,7 +28,12 @@ mod vhost_tests {
|
||||
assert_eq!(n(&m.status[4]), 1);
|
||||
assert_eq!(n(&m.buckets[1]), 0);
|
||||
assert_eq!(n(&m.buckets[2]), 1);
|
||||
assert_eq!(n(&m.buckets[10]), 1);
|
||||
assert_eq!(n(&m.buckets[10]), 0); // exclusive +Inf bin
|
||||
let (buckets, count) = m.duration_histogram();
|
||||
assert_eq!(buckets[1], 0);
|
||||
assert_eq!(buckets[2], 1);
|
||||
assert_eq!(buckets[9], 1);
|
||||
assert_eq!(count, 1);
|
||||
let view = m.view();
|
||||
assert_eq!(view.requests, 0);
|
||||
assert_eq!(view.completed, 1);
|
||||
|
||||
@@ -240,16 +240,9 @@ fn render_histograms(
|
||||
"rust_proxy_vhost_exchange_duration_seconds_sum{{{l}}} {:.6}",
|
||||
m.duration_us.load(Ordering::Relaxed) as f64 / 1e6
|
||||
);
|
||||
// Read the cumulative last bucket once for +Inf and count.
|
||||
let count =
|
||||
m.buckets[crate::metrics::DURATION_BUCKETS_US.len()].load(Ordering::Relaxed);
|
||||
let mut previous = 0;
|
||||
let (buckets, count) = m.duration_histogram();
|
||||
for (i, threshold) in crate::metrics::DURATION_BUCKETS_US.iter().enumerate() {
|
||||
let value = m.buckets[i]
|
||||
.load(Ordering::Relaxed)
|
||||
.max(previous)
|
||||
.min(count);
|
||||
previous = value;
|
||||
let value = buckets[i];
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"rust_proxy_vhost_exchange_duration_seconds_bucket{{{l},le=\"{}\"}} {value}",
|
||||
@@ -275,15 +268,9 @@ fn render_histograms(
|
||||
"rust_proxy_docker_vhost_exchange_duration_seconds_sum{{{l}}} {:.6}",
|
||||
m.duration_us.load(Ordering::Relaxed) as f64 / 1e6
|
||||
);
|
||||
let count =
|
||||
m.buckets[crate::metrics::DURATION_BUCKETS_US.len()].load(Ordering::Relaxed);
|
||||
let mut previous = 0;
|
||||
let (buckets, count) = m.duration_histogram();
|
||||
for (i, threshold) in crate::metrics::DURATION_BUCKETS_US.iter().enumerate() {
|
||||
let value = m.buckets[i]
|
||||
.load(Ordering::Relaxed)
|
||||
.max(previous)
|
||||
.min(count);
|
||||
previous = value;
|
||||
let value = buckets[i];
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"rust_proxy_docker_vhost_exchange_duration_seconds_bucket{{{l},le=\"{}\"}} {value}",
|
||||
|
||||
+3
-3
@@ -218,9 +218,9 @@ impl Drop for Exchange {
|
||||
} else {
|
||||
"complete"
|
||||
};
|
||||
if let Some(vhost) = self.vhost.clone() {
|
||||
self.traffic.push(crate::traffic::Sample {
|
||||
vhost,
|
||||
if let Some(vhost) = self.vhost.as_ref() {
|
||||
self.traffic.push_lazy(|| crate::traffic::Sample {
|
||||
vhost: vhost.clone(),
|
||||
route: route.clone(),
|
||||
method: self.method.clone(),
|
||||
path: self.path.clone(),
|
||||
|
||||
+12
-2
@@ -243,8 +243,18 @@ impl Analytics {
|
||||
}
|
||||
|
||||
pub fn push(&self, sample: Sample) {
|
||||
if self.tx.try_send(sample).is_err() {
|
||||
self.dropped.fetch_add(1, Ordering::Relaxed);
|
||||
self.push_lazy(|| sample);
|
||||
}
|
||||
|
||||
/// Acquire queue capacity before constructing owned analytics fields.
|
||||
/// Overflow accounting is identical to `push`, and the queue remains
|
||||
/// bounded. In saturated traffic the rejected sample allocates nothing.
|
||||
pub fn push_lazy(&self, make_sample: impl FnOnce() -> Sample) {
|
||||
match self.tx.try_reserve() {
|
||||
Ok(permit) => permit.send(make_sample()),
|
||||
Err(_) => {
|
||||
self.dropped.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,28 @@ mod tests {
|
||||
assert_eq!(view.client_ips[0].count, 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn saturated_analytics_queue_does_not_build_discarded_samples() {
|
||||
let analytics = Analytics::start(CancellationToken::new());
|
||||
let made = AtomicU64::new(0);
|
||||
// No .await until the queue is filled: on this current-thread test
|
||||
// runtime the consumer has not removed a sample in the background.
|
||||
for _ in 0..(QUEUE_CAPACITY + 5) {
|
||||
analytics.push_lazy(|| {
|
||||
made.fetch_add(1, Ordering::Relaxed);
|
||||
Sample {
|
||||
vhost: "site".into(), route: None, method: "GET".into(),
|
||||
path: "/".into(), client_ip: None, status: 200,
|
||||
duration_ms: 1, request_bytes: 0, response_bytes: 1040,
|
||||
cache: "BYPASS", source: "reverse_proxy",
|
||||
user_agent: None, country: None,
|
||||
}
|
||||
});
|
||||
}
|
||||
assert_eq!(made.load(Ordering::Relaxed), QUEUE_CAPACITY as u64);
|
||||
assert_eq!(analytics.dropped.load(Ordering::Relaxed), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prometheus_dimensions_are_fixed_cardinality() {
|
||||
assert_eq!(method_family("GET"), "GET");
|
||||
|
||||
Reference in new issue
Block a user