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