reth_node_metrics/
hooks.rs1use metrics_process::Collector;
2use parking_lot::Mutex;
3use reth_tasks::TaskExecutor;
4use std::{
5 fmt,
6 panic::{catch_unwind, AssertUnwindSafe},
7 sync::Arc,
8 time::{Duration, Instant},
9};
10
11pub trait Hook: Fn() + Send + Sync + 'static {}
13impl<T: 'static + Fn() + Send + Sync> Hook for T {}
14
15pub struct HooksBuilder {
17 hooks: Vec<Box<dyn Hook<Output = ()>>>,
18 background_hooks: Vec<Box<dyn Hook<Output = ()>>>,
19 background_interval: Duration,
20}
21
22impl HooksBuilder {
23 pub const DEFAULT_BACKGROUND_INTERVAL: Duration = Duration::from_secs(5 * 60);
25
26 pub fn with_hook(self, hook: impl Hook) -> Self {
31 self.with_boxed_hook(Box::new(hook))
32 }
33
34 pub fn install_hook<F, H>(self, f: F) -> Self
36 where
37 F: FnOnce() -> H,
38 H: Hook,
39 {
40 self.with_hook(f())
41 }
42
43 #[inline]
45 pub fn with_boxed_hook(mut self, hook: Box<dyn Hook<Output = ()>>) -> Self {
46 self.hooks.push(hook);
47 self
48 }
49
50 pub fn with_background_hook(mut self, hook: impl Hook) -> Self {
63 self.background_hooks.push(Box::new(hook));
64 self
65 }
66
67 pub const fn with_background_interval(mut self, interval: Duration) -> Self {
69 self.background_interval = interval;
70 self
71 }
72
73 pub fn build(self) -> Hooks {
75 Hooks {
76 inner: Arc::new(self.hooks),
77 background: Arc::new(BackgroundHooks {
78 hooks: self.background_hooks,
79 interval: self.background_interval,
80 state: Mutex::default(),
81 }),
82 }
83 }
84}
85
86impl Default for HooksBuilder {
87 fn default() -> Self {
88 Self {
89 hooks: vec![
90 Box::new(|| Collector::default().collect()),
91 Box::new(collect_memory_stats),
92 Box::new(collect_io_stats),
93 ],
94 background_hooks: Vec::new(),
95 background_interval: Self::DEFAULT_BACKGROUND_INTERVAL,
96 }
97 }
98}
99
100impl std::fmt::Debug for HooksBuilder {
101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 f.debug_struct("HooksBuilder")
103 .field("hooks", &format_args!("Vec<Box<dyn Hook>>, len: {}", self.hooks.len()))
104 .field("background_hooks", &self.background_hooks.len())
105 .field("background_interval", &self.background_interval)
106 .finish()
107 }
108}
109
110#[derive(Clone)]
112pub struct Hooks {
113 inner: Arc<Vec<Box<dyn Hook<Output = ()>>>>,
114 background: Arc<BackgroundHooks>,
115}
116
117impl Hooks {
118 #[inline]
120 pub fn builder() -> HooksBuilder {
121 HooksBuilder::default()
122 }
123
124 pub(crate) fn iter(&self) -> impl Iterator<Item = &Box<dyn Hook<Output = ()>>> {
125 self.inner.iter()
126 }
127
128 pub(crate) fn refresh_background(&self, executor: &TaskExecutor) {
134 if !self.background.claim() {
135 return
136 }
137
138 let background = self.background.clone();
139 executor.spawn_blocking(move || background.collect());
140 }
141}
142
143impl fmt::Debug for Hooks {
144 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145 let hooks_len = self.inner.len();
146 f.debug_struct("Hooks")
147 .field("inner", &format_args!("Arc<Vec<Box<dyn Hook>>>, len: {hooks_len}"))
148 .field("background", &self.background)
149 .finish()
150 }
151}
152
153struct BackgroundHooks {
156 hooks: Vec<Box<dyn Hook<Output = ()>>>,
157 interval: Duration,
158 state: Mutex<BackgroundHooksState>,
159}
160
161impl BackgroundHooks {
162 fn claim(&self) -> bool {
169 if self.hooks.is_empty() {
170 return false
171 }
172
173 let mut state = self.state.lock();
174 if state.in_flight ||
175 state.last_collected.is_some_and(|last| last.elapsed() < self.interval)
176 {
177 return false
178 }
179 state.last_collected = Some(Instant::now());
180 state.in_flight = true;
181
182 true
183 }
184
185 fn collect(&self) {
190 let _in_flight = InFlightGuard(&self.state);
191 for (idx, hook) in self.hooks.iter().enumerate() {
192 if catch_unwind(AssertUnwindSafe(hook)).is_err() {
193 tracing::error!(hook = idx, "Background metrics hook panicked");
194 }
195 }
196 }
197}
198
199impl fmt::Debug for BackgroundHooks {
200 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201 f.debug_struct("BackgroundHooks")
202 .field("hooks", &format_args!("Vec<Box<dyn Hook>>, len: {}", self.hooks.len()))
203 .field("interval", &self.interval)
204 .finish()
205 }
206}
207
208#[derive(Default)]
211struct BackgroundHooksState {
212 last_collected: Option<Instant>,
214 in_flight: bool,
216}
217
218struct InFlightGuard<'a>(&'a Mutex<BackgroundHooksState>);
220
221impl Drop for InFlightGuard<'_> {
222 fn drop(&mut self) {
223 self.0.lock().in_flight = false;
224 }
225}
226
227#[cfg(all(feature = "jemalloc", unix))]
228fn collect_memory_stats() {
229 use metrics::gauge;
230 use tikv_jemalloc_ctl::{epoch, stats};
231 use tracing::error;
232
233 if epoch::advance().map_err(|error| error!(%error, "Failed to advance jemalloc epoch")).is_err()
234 {
235 return
236 }
237
238 if let Ok(value) = stats::active::read()
239 .map_err(|error| error!(%error, "Failed to read jemalloc.stats.active"))
240 {
241 gauge!("jemalloc.active").set(value as f64);
242 }
243
244 if let Ok(value) = stats::allocated::read()
245 .map_err(|error| error!(%error, "Failed to read jemalloc.stats.allocated"))
246 {
247 gauge!("jemalloc.allocated").set(value as f64);
248 }
249
250 if let Ok(value) = stats::mapped::read()
251 .map_err(|error| error!(%error, "Failed to read jemalloc.stats.mapped"))
252 {
253 gauge!("jemalloc.mapped").set(value as f64);
254 }
255
256 if let Ok(value) = stats::metadata::read()
257 .map_err(|error| error!(%error, "Failed to read jemalloc.stats.metadata"))
258 {
259 gauge!("jemalloc.metadata").set(value as f64);
260 }
261
262 if let Ok(value) = stats::resident::read()
263 .map_err(|error| error!(%error, "Failed to read jemalloc.stats.resident"))
264 {
265 gauge!("jemalloc.resident").set(value as f64);
266 }
267
268 if let Ok(value) = stats::retained::read()
269 .map_err(|error| error!(%error, "Failed to read jemalloc.stats.retained"))
270 {
271 gauge!("jemalloc.retained").set(value as f64);
272 }
273}
274
275#[cfg(not(all(feature = "jemalloc", unix)))]
276const fn collect_memory_stats() {}
277
278#[cfg(target_os = "linux")]
279fn collect_io_stats() {
280 use metrics::counter;
281 use tracing::error;
282
283 let Ok(process) = procfs::process::Process::myself()
284 .map_err(|error| error!(%error, "Failed to get currently running process"))
285 else {
286 return
287 };
288
289 let Ok(io) = process.io().map_err(
290 |error| error!(%error, "Failed to get IO stats for the currently running process"),
291 ) else {
292 return
293 };
294
295 counter!("io.rchar").absolute(io.rchar);
296 counter!("io.wchar").absolute(io.wchar);
297 counter!("io.syscr").absolute(io.syscr);
298 counter!("io.syscw").absolute(io.syscw);
299 counter!("io.read_bytes").absolute(io.read_bytes);
300 counter!("io.write_bytes").absolute(io.write_bytes);
301 counter!("io.cancelled_write_bytes").absolute(io.cancelled_write_bytes);
302}
303
304#[cfg(not(target_os = "linux"))]
305const fn collect_io_stats() {}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use reth_tasks::Runtime;
311 use std::sync::atomic::{AtomicUsize, Ordering};
312
313 fn refresh_until(hooks: &Hooks, runtime: &Runtime, done: impl Fn() -> bool) {
316 let deadline = Instant::now() + Duration::from_secs(10);
317 while !done() {
318 assert!(Instant::now() < deadline, "background collection did not make progress");
319 hooks.refresh_background(runtime);
320 std::thread::sleep(Duration::from_millis(5));
321 }
322 }
323
324 #[test]
325 fn background_collections_do_not_overlap() {
326 let runtime = Runtime::test();
327 let active = Arc::new(AtomicUsize::new(0));
328 let max_active = Arc::new(AtomicUsize::new(0));
329 let collections = Arc::new(AtomicUsize::new(0));
330 let hooks = Hooks::builder()
331 .with_background_interval(Duration::from_millis(20))
332 .with_background_hook({
333 let active = active.clone();
334 let max_active = max_active.clone();
335 let collections = collections.clone();
336 move || {
337 let now_active = active.fetch_add(1, Ordering::SeqCst) + 1;
338 max_active.fetch_max(now_active, Ordering::SeqCst);
339 std::thread::sleep(Duration::from_millis(100));
341 collections.fetch_add(1, Ordering::SeqCst);
342 active.fetch_sub(1, Ordering::SeqCst);
343 }
344 })
345 .build();
346
347 let started = Instant::now();
348 while started.elapsed() < Duration::from_millis(350) {
349 hooks.refresh_background(&runtime);
350 std::thread::sleep(Duration::from_millis(5));
351 }
352 while active.load(Ordering::SeqCst) > 0 {
353 std::thread::sleep(Duration::from_millis(5));
354 }
355
356 assert_eq!(max_active.load(Ordering::SeqCst), 1, "collections overlapped");
357 assert!(
358 collections.load(Ordering::SeqCst) >= 2,
359 "collection did not resume once the previous one finished"
360 );
361 }
362
363 #[test]
364 fn panicking_background_hook_does_not_block_collection() {
365 let runtime = Runtime::test();
366 let collections = Arc::new(AtomicUsize::new(0));
367 let hooks = Hooks::builder()
368 .with_background_interval(Duration::ZERO)
369 .with_background_hook(|| panic!("hook panicked"))
370 .with_background_hook({
371 let collections = collections.clone();
372 move || {
373 collections.fetch_add(1, Ordering::SeqCst);
374 }
375 })
376 .build();
377
378 refresh_until(&hooks, &runtime, || collections.load(Ordering::SeqCst) >= 1);
380 refresh_until(&hooks, &runtime, || collections.load(Ordering::SeqCst) >= 2);
382 }
383}