Skip to main content

saluki_core/observability/metrics/
mod.rs

1//! Internal metrics support.
2//!
3//! Includes the [`MetricsStream`] broadcast of internally emitted metrics events and the
4//! [`Reflector`]-based [`AggregatedMetricsState`] view that downstream callers query for
5//! Prometheus exposition.
6
7use std::{
8    num::NonZeroUsize,
9    pin::Pin,
10    sync::{
11        atomic::{AtomicBool, AtomicU32, Ordering},
12        Arc, LazyLock, Mutex, OnceLock,
13    },
14    task::{self, ready, Poll},
15    time::Duration,
16};
17
18use async_trait::async_trait;
19use futures::Stream;
20use metrics::{
21    atomics::AtomicU64, Counter, CounterFn, Gauge, GaugeFn, Histogram, HistogramFn, Key, KeyName, Level, Metadata,
22    Recorder, SetRecorderError, SharedString, Unit,
23};
24use metrics_util::storage::AtomicBucket;
25use saluki_common::{collections::FastHashMap, sync::shutdown::ShutdownHandle};
26use saluki_error::GenericError;
27use tokio::{
28    select,
29    sync::broadcast::{self, error::RecvError, Receiver},
30};
31use tokio_util::sync::ReusableBoxFuture;
32use tracing::debug;
33
34use crate::{
35    data_model::{
36        event::{
37            metric::{
38                context::{Context, ContextResolver, ContextResolverBuilder},
39                *,
40            },
41            Event,
42        },
43        origin::RawOrigin,
44        tags::{Tag, TagSet},
45    },
46    runtime::{InitializationError, Supervisable, SupervisorFuture},
47};
48
49mod aggregated;
50pub use self::aggregated::{
51    get_shared_metrics_state, initialize_shared_metrics_state, AggregatedMetricValue, AggregatedMetricsProcessor,
52    AggregatedMetricsState, SharedMetricsWorker,
53};
54
55mod api;
56pub use self::api::InternalTelemetryAPIWorker;
57
58mod histogram;
59pub use self::histogram::AggregatedHistogram;
60
61mod processor;
62pub use self::processor::TelemetryProcessor;
63
64mod reflector;
65pub use self::reflector::{Processor, Reflector, ReflectorWorker};
66
67mod remapper;
68pub use self::remapper::{RemappedMetric, RemapperRule};
69
70const FLUSH_INTERVAL: Duration = Duration::from_secs(1);
71const INTERNAL_METRICS_INTERNER_SIZE: NonZeroUsize = NonZeroUsize::new(16_384).unwrap();
72
73/// Number of consecutive flush intervals that a metric must go without a live handle and without being re-registered
74/// before it is evicted from the registry.
75///
76/// This grace period prevents metrics that are emitted through `metrics` macros without caching the returned handle --
77/// and that therefore re-register on every call -- from being churned in and out of the registry while they are still
78/// being emitted. Naturally, this only applies to metrics which re-register within `IDLE_EVICT_INTERVALS *
79/// FLUSH_INTERVAL`, which is 3 seconds by default... but that's a tradeoff we're making here to avoid unbounded growth.
80const IDLE_EVICT_INTERVALS: u32 = 3;
81
82static RECEIVER_STATE: OnceLock<Arc<State>> = OnceLock::new();
83
84/// A batch of metric updates produced by a single flush.
85#[derive(Clone, Default)]
86pub struct MetricsSnapshot {
87    /// Updates to active metrics.
88    pub upserts: Vec<Event>,
89
90    /// Metrics which are no longer active and should be removed.
91    pub evictions: Vec<Context>,
92}
93
94/// A [`MetricsSnapshot`] that can be cheaply cloned and shared between multiple consumers.
95pub type SharedMetricsSnapshot = Arc<MetricsSnapshot>;
96
97struct Handle<T> {
98    inner: T,
99    level: Level,
100    idle: AtomicU32,
101}
102
103impl<T> Handle<T> {
104    const fn new(inner: T, level: Level) -> Self {
105        Self {
106            inner,
107            level,
108            idle: AtomicU32::new(0),
109        }
110    }
111
112    fn level(&self) -> Level {
113        self.level
114    }
115
116    fn reset_idle(&self) {
117        self.idle.store(0, Ordering::Relaxed);
118    }
119
120    fn bump_idle(&self) -> u32 {
121        self.idle.fetch_add(1, Ordering::Relaxed).saturating_add(1)
122    }
123}
124
125struct CounterInner {
126    value: AtomicU64,
127}
128
129impl Handle<CounterInner> {
130    const fn new_counter(level: Level) -> Self {
131        Self::new(
132            CounterInner {
133                value: AtomicU64::new(0),
134            },
135            level,
136        )
137    }
138
139    /// Consumes the accumulated delta since the last flush, resetting the counter to zero.
140    fn consume(&self) -> u64 {
141        self.inner.value.swap(0, Ordering::Relaxed)
142    }
143}
144
145impl CounterFn for Handle<CounterInner> {
146    fn increment(&self, value: u64) {
147        CounterFn::increment(&self.inner.value, value);
148    }
149
150    fn absolute(&self, value: u64) {
151        CounterFn::absolute(&self.inner.value, value);
152    }
153}
154
155struct GaugeInner {
156    value: AtomicU64,
157    // Tracks whether or not the gauge has been written since the last flush, since the gauge _could_
158    // be modified in a way where the value seen by two consecutive flushes is the same, even though
159    // it _was_ modified between the two and should be considered non-idle.
160    dirty: AtomicBool,
161}
162
163impl Handle<GaugeInner> {
164    const fn new_gauge(level: Level) -> Self {
165        Self::new(
166            GaugeInner {
167                value: AtomicU64::new(0),
168                dirty: AtomicBool::new(false),
169            },
170            level,
171        )
172    }
173
174    /// Reads the current gauge value.
175    fn load(&self) -> f64 {
176        f64::from_bits(self.inner.value.load(Ordering::Relaxed))
177    }
178
179    /// Returns whether the gauge was written since the last flush, clearing the flag.
180    fn take_dirty(&self) -> bool {
181        self.inner.dirty.swap(false, Ordering::Relaxed)
182    }
183}
184
185impl GaugeFn for Handle<GaugeInner> {
186    fn increment(&self, value: f64) {
187        GaugeFn::increment(&self.inner.value, value);
188        self.inner.dirty.store(true, Ordering::Relaxed);
189    }
190
191    fn decrement(&self, value: f64) {
192        GaugeFn::decrement(&self.inner.value, value);
193        self.inner.dirty.store(true, Ordering::Relaxed);
194    }
195
196    fn set(&self, value: f64) {
197        GaugeFn::set(&self.inner.value, value);
198        self.inner.dirty.store(true, Ordering::Relaxed);
199    }
200}
201
202/// Storage for a single histogram, shared between the registry and every caller-held [`Histogram`].
203struct HistogramInner {
204    value: AtomicBucket<f64>,
205}
206
207impl Handle<HistogramInner> {
208    fn new_histogram(level: Level) -> Self {
209        Self::new(
210            HistogramInner {
211                value: AtomicBucket::new(),
212            },
213            level,
214        )
215    }
216
217    /// Drains all recorded samples since the last flush into `out`, clearing the histogram.
218    fn drain_into(&self, out: &mut Vec<f64>) {
219        self.inner.value.clear_with(|samples| out.extend(samples));
220    }
221}
222
223impl HistogramFn for Handle<HistogramInner> {
224    fn record(&self, value: f64) {
225        self.inner.value.push(value);
226    }
227}
228
229/// A registry for all internal metrics.
230///
231/// Optimized for simplicity and the ability to efficiently track metric state and evict idle metrics. Not optimized for
232/// high concurrency with regards to registration, so metric handles should always be held for as long as possible, when
233/// possible.
234#[derive(Default)]
235struct MetricsRegistry {
236    maps: Mutex<RegistryMaps>,
237}
238
239#[derive(Default)]
240struct RegistryMaps {
241    counters: FastHashMap<Key, Arc<Handle<CounterInner>>>,
242    gauges: FastHashMap<Key, Arc<Handle<GaugeInner>>>,
243    histograms: FastHashMap<Key, Arc<Handle<HistogramInner>>>,
244}
245
246impl MetricsRegistry {
247    /// Returns a handle to the counter for `key`, creating it at `level` if it doesn't yet exist.
248    ///
249    /// The level is fixed when the metric is first created; re-registration only refreshes the idle
250    /// counter (the activity signal that keeps actively emitted metrics from being evicted).
251    fn get_or_create_counter(&self, key: &Key, level: Level) -> Counter {
252        let mut maps = self.maps.lock().unwrap();
253        let handle = if let Some(handle) = maps.counters.get(key) {
254            handle.reset_idle();
255            Arc::clone(handle)
256        } else {
257            let handle = Arc::new(Handle::new_counter(level));
258            maps.counters.insert(key.clone(), Arc::clone(&handle));
259            handle
260        };
261
262        Counter::from_arc(handle)
263    }
264
265    fn remove_counter(&self, key: &Key) -> Option<Arc<Handle<CounterInner>>> {
266        let mut maps = self.maps.lock().unwrap();
267        maps.counters.remove(key)
268    }
269
270    /// Returns a handle to the gauge for `key`, creating it at `level` if it doesn't yet exist.
271    fn get_or_create_gauge(&self, key: &Key, level: Level) -> Gauge {
272        let mut maps = self.maps.lock().unwrap();
273        let handle = if let Some(handle) = maps.gauges.get(key) {
274            handle.reset_idle();
275            Arc::clone(handle)
276        } else {
277            let handle = Arc::new(Handle::new_gauge(level));
278            maps.gauges.insert(key.clone(), Arc::clone(&handle));
279            handle
280        };
281
282        Gauge::from_arc(handle)
283    }
284
285    /// Returns a handle to the histogram for `key`, creating it at `level` if it doesn't yet exist.
286    fn get_or_create_histogram(&self, key: &Key, level: Level) -> Histogram {
287        let mut maps = self.maps.lock().unwrap();
288        let handle = if let Some(handle) = maps.histograms.get(key) {
289            handle.reset_idle();
290            Arc::clone(handle)
291        } else {
292            let handle = Arc::new(Handle::new_histogram(level));
293            maps.histograms.insert(key.clone(), Arc::clone(&handle));
294            handle
295        };
296
297        Histogram::from_arc(handle)
298    }
299}
300
301/// Handle to the metrics filter.
302///
303/// Allows for overriding the current metrics filter level, which influences which metrics are emitted to downstream
304/// receivers.
305pub struct FilterHandle {
306    state: Arc<State>,
307}
308
309impl FilterHandle {
310    /// Overrides the current metrics filter level.
311    pub fn override_filter(&self, level: Level) {
312        *self.state.current_level.lock().unwrap() = level;
313    }
314
315    /// Resets the metrics filter level to the default that was configured when the metrics subsystem was initialized.
316    pub fn reset_filter(&self) {
317        *self.state.current_level.lock().unwrap() = self.state.default_level;
318    }
319}
320
321struct State {
322    registry: MetricsRegistry,
323    flush_tx: broadcast::Sender<SharedMetricsSnapshot>,
324    metrics_prefix: String,
325    default_level: Level,
326    current_level: Mutex<Level>,
327    idle_evict_intervals: u32,
328    flush_interval: Duration,
329}
330
331struct MetricsRecorder {
332    state: Arc<State>,
333}
334
335impl MetricsRecorder {
336    fn new(metrics_prefix: String, default_level: Level) -> Self {
337        let (flush_tx, _) = broadcast::channel(2);
338        Self {
339            state: Arc::new(State {
340                registry: MetricsRegistry::default(),
341                flush_tx,
342                metrics_prefix,
343                default_level,
344                current_level: Mutex::new(default_level),
345                idle_evict_intervals: IDLE_EVICT_INTERVALS,
346                flush_interval: FLUSH_INTERVAL,
347            }),
348        }
349    }
350
351    fn filter_handle(&self) -> FilterHandle {
352        FilterHandle {
353            state: Arc::clone(&self.state),
354        }
355    }
356
357    fn install(self) -> Result<(), SetRecorderError<Self>> {
358        let state = Arc::clone(&self.state);
359        metrics::set_global_recorder(self)?;
360
361        if RECEIVER_STATE.set(state).is_err() {
362            panic!("metrics receiver should never be set prior to global recorder being installed");
363        }
364
365        Ok(())
366    }
367
368    fn prefix_key(&self, key: &Key) -> Key {
369        Key::from_parts(format!("{}.{}", self.state.metrics_prefix, key.name()), key.labels())
370    }
371}
372
373impl Recorder for MetricsRecorder {
374    fn describe_counter(&self, _: KeyName, _: Option<Unit>, _: SharedString) {}
375    fn describe_gauge(&self, _: KeyName, _: Option<Unit>, _: SharedString) {}
376    fn describe_histogram(&self, _: KeyName, _: Option<Unit>, _: SharedString) {}
377
378    fn register_counter(&self, key: &Key, metadata: &Metadata<'_>) -> Counter {
379        let prefixed_key = self.prefix_key(key);
380        self.state
381            .registry
382            .get_or_create_counter(&prefixed_key, *metadata.level())
383    }
384
385    fn register_gauge(&self, key: &Key, metadata: &Metadata<'_>) -> Gauge {
386        let prefixed_key = self.prefix_key(key);
387        self.state
388            .registry
389            .get_or_create_gauge(&prefixed_key, *metadata.level())
390    }
391
392    fn register_histogram(&self, key: &Key, metadata: &Metadata<'_>) -> Histogram {
393        let prefixed_key = self.prefix_key(key);
394        self.state
395            .registry
396            .get_or_create_histogram(&prefixed_key, *metadata.level())
397    }
398}
399
400/// Deletes an internal counter from the metrics registry.
401///
402/// If the counter exists and metrics snapshots are being consumed, this sends any pending counter
403/// delta before the eviction update so downstream consumers do not lose the final increment.
404pub fn delete_counter(key: Key) -> bool {
405    let Some(state) = RECEIVER_STATE.get() else {
406        return false;
407    };
408
409    let prefixed_key = Key::from_parts(format!("{}.{}", state.metrics_prefix, key.name()), key.labels());
410    let Some(snapshot) = delete_counter_from_state(state, &prefixed_key) else {
411        return false;
412    };
413
414    if state.flush_tx.receiver_count() > 0 {
415        let _ = state.flush_tx.send(Arc::new(snapshot));
416    }
417
418    true
419}
420
421fn delete_counter_from_state(state: &State, prefixed_key: &Key) -> Option<MetricsSnapshot> {
422    let handle = state.registry.remove_counter(prefixed_key)?;
423    let delta = handle.consume();
424    let context = context_from_key(prefixed_key);
425    let current_level = *state.current_level.lock().unwrap();
426
427    let mut snapshot = MetricsSnapshot {
428        upserts: Vec::new(),
429        evictions: vec![context.clone()],
430    };
431    if delta > 0 && handle.level() >= current_level {
432        snapshot
433            .upserts
434            .push(Event::Metric(Metric::counter(context, delta as f64)));
435    }
436
437    Some(snapshot)
438}
439
440fn context_from_key(key: &Key) -> Context {
441    let tags = key
442        .labels()
443        .map(|l| Tag::from(format!("{}:{}", l.key(), l.value())))
444        .collect::<TagSet>();
445
446    Context::from_parts(key.name().to_string(), tags)
447}
448
449/// Internal metrics stream
450///
451/// Used to receive periodic snapshots of the internal metrics registry, which contains all metrics that are currently
452/// active within the process.
453pub struct MetricsStream {
454    inner: ReusableBoxFuture<
455        'static,
456        (
457            Result<SharedMetricsSnapshot, RecvError>,
458            Receiver<SharedMetricsSnapshot>,
459        ),
460    >,
461}
462
463impl MetricsStream {
464    /// Creates a new `MetricsStream` that receives updates from the internal metrics registry.
465    pub fn register() -> Self {
466        let state = RECEIVER_STATE.get().expect("metrics receiver should be set");
467        Self {
468            inner: ReusableBoxFuture::new(make_rx_future(state.flush_tx.subscribe())),
469        }
470    }
471}
472
473impl Stream for MetricsStream {
474    type Item = SharedMetricsSnapshot;
475
476    fn poll_next(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll<Option<Self::Item>> {
477        loop {
478            // Poll the receiver, and rearm the future once we actually resolve it.
479            let (result, rx) = ready!(self.inner.poll(cx));
480            self.inner.set(make_rx_future(rx));
481
482            match result {
483                Ok(item) => return Poll::Ready(Some(item)),
484                Err(RecvError::Closed) => return Poll::Ready(None),
485                Err(RecvError::Lagged(n)) => {
486                    debug!(
487                        missed_payloads = n,
488                        "Stream lagging behind internal metrics producer. Internal metrics may have been lost."
489                    );
490                    continue;
491                }
492            }
493        }
494    }
495}
496
497async fn make_rx_future(
498    mut rx: Receiver<SharedMetricsSnapshot>,
499) -> (
500    Result<SharedMetricsSnapshot, RecvError>,
501    Receiver<SharedMetricsSnapshot>,
502) {
503    let result = rx.recv().await;
504    (result, rx)
505}
506
507#[derive(Default)]
508struct FlushState {
509    counter_emits: Vec<(Key, f64)>,
510    gauge_emits: Vec<(Key, f64)>,
511    histogram_emits: Vec<(Key, Vec<f64>)>,
512    evicted_keys: Vec<Key>,
513}
514
515impl FlushState {
516    fn clear(&mut self) {
517        self.counter_emits.clear();
518        self.gauge_emits.clear();
519        self.histogram_emits.clear();
520        self.evicted_keys.clear();
521    }
522}
523
524async fn flush_metrics() {
525    let mut context_resolver = MetricsContextResolver::new(INTERNAL_METRICS_INTERNER_SIZE);
526
527    let state = RECEIVER_STATE.get().expect("metrics receiver should be set");
528
529    let mut flush_interval = tokio::time::interval(state.flush_interval);
530    flush_interval.tick().await;
531
532    let mut flush_state = FlushState::default();
533
534    loop {
535        flush_interval.tick().await;
536        flush_once(state, &mut context_resolver, &mut flush_state);
537    }
538}
539
540/// Performs a single flush pass over the registry: consumes each metric's value, evicts metrics that
541/// are no longer referenced and have gone idle, and broadcasts the resulting metric values and
542/// evictions to downstream consumers.
543fn flush_once(state: &State, context_resolver: &mut MetricsContextResolver, flush_state: &mut FlushState) {
544    let has_listeners = state.flush_tx.receiver_count() > 0;
545    let current_level = *state.current_level.lock().unwrap();
546    let idle_threshold = state.idle_evict_intervals;
547    let mut snapshot = MetricsSnapshot::default();
548
549    flush_state.clear();
550    let FlushState {
551        counter_emits,
552        gauge_emits,
553        histogram_emits,
554        evicted_keys,
555    } = flush_state;
556
557    {
558        let mut maps = state.registry.maps.lock().unwrap();
559
560        // Counters: a counter is never idle so long as it has a non-zero delta during a flush _or_ has
561        // an active reference (strong count > 1).
562        //
563        // The strong-count check precedes `consume()`, with an Acquire fence on the orphaned path:
564        // observing `strong_count == 1` synchronizes-with the dropping thread's release, which
565        // guarantees the following `consume()` observes that thread's final increment.
566        maps.counters.retain(|key, handle| {
567            let idle = handle.bump_idle();
568            let orphaned = Arc::strong_count(handle) == 1;
569            if orphaned {
570                std::sync::atomic::fence(Ordering::Acquire);
571            }
572            let delta = handle.consume();
573
574            if orphaned && delta == 0 && idle >= idle_threshold {
575                evicted_keys.push(key.clone());
576                return false;
577            }
578
579            if has_listeners && handle.level() >= current_level {
580                counter_emits.push((key.clone(), delta as f64));
581            }
582            true
583        });
584
585        // Gauges: a gauge is never idle so long as it was touched ("dirty") prior to a flush _or_ has
586        // an active reference (strong count > 1).
587        maps.gauges.retain(|key, handle| {
588            let idle = handle.bump_idle();
589            let orphaned = Arc::strong_count(handle) == 1;
590            if orphaned {
591                std::sync::atomic::fence(Ordering::Acquire);
592            }
593            let value = handle.load();
594            let written = handle.take_dirty();
595
596            if orphaned && !written && idle >= idle_threshold {
597                evicted_keys.push(key.clone());
598                return false;
599            }
600
601            if has_listeners && handle.level() >= current_level {
602                gauge_emits.push((key.clone(), value));
603            }
604            true
605        });
606
607        // Histograms: a histogram is never idle so long as it has recorded samples prior to a flush _or_
608        // has an active reference (strong count > 1).
609        //
610        // The strong-count check precedes the drain, with an Acquire fence on the orphaned path, so the
611        // drain observes a dropping thread's final `record()`.
612        maps.histograms.retain(|key, handle| {
613            let idle = handle.bump_idle();
614            let orphaned = Arc::strong_count(handle) == 1;
615            if orphaned {
616                std::sync::atomic::fence(Ordering::Acquire);
617            }
618            let mut histogram_samples = Vec::new();
619            handle.drain_into(&mut histogram_samples);
620            let had_samples = !histogram_samples.is_empty();
621
622            if orphaned && !had_samples && idle >= idle_threshold {
623                evicted_keys.push(key.clone());
624                return false;
625            }
626
627            if has_listeners && had_samples && handle.level() >= current_level {
628                histogram_emits.push((key.clone(), histogram_samples));
629            }
630            true
631        });
632    }
633
634    // For every evicted key we collected, take its cached context (if it exists) and add it to the
635    // snapshot's evictions list.
636    for key in evicted_keys {
637        if let Some(context) = context_resolver.take(key) {
638            if has_listeners {
639                snapshot.evictions.push(context);
640            }
641        }
642    }
643
644    if !has_listeners {
645        return;
646    }
647
648    for (key, value) in counter_emits.drain(..) {
649        let context = context_resolver.resolve_from_key(key);
650        snapshot.upserts.push(Event::Metric(Metric::counter(context, value)));
651    }
652    for (key, value) in gauge_emits.drain(..) {
653        let context = context_resolver.resolve_from_key(key);
654        snapshot.upserts.push(Event::Metric(Metric::gauge(context, value)));
655    }
656    for (key, samples) in histogram_emits.drain(..) {
657        let context = context_resolver.resolve_from_key(key);
658        snapshot
659            .upserts
660            .push(Event::Metric(Metric::histogram(context, &samples[..])));
661    }
662
663    if !snapshot.upserts.is_empty() || !snapshot.evictions.is_empty() {
664        let _ = state.flush_tx.send(Arc::new(snapshot));
665    }
666}
667
668struct MetricsContextResolver {
669    context_resolver: ContextResolver,
670    key_context_cache: FastHashMap<Key, Context>,
671}
672
673impl MetricsContextResolver {
674    fn new(resolver_interner_size_bytes: NonZeroUsize) -> Self {
675        Self {
676            // Set up our context resolver without caching, since we will be caching the contexts ourselves.
677            context_resolver: ContextResolverBuilder::from_name("core/internal_metrics")
678                .expect("resolver name is not empty")
679                .with_interner_capacity_bytes(resolver_interner_size_bytes)
680                .without_caching()
681                .build(),
682            key_context_cache: FastHashMap::default(),
683        }
684    }
685
686    fn resolve_from_key(&mut self, key: Key) -> Context {
687        static SELF_ORIGIN_INFO: LazyLock<RawOrigin<'static>> = LazyLock::new(|| {
688            let mut origin_info = RawOrigin::default();
689            origin_info.set_process_id(std::process::id());
690            origin_info
691        });
692
693        // Check the cache first.
694        if let Some(context) = self.key_context_cache.get(&key) {
695            return context.clone();
696        }
697
698        // We don't have the context cached, so we need to resolve it.
699        let tags = key
700            .labels()
701            .map(|l| Tag::from(format!("{}:{}", l.key(), l.value())))
702            .collect::<TagSet>();
703
704        let context = self
705            .context_resolver
706            .resolve(key.name(), &tags, Some(SELF_ORIGIN_INFO.clone()))
707            .expect("resolver should always allow falling back");
708
709        self.key_context_cache.insert(key, context.clone());
710        context
711    }
712
713    /// Removes a key's cached context, returning it if present.
714    ///
715    /// Returns `Some` only if the key had been resolved before (that is, the metric was emitted at
716    /// least once), which is what tells the flush loop whether a downstream eviction needs to be sent.
717    fn take(&mut self, key: &Key) -> Option<Context> {
718        self.key_context_cache.remove(key)
719    }
720}
721
722/// Initializes the metrics subsystem with the given metrics prefix and default filter level.
723///
724/// `default_level` sets the initial filter level for emitted metrics, and is also what the filter is restored to when
725/// [`FilterHandle::reset_filter`] is invoked. Metrics whose level is more verbose than this default are filtered out
726/// until a runtime override is applied via [`FilterHandle::override_filter`].
727///
728/// Returns a [`FilterHandle`] for adjusting the runtime metrics filter, plus a [`MetricsFlusherWorker`]
729/// that must be added to a [`Supervisor`][crate::runtime::Supervisor] in order to drive the periodic
730/// flush loop. Internal metrics aren't propagated to subscribers until the worker is running.
731///
732/// # Errors
733///
734/// If a global recorder was already installed, an error will be returned.
735pub async fn initialize_metrics(
736    metrics_prefix: String, default_level: Level,
737) -> Result<(FilterHandle, MetricsFlusherWorker), GenericError> {
738    let recorder = MetricsRecorder::new(metrics_prefix, default_level);
739    let filter_handle = recorder.filter_handle();
740    recorder.install()?;
741
742    Ok((filter_handle, MetricsFlusherWorker))
743}
744
745/// A worker that periodically flushes the internal metrics registry to broadcast subscribers.
746///
747/// Wraps the internal flush loop and runs it under a [`Supervisor`][crate::runtime::Supervisor]. Must
748/// only be added to a supervisor after [`initialize_metrics`] has been called -- the flush loop
749/// reads from the global recorder state set by that call.
750pub struct MetricsFlusherWorker;
751
752#[async_trait]
753impl Supervisable for MetricsFlusherWorker {
754    fn name(&self) -> &str {
755        "flusher"
756    }
757
758    async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
759        Ok(Box::pin(async move {
760            select! {
761                _ = process_shutdown => {},
762                _ = flush_metrics() => {},
763            }
764
765            Ok(())
766        }))
767    }
768}
769
770/// Feeds `upserts` through a fresh [`AggregatedMetricsProcessor`] and returns the resulting
771/// [`AggregatedMetricsState`].
772#[cfg(test)]
773pub(crate) fn aggregate_upserts(upserts: Vec<Event>) -> AggregatedMetricsState {
774    let processor = AggregatedMetricsProcessor;
775    let state = processor.build_initial_state();
776    processor.process(
777        MetricsSnapshot {
778            upserts,
779            evictions: Vec::new(),
780        },
781        &state,
782    );
783    state
784}
785
786#[cfg(test)]
787mod tests {
788    use metrics::Label;
789    use saluki_common::collections::FastHashSet;
790
791    use super::*;
792
793    const HIGH_CARDINALITY_COUNTER_NAME: &str = "high_cardinality_counter";
794    const HIGH_CARDINALITY_COUNTERS: usize = 64;
795
796    // We keep a live receiver in each test so `flush_once` observes a listener and emits updates.
797    fn make_state(idle_evict_intervals: u32) -> (Arc<State>, Receiver<SharedMetricsSnapshot>) {
798        let (flush_tx, rx) = broadcast::channel(64);
799        let state = Arc::new(State {
800            registry: MetricsRegistry::default(),
801            flush_tx,
802            metrics_prefix: "test".to_string(),
803            default_level: Level::TRACE,
804            current_level: Mutex::new(Level::TRACE),
805            idle_evict_intervals,
806            flush_interval: FLUSH_INTERVAL,
807        });
808        (state, rx)
809    }
810
811    fn resolver() -> MetricsContextResolver {
812        MetricsContextResolver::new(INTERNAL_METRICS_INTERNER_SIZE)
813    }
814
815    // Mimics `MetricsRecorder::register_counter` against a raw key (no prefixing needed in tests).
816    fn register_counter(state: &State, name: &'static str) -> Counter {
817        state
818            .registry
819            .get_or_create_counter(&Key::from_name(name), Level::TRACE)
820    }
821
822    fn register_counter_with_origin(state: &State, name: &'static str, origin: &str) -> Counter {
823        let key = Key::from_parts(name, vec![Label::new("origin", origin.to_string())]);
824        state.registry.get_or_create_counter(&key, Level::TRACE)
825    }
826
827    fn register_gauge(state: &State, name: &'static str) -> Gauge {
828        state.registry.get_or_create_gauge(&Key::from_name(name), Level::TRACE)
829    }
830
831    fn register_histogram(state: &State, name: &'static str) -> Histogram {
832        state
833            .registry
834            .get_or_create_histogram(&Key::from_name(name), Level::TRACE)
835    }
836
837    fn counter_present(state: &State, name: &'static str) -> bool {
838        state
839            .registry
840            .maps
841            .lock()
842            .unwrap()
843            .counters
844            .contains_key(&Key::from_name(name))
845    }
846
847    fn counter_count(state: &State) -> usize {
848        state.registry.maps.lock().unwrap().counters.len()
849    }
850
851    fn gauge_present(state: &State, name: &'static str) -> bool {
852        state
853            .registry
854            .maps
855            .lock()
856            .unwrap()
857            .gauges
858            .contains_key(&Key::from_name(name))
859    }
860
861    fn histogram_present(state: &State, name: &'static str) -> bool {
862        state
863            .registry
864            .maps
865            .lock()
866            .unwrap()
867            .histograms
868            .contains_key(&Key::from_name(name))
869    }
870
871    fn drain(rx: &mut Receiver<SharedMetricsSnapshot>) -> Vec<MetricsSnapshot> {
872        let mut out = Vec::new();
873        while let Ok(batch) = rx.try_recv() {
874            out.push((*batch).clone());
875        }
876        out
877    }
878
879    fn evicted_names(snapshots: &[MetricsSnapshot]) -> Vec<String> {
880        snapshots
881            .iter()
882            .flat_map(|snapshot| snapshot.evictions.iter().map(|ctx| ctx.name().to_string()))
883            .collect()
884    }
885
886    fn metric_count(state: &AggregatedMetricsState, name: &str) -> usize {
887        let mut count = 0;
888        state.visit_metrics(|context, _| {
889            if context.name() == name {
890                count += 1;
891            }
892        });
893        count
894    }
895
896    #[test]
897    fn delete_counter_from_state_emits_pending_delta_before_eviction() {
898        let (state, _rx) = make_state(IDLE_EVICT_INTERVALS);
899        let key = Key::from_parts(
900            "deleted_counter",
901            vec![Label::new("origin", "container_id://deleted".to_string())],
902        );
903
904        let counter = state.registry.get_or_create_counter(&key, Level::TRACE);
905        counter.increment(7);
906        drop(counter);
907
908        let snapshot = delete_counter_from_state(&state, &key).expect("counter should be deleted");
909
910        assert!(!state.registry.maps.lock().unwrap().counters.contains_key(&key));
911        assert_eq!(snapshot.evictions.len(), 1);
912        assert_eq!(snapshot.evictions[0].name(), "deleted_counter");
913        assert!(snapshot.evictions[0].tags().has_tag("origin:container_id://deleted"));
914
915        assert_eq!(snapshot.upserts.len(), 1);
916        let Event::Metric(metric) = &snapshot.upserts[0] else {
917            panic!("delete should emit the pending counter delta");
918        };
919        assert_eq!(metric.context(), &snapshot.evictions[0]);
920        let MetricValues::Counter(points) = metric.values() else {
921            panic!("delete should emit a counter metric");
922        };
923        assert_eq!(points.into_iter().map(|(_, value)| value).sum::<f64>(), 7.0);
924    }
925
926    #[tokio::test]
927    async fn evicts_high_cardinality_counters_from_registry_and_aggregated_state() {
928        let (state, mut rx) = make_state(IDLE_EVICT_INTERVALS);
929        let mut resolver = resolver();
930        let mut flush_state = FlushState::default();
931        let agg_processor = AggregatedMetricsProcessor;
932        let agg_state = agg_processor.build_initial_state();
933        let mut expected_eviction_tags = FastHashSet::default();
934        let mut observed_eviction_tags = FastHashSet::default();
935
936        for index in 0..HIGH_CARDINALITY_COUNTERS {
937            let origin = format!("entity://source-{index}");
938            expected_eviction_tags.insert(format!("origin:{origin}"));
939
940            let counter = register_counter_with_origin(&state, HIGH_CARDINALITY_COUNTER_NAME, &origin);
941            counter.increment(1);
942            drop(counter);
943        }
944
945        assert_eq!(counter_count(&state), HIGH_CARDINALITY_COUNTERS);
946
947        for _ in 0..IDLE_EVICT_INTERVALS {
948            flush_once(&state, &mut resolver, &mut flush_state);
949
950            for update in drain(&mut rx) {
951                for context in &update.evictions {
952                    if context.name() != HIGH_CARDINALITY_COUNTER_NAME {
953                        continue;
954                    }
955
956                    for tag in &expected_eviction_tags {
957                        if context.tags().has_tag(tag) {
958                            observed_eviction_tags.insert(tag.clone());
959                        }
960                    }
961                }
962
963                agg_processor.process(update, &agg_state);
964            }
965        }
966
967        assert_eq!(counter_count(&state), 0);
968        assert_eq!(metric_count(&agg_state, HIGH_CARDINALITY_COUNTER_NAME), 0);
969        assert_eq!(observed_eviction_tags, expected_eviction_tags);
970    }
971
972    #[tokio::test]
973    async fn evicts_counter_within_bounded_time_after_handle_dropped() {
974        let (state, mut rx) = make_state(3);
975        let mut resolver = resolver();
976        let mut flush_state = FlushState::default();
977
978        let counter = register_counter(&state, "dropped_counter");
979        counter.increment(5);
980
981        // While the handle is held, the metric is retained and its delta is emitted.
982        flush_once(&state, &mut resolver, &mut flush_state);
983        assert!(counter_present(&state, "dropped_counter"));
984
985        // Drop the last handle; the metric must be reclaimed within `idle_evict_intervals` flushes.
986        drop(counter);
987        let _ = drain(&mut rx);
988        for _ in 0..3 {
989            flush_once(&state, &mut resolver, &mut flush_state);
990        }
991        assert!(!counter_present(&state, "dropped_counter"));
992
993        // The eviction is propagated downstream so the aggregated view drops it too.
994        let updates = drain(&mut rx);
995        assert!(evicted_names(&updates).iter().any(|n| n == "dropped_counter"));
996    }
997
998    #[tokio::test]
999    async fn does_not_evict_while_handle_is_held() {
1000        let (state, _rx) = make_state(3);
1001        let mut resolver = resolver();
1002        let mut flush_state = FlushState::default();
1003
1004        let counter = register_counter(&state, "held_counter");
1005
1006        // Idle well past the eviction threshold without dropping the handle.
1007        for _ in 0..8 {
1008            flush_once(&state, &mut resolver, &mut flush_state);
1009        }
1010
1011        assert!(
1012            counter_present(&state, "held_counter"),
1013            "a held metric must never be evicted"
1014        );
1015        drop(counter);
1016    }
1017
1018    #[tokio::test]
1019    async fn reregistration_keeps_uncached_metric_alive() {
1020        // Models the uncached-macro pattern: a fresh handle is registered (and dropped) every interval.
1021        // Re-registration resets the idle counter, so the metric is never churned out while in use.
1022        let (state, _rx) = make_state(3);
1023        let mut resolver = resolver();
1024        let mut flush_state = FlushState::default();
1025
1026        for _ in 0..8 {
1027            let counter = register_counter(&state, "uncached_counter");
1028            counter.increment(1);
1029            drop(counter);
1030            flush_once(&state, &mut resolver, &mut flush_state);
1031        }
1032        assert!(counter_present(&state, "uncached_counter"));
1033
1034        // Once it stops being re-registered, it is reclaimed within the idle threshold.
1035        for _ in 0..3 {
1036            flush_once(&state, &mut resolver, &mut flush_state);
1037        }
1038        assert!(!counter_present(&state, "uncached_counter"));
1039    }
1040
1041    #[tokio::test]
1042    async fn gauge_reset_to_same_value_is_not_evicted() {
1043        // A gauge re-set to the same value every interval (e.g. a backoff gauge that sits at 0.0) must
1044        // stay alive. Activity is driven by re-registration, not value comparison.
1045        let (state, _rx) = make_state(3);
1046        let mut resolver = resolver();
1047        let mut flush_state = FlushState::default();
1048
1049        for _ in 0..8 {
1050            let gauge = register_gauge(&state, "steady_gauge");
1051            gauge.set(0.0);
1052            drop(gauge);
1053            flush_once(&state, &mut resolver, &mut flush_state);
1054        }
1055        assert!(gauge_present(&state, "steady_gauge"));
1056    }
1057
1058    #[tokio::test]
1059    async fn final_counter_delta_reaches_downstream_before_eviction() {
1060        let (state, mut rx) = make_state(1);
1061        let mut resolver = resolver();
1062        let mut flush_state = FlushState::default();
1063
1064        let counter = register_counter(&state, "final_counter");
1065
1066        // Arm eviction by letting the idle counter reach the threshold while the handle is held.
1067        flush_once(&state, &mut resolver, &mut flush_state);
1068
1069        // Increment and drop the handle in the same inter-flush window. The next flush sees a non-zero
1070        // delta, so it must emit it and defer eviction by one interval (so the delta isn't lost).
1071        counter.increment(7);
1072        drop(counter);
1073        flush_once(&state, &mut resolver, &mut flush_state);
1074        assert!(
1075            counter_present(&state, "final_counter"),
1076            "must not evict in an interval that produced a delta"
1077        );
1078
1079        // The emitted delta must reach the downstream aggregated state.
1080        let agg_processor = AggregatedMetricsProcessor;
1081        let agg_state = agg_processor.build_initial_state();
1082        for update in drain(&mut rx) {
1083            agg_processor.process(update, &agg_state);
1084        }
1085        assert_eq!(agg_state.get_aggregated_with_tags("final_counter", &[]), 7.0);
1086
1087        // The following interval (no delta) finally evicts it.
1088        flush_once(&state, &mut resolver, &mut flush_state);
1089        assert!(!counter_present(&state, "final_counter"));
1090    }
1091
1092    #[tokio::test]
1093    async fn gauge_final_value_reaches_downstream_before_eviction() {
1094        let (state, mut rx) = make_state(1);
1095        let mut resolver = resolver();
1096        let mut flush_state = FlushState::default();
1097
1098        let gauge = register_gauge(&state, "final_gauge");
1099        gauge.set(1.0);
1100
1101        // Arm eviction by letting the idle counter reach the threshold while the handle is held.
1102        flush_once(&state, &mut resolver, &mut flush_state);
1103
1104        // Update to a NEW value and drop the handle in the same inter-flush window. Even though the
1105        // gauge is now orphaned and idle, the write must not be lost: the next flush sees the dirty flag,
1106        // emits the value, and defers eviction by one interval.
1107        gauge.set(42.0);
1108        drop(gauge);
1109        flush_once(&state, &mut resolver, &mut flush_state);
1110        assert!(
1111            gauge_present(&state, "final_gauge"),
1112            "must not evict in an interval that wrote a new value"
1113        );
1114
1115        // The final value must reach the downstream aggregated state.
1116        let agg_processor = AggregatedMetricsProcessor;
1117        let agg_state = agg_processor.build_initial_state();
1118        for update in drain(&mut rx) {
1119            agg_processor.process(update, &agg_state);
1120        }
1121        assert_eq!(agg_state.find_single_with_tags("final_gauge", &[]), Some(42.0));
1122
1123        // The following interval (no write) finally evicts it.
1124        flush_once(&state, &mut resolver, &mut flush_state);
1125        assert!(!gauge_present(&state, "final_gauge"));
1126    }
1127
1128    #[tokio::test]
1129    async fn histogram_final_samples_reach_downstream_before_eviction() {
1130        // Regression test for the histogram eviction race fixed in commit 3752c5ad1b
1131        // ("rework internal metrics registry to support evicting expired/idle metrics", #1947).
1132        //
1133        // The histogram branch of `flush_once` must read the Arc strong count first (Acquire-fencing
1134        // on the orphaned path) and only THEN drain the sample bucket, exactly like the counter and
1135        // gauge paths. If a final `record()` and the handle drop both land in the inter-flush window
1136        // where eviction is armed, the flush that observes the orphaned handle must still see and emit
1137        // that sample -- and defer eviction by one interval -- rather than dropping the samples along
1138        // with the evicted registry entry. That commit shipped the counter regression coverage
1139        // (`final_counter_delta_reaches_downstream_before_eviction` plus the loom model) but never the
1140        // symmetric histogram coverage; this is it.
1141        let (state, mut rx) = make_state(1);
1142        let mut resolver = resolver();
1143        let mut flush_state = FlushState::default();
1144
1145        let histogram = register_histogram(&state, "final_histogram");
1146
1147        // Arm eviction by letting the idle counter reach the threshold while the handle is held.
1148        flush_once(&state, &mut resolver, &mut flush_state);
1149
1150        // Record a sample and drop the handle in the same inter-flush window. The next flush observes
1151        // the recorded sample, so it must emit it and defer eviction by one interval.
1152        histogram.record(1.5);
1153        drop(histogram);
1154        flush_once(&state, &mut resolver, &mut flush_state);
1155        assert!(
1156            histogram_present(&state, "final_histogram"),
1157            "must not evict in an interval that recorded a sample"
1158        );
1159
1160        // The recorded sample must reach the downstream aggregated state.
1161        let agg_processor = AggregatedMetricsProcessor;
1162        let agg_state = agg_processor.build_initial_state();
1163        for update in drain(&mut rx) {
1164            agg_processor.process(update, &agg_state);
1165        }
1166
1167        let mut downstream_sample_count = None;
1168        agg_state.visit_metrics(|context, value| {
1169            if context.name() == "final_histogram" {
1170                if let AggregatedMetricValue::Histogram(histogram) = value {
1171                    downstream_sample_count = Some(histogram.count());
1172                }
1173            }
1174        });
1175        assert_eq!(
1176            downstream_sample_count,
1177            Some(1),
1178            "the final recorded sample must reach downstream before eviction"
1179        );
1180
1181        // The following interval (no samples) finally evicts it.
1182        flush_once(&state, &mut resolver, &mut flush_state);
1183        assert!(!histogram_present(&state, "final_histogram"));
1184    }
1185}
1186
1187// Loom model of the counter eviction path in `flush_once`.
1188//
1189// In `flush_once`, the registry `maps` lock is held during the drain, but the increment/drop path
1190// does not take that lock: `Counter::increment` writes straight through the `Arc<Handle>`, and
1191// dropping a caller's `Counter` only decrements the Arc strong count. A component holding a cached
1192// handle can therefore land a final increment and drop the handle while a flush is mid-pass over it.
1193//
1194// The model transcribes the counter branch with loom primitives instead of exercising the real types,
1195// which loom cannot instrument here:
1196//
1197// - The strong count is an explicit `AtomicUsize`. loom's `Arc::strong_count` does not reflect a
1198//   concurrent decrement from another thread (it models Arc's drop synchronization, not the observable
1199//   count value), so a flush reading it never sees the orphaned (`== 1`) state mid-race. The explicit
1200//   atomic matches `std`'s `Arc`: clone increments (Relaxed), drop decrements with `Release`,
1201//   observation is a `Relaxed` load -- the same shape as how `flush_once` reads `Arc::strong_count`.
1202// - The real `Handle` is wrapped by the `metrics` crate's `Counter`/`CounterFn`, which construct
1203//   `std::sync::Arc` internally. loom only instruments atomics and `Arc`s swapped to `loom::sync::*`
1204//   behind the `loom` cfg, not those inside a third-party crate.
1205//
1206// `flush_decision_consume_first` and `flush_decision_strong_count_first` mirror the two possible
1207// orderings of `flush_once`'s counter branch and must stay in lockstep with it.
1208#[cfg(all(test, feature = "loom"))]
1209mod loom_tests {
1210    use loom::sync::atomic::{fence, AtomicU64, AtomicUsize, Ordering};
1211    use loom::sync::Arc;
1212
1213    /// The shared state behind a counter handle: the accumulator the flush loop drains, and the Arc
1214    /// strong count it consults to decide whether the handle is orphaned.
1215    struct Shared {
1216        value: AtomicU64,
1217        strong: AtomicUsize,
1218    }
1219
1220    /// A component holding a cached handle lands one final increment, then drops the handle.
1221    fn caller_increment_then_drop(shared: &Arc<Shared>, amount: u64) {
1222        shared.value.fetch_add(amount, Ordering::Relaxed);
1223        shared.strong.fetch_sub(1, Ordering::Release); // Arc clone drop
1224    }
1225
1226    /// Consumes the value, then checks the strong count (value read before the liveness check).
1227    fn flush_decision_consume_first(shared: &Arc<Shared>) -> (u64, bool) {
1228        let delta = shared.value.swap(0, Ordering::Relaxed);
1229        let orphaned = shared.strong.load(Ordering::Relaxed) == 1;
1230        (delta, orphaned && delta == 0)
1231    }
1232
1233    /// Checks the strong count first; if orphaned, Acquire-fences to synchronize with the dropping
1234    /// thread's `Release`, then consumes the value.
1235    fn flush_decision_strong_count_first(shared: &Arc<Shared>) -> (u64, bool) {
1236        let orphaned = shared.strong.load(Ordering::Relaxed) == 1;
1237        if orphaned {
1238            fence(Ordering::Acquire);
1239        }
1240        let delta = shared.value.swap(0, Ordering::Relaxed);
1241        (delta, orphaned && delta == 0)
1242    }
1243
1244    fn model(decision: fn(&Arc<Shared>) -> (u64, bool)) {
1245        loom::model(move || {
1246            const FINAL: u64 = 7;
1247
1248            // strong count starts at 2: one ref in the registry map, one held by the caller.
1249            let shared = Arc::new(Shared {
1250                value: AtomicU64::new(0),
1251                strong: AtomicUsize::new(2),
1252            });
1253            let caller_shared = Arc::clone(&shared);
1254
1255            let caller = loom::thread::spawn(move || {
1256                caller_increment_then_drop(&caller_shared, FINAL);
1257            });
1258
1259            // The flush task evaluates this counter while the component races.
1260            let (delta, evicted) = decision(&shared);
1261            caller.join().unwrap();
1262
1263            // Invariant: a counter may only be evicted once everything it accumulated has been
1264            // emitted. If the flush evicts it, the delta it emitted downstream must already include
1265            // the final increment -- otherwise those counts are silently discarded with the handle.
1266            if evicted {
1267                assert_eq!(
1268                    delta,
1269                    FINAL,
1270                    "counter evicted while {} counts were never emitted -> permanently lost",
1271                    FINAL - delta
1272                );
1273            }
1274        });
1275    }
1276
1277    // Consuming the value before checking the strong count is lossy: loom finds an interleaving where
1278    // the handle is evicted while a final increment goes unemitted. `#[should_panic]` asserts loom
1279    // reaches that interleaving.
1280    #[test]
1281    #[should_panic(expected = "permanently lost")]
1282    fn consume_first_ordering_loses_a_final_increment() {
1283        model(flush_decision_consume_first);
1284    }
1285
1286    // Checking the strong count first (Acquire-fencing when orphaned) before consuming preserves every
1287    // increment across all interleavings loom explores.
1288    #[test]
1289    fn strong_count_first_ordering_preserves_a_final_increment() {
1290        model(flush_decision_strong_count_first);
1291    }
1292
1293    // The histogram branch of `flush_once` has the same race shape as the counter branch, and got the
1294    // same fix in commit 3752c5ad1b (#1947): `Histogram::record` pushes straight through the
1295    // `Arc<Handle>`'s sample bucket, and dropping a caller's `Histogram` only decrements the Arc strong
1296    // count, so a component can land a final sample and drop its handle while a flush is mid-pass over
1297    // that histogram. Reading the strong count first (Acquire-fencing when orphaned) before draining
1298    // guarantees the drain observes the dropping thread's final `record()`.
1299    //
1300    // As with the counter model, loom cannot instrument the real storage: histogram samples live in
1301    // `metrics_util`'s `AtomicBucket`, which loom does not rewrite behind the `loom` cfg. We transcribe
1302    // "number of un-drained samples" into the same loom `AtomicU64` accumulator (record => fetch_add(1),
1303    // drain => swap(0)); that is precisely the ordering that decides whether the drain sees the final
1304    // sample, so the two decision functions above -- `flush_decision_consume_first` (drain-first) and
1305    // `flush_decision_strong_count_first` -- model the histogram drain unchanged, just with a
1306    // single-sample final write.
1307    fn histogram_model(decision: fn(&Arc<Shared>) -> (u64, bool)) {
1308        loom::model(move || {
1309            const FINAL_SAMPLES: u64 = 1;
1310
1311            // strong count starts at 2: one ref in the registry map, one held by the caller.
1312            let shared = Arc::new(Shared {
1313                value: AtomicU64::new(0),
1314                strong: AtomicUsize::new(2),
1315            });
1316            let caller_shared = Arc::clone(&shared);
1317
1318            // A component holding a cached histogram handle records one final sample, then drops it.
1319            let caller = loom::thread::spawn(move || {
1320                caller_shared.value.fetch_add(FINAL_SAMPLES, Ordering::Relaxed); // Histogram::record
1321                caller_shared.strong.fetch_sub(1, Ordering::Release); // Arc clone drop
1322            });
1323
1324            // The flush task evaluates this histogram while the component races.
1325            let (drained, evicted) = decision(&shared);
1326            caller.join().unwrap();
1327
1328            // Invariant: a histogram may only be evicted once every recorded sample has been drained
1329            // (and thus emitted). If the flush evicts it, the drain it performed must have observed the
1330            // final sample -- otherwise that sample is discarded with the evicted registry entry.
1331            if evicted {
1332                assert_eq!(
1333                    drained,
1334                    FINAL_SAMPLES,
1335                    "histogram evicted while {} recorded sample(s) were never drained -> permanently lost",
1336                    FINAL_SAMPLES - drained
1337                );
1338            }
1339        });
1340    }
1341
1342    // Draining the sample bucket before checking the strong count is lossy: loom finds an interleaving
1343    // where the histogram is evicted while a final recorded sample goes undrained. `#[should_panic]`
1344    // asserts loom reaches that interleaving.
1345    #[test]
1346    #[should_panic(expected = "permanently lost")]
1347    fn histogram_drain_first_ordering_loses_a_final_sample() {
1348        histogram_model(flush_decision_consume_first);
1349    }
1350
1351    // Checking the strong count first (Acquire-fencing when orphaned) before draining preserves the
1352    // final recorded sample across all interleavings loom explores.
1353    #[test]
1354    fn histogram_strong_count_first_ordering_preserves_a_final_sample() {
1355        histogram_model(flush_decision_strong_count_first);
1356    }
1357}