Skip to main content

fast_telemetry_export/
sweeper.rs

1//! Periodic sweeper for stale dynamic metric series.
2//!
3//! Advances the global eviction cycle and evicts series that have been inactive
4//! for longer than the configured threshold. This bounds memory usage from
5//! dynamic labels regardless of which exporters are active.
6//!
7//! The actual eviction logic is provided by the caller via a closure,
8//! making this work with any metrics struct that has dynamic series.
9
10use std::time::Duration;
11
12#[cfg(feature = "tokio-runtime")]
13use tokio_util::sync::CancellationToken;
14
15/// Default sweep interval.
16const DEFAULT_SWEEP_INTERVAL: Duration = Duration::from_secs(10);
17
18/// Default eviction threshold: series inactive for this many sweep cycles are
19/// evicted. With the default 10s interval this equals ~5 minutes of inactivity.
20const DEFAULT_EVICTION_THRESHOLD: u32 = 30;
21
22/// Configuration for the stale-series sweeper.
23#[derive(Clone)]
24pub struct SweepConfig {
25    /// How often to run the sweep (default: 10s).
26    pub interval: Duration,
27    /// Number of consecutive idle cycles before a series is evicted (default: 30).
28    pub eviction_threshold: u32,
29}
30
31impl Default for SweepConfig {
32    fn default() -> Self {
33        Self {
34            interval: DEFAULT_SWEEP_INTERVAL,
35            eviction_threshold: DEFAULT_EVICTION_THRESHOLD,
36        }
37    }
38}
39
40impl SweepConfig {
41    pub fn new() -> Self {
42        Self::default()
43    }
44
45    pub fn with_interval(mut self, interval: Duration) -> Self {
46        self.interval = interval;
47        self
48    }
49
50    pub fn with_eviction_threshold(mut self, threshold: u32) -> Self {
51        self.eviction_threshold = threshold;
52        self
53    }
54}
55
56/// Run the stale-series sweep loop.
57///
58/// `sweep_fn` is called each cycle with the eviction threshold. It should
59/// advance the eviction cycle and evict stale series, returning the number
60/// of series evicted.
61///
62/// Runs until `cancel` is triggered.
63///
64/// # Example
65///
66/// ```ignore
67/// use std::sync::Arc;
68///
69/// use fast_telemetry::advance_cycle;
70/// use fast_telemetry_export::sweeper::{SweepConfig, run};
71/// use tokio_util::sync::CancellationToken;
72///
73/// let metrics = Arc::new(MyMetrics::new());
74/// let cancel = CancellationToken::new();
75///
76/// let m = metrics.clone();
77/// tokio::spawn(run(SweepConfig::default(), cancel, move |threshold| {
78///     advance_cycle();
79///     m.requests_by_endpoint.evict_stale(threshold)
80///         + m.latency_by_endpoint.evict_stale(threshold)
81/// }));
82/// ```
83#[cfg(feature = "tokio-runtime")]
84pub async fn run<F>(config: SweepConfig, cancel: CancellationToken, mut sweep_fn: F)
85where
86    F: FnMut(u32) -> usize,
87{
88    use tokio::time::MissedTickBehavior;
89
90    log::info!(
91        "Starting stale-series sweeper, interval={}s, eviction_threshold={}",
92        config.interval.as_secs(),
93        config.eviction_threshold
94    );
95
96    let mut interval = tokio::time::interval(config.interval);
97    interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
98    interval.tick().await;
99
100    loop {
101        tokio::select! {
102            _ = interval.tick() => {}
103            _ = cancel.cancelled() => {
104                log::info!("Stale-series sweeper shutting down");
105                return;
106            }
107        }
108
109        let evicted = sweep_fn(config.eviction_threshold);
110
111        if evicted > 0 {
112            log::debug!("Evicted {evicted} stale metric series");
113        }
114    }
115}
116
117/// Run the stale-series sweep loop on a monoio runtime.
118///
119/// This is the monoio-native counterpart to [`run`]. It uses
120/// [`monoio::time::interval`], so the caller must run it inside a monoio
121/// runtime with timers enabled.
122#[cfg(feature = "monoio")]
123pub async fn run_monoio<F>(config: SweepConfig, cancel: CancellationToken, mut sweep_fn: F)
124where
125    F: FnMut(u32) -> usize,
126{
127    use monoio::time::MissedTickBehavior;
128
129    log::info!(
130        "Starting monoio stale-series sweeper, interval={}s, eviction_threshold={}",
131        config.interval.as_secs(),
132        config.eviction_threshold
133    );
134
135    let mut interval = monoio::time::interval(config.interval);
136    interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
137    interval.tick().await;
138
139    loop {
140        monoio::select! {
141            _ = interval.tick() => {}
142            _ = cancel.cancelled() => {
143                log::info!("monoio stale-series sweeper shutting down");
144                return;
145            }
146        }
147
148        let evicted = sweep_fn(config.eviction_threshold);
149
150        if evicted > 0 {
151            log::debug!("Evicted {evicted} stale metric series");
152        }
153    }
154}
155
156/// Run the stale-series sweep loop on a compio runtime.
157///
158/// This is the compio-native counterpart to `run`. It uses
159/// [`compio::time`], so the caller must run it inside a compio runtime. `cancel`
160/// may be any future that completes when the sweeper should shut down.
161#[cfg(feature = "compio")]
162pub async fn run_compio<F>(
163    config: SweepConfig,
164    cancel: impl std::future::Future<Output = ()>,
165    mut sweep_fn: F,
166) where
167    F: FnMut(u32) -> usize,
168{
169    use futures_util::{FutureExt as _, select};
170
171    log::info!(
172        "Starting compio stale-series sweeper, interval={}s, eviction_threshold={}",
173        config.interval.as_secs(),
174        config.eviction_threshold
175    );
176
177    let mut interval = compio::time::interval(config.interval);
178    interval.tick().await;
179    let cancel = cancel.fuse();
180    let mut cancel = std::pin::pin!(cancel);
181
182    loop {
183        let tick = interval.tick();
184        let tick = std::pin::pin!(tick);
185
186        select! {
187            _ = tick.fuse() => {},
188            _ = cancel.as_mut() => {
189                log::info!("compio stale-series sweeper shutting down");
190                return;
191            }
192        }
193
194        let evicted = sweep_fn(config.eviction_threshold);
195
196        if evicted > 0 {
197            log::debug!("Evicted {evicted} stale metric series");
198        }
199    }
200}