Skip to content

Commit b0d84ba

Browse files
authored
Introduce support for thread-local default recorder (#523)
Currently, the only way to install a thread-local recorder is to use the provided function `with_local_recorder` function. In asynchronous contexts, especially where single-threaded runtimes are used, using locally scoped recorders can be cumbersome since `with_local_recorder` does not support async execution (unless the closure itself spawns a runtime). In order to provide an API that overcomes this limitation, for users that want to make use of locally-scoped default recorders, we make it so that the guard can be returned through a newly introduced `set_local_default` function that takes in a trait object. The returned guard is the same type we are using for `with_local_recorder` and when dropped will reset the thread local variable. The change does not introduce any breaking changes to the existing API. It does however make minimal changes to the internals in order to uphold the safety guarantees that were previously in place. Since the guard is no longer tied to a scope that also encloses the reference, we need to have an explicit lifetime on the guard to guarantee the recorder is not dropped before the guard is. We achieve this through a `PhantomData` type which shouldn't result in any runtime overhead. Closes #502 Signed-off-by: Matei <[email protected]>
1 parent 9116b2d commit b0d84ba

File tree

1 file changed

+232
-45
lines changed

1 file changed

+232
-45
lines changed

metrics/src/recorder/mod.rs

Lines changed: 232 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{cell::Cell, ptr::NonNull};
1+
use std::{cell::Cell, marker::PhantomData, ptr::NonNull};
22

33
mod cell;
44
use self::cell::RecorderOnceCell;
@@ -130,30 +130,39 @@ impl_recorder!(T, std::sync::Arc<T>);
130130
/// (thread-local storage) so that it can be accessed by the macros. This guard ensures that the
131131
/// pointer we store to the reference is cleared when the guard is dropped, so that it can't be used
132132
/// after the closure has finished, even if the closure panics and unwinds the stack.
133-
struct LocalRecorderGuard;
133+
///
134+
/// ## Note
135+
///
136+
/// The guard has a lifetime parameter `'a` that is bounded using a
137+
/// `PhantomData` type. This upholds the guard's contravariance, it must live
138+
/// _at most as long_ as the recorder it takes a reference to. The bounded
139+
/// lifetime prevents accidental use-after-free errors when using a guard
140+
/// directly through [`crate::set_default_local_recorder`].
141+
pub struct LocalRecorderGuard<'a> {
142+
prev_recorder: Option<NonNull<dyn Recorder>>,
143+
phantom: PhantomData<&'a dyn Recorder>,
144+
}
134145

135-
impl LocalRecorderGuard {
146+
impl<'a> LocalRecorderGuard<'a> {
136147
/// Creates a new `LocalRecorderGuard` and sets the thread-local recorder.
137-
fn new(recorder: &dyn Recorder) -> Self {
148+
fn new(recorder: &'a dyn Recorder) -> Self {
138149
// SAFETY: While we take a lifetime-less pointer to the given reference, the reference we
139-
// derive _from_ the pointer is never given a lifetime that exceeds the lifetime of the
140-
// input reference.
150+
// derive _from_ the pointer is given the same lifetime of the reference
151+
// used to construct the guard -- captured in the guard type itself --
152+
// and so derived references never outlive the source reference.
141153
let recorder_ptr = unsafe { NonNull::new_unchecked(recorder as *const _ as *mut _) };
142154

143-
LOCAL_RECORDER.with(|local_recorder| {
144-
local_recorder.set(Some(recorder_ptr));
145-
});
155+
let prev_recorder =
156+
LOCAL_RECORDER.with(|local_recorder| local_recorder.replace(Some(recorder_ptr)));
146157

147-
Self
158+
Self { prev_recorder, phantom: PhantomData }
148159
}
149160
}
150161

151-
impl Drop for LocalRecorderGuard {
162+
impl<'a> Drop for LocalRecorderGuard<'a> {
152163
fn drop(&mut self) {
153164
// Clear the thread-local recorder.
154-
LOCAL_RECORDER.with(|local_recorder| {
155-
local_recorder.set(None);
156-
});
165+
LOCAL_RECORDER.with(|local_recorder| local_recorder.replace(self.prev_recorder.take()));
157166
}
158167
}
159168

@@ -175,6 +184,32 @@ where
175184
GLOBAL_RECORDER.set(recorder)
176185
}
177186

187+
/// Sets the recorder as the default for the current thread for the duration of
188+
/// the lifetime of the returned [`LocalRecorderGuard`].
189+
///
190+
/// This function is suitable for capturing metrics in asynchronous code, in particular
191+
/// when using a single-threaded runtime. Any metrics registered prior to the returned
192+
/// guard will remain attached to the recorder that was present at the time of registration,
193+
/// and so this cannot be used to intercept existing metrics.
194+
///
195+
/// Additionally, local recorders can be used in a nested fashion. When setting a new
196+
/// default local recorder, the previous default local recorder will be captured if one
197+
/// was set, and will be restored when the returned guard drops.
198+
/// the lifetime of the returned [`LocalRecorderGuard`].
199+
///
200+
/// Any metrics recorded before a guard is returned will be completely ignored.
201+
/// Metrics implementations should provide an initialization method that
202+
/// installs the recorder internally.
203+
///
204+
/// The function is suitable for capturing metrics in asynchronous code that
205+
/// uses a single threaded runtime.
206+
///
207+
/// If a global recorder is set, it will be restored once the guard is dropped.
208+
#[must_use]
209+
pub fn set_default_local_recorder(recorder: &dyn Recorder) -> LocalRecorderGuard {
210+
LocalRecorderGuard::new(recorder)
211+
}
212+
178213
/// Runs the closure with the given recorder set as the global recorder for the duration.
179214
///
180215
/// This only applies as long as the closure is running, and on the thread where `with_local_recorder` is called. This
@@ -212,22 +247,129 @@ pub fn with_recorder<T>(f: impl FnOnce(&dyn Recorder) -> T) -> T {
212247

213248
#[cfg(test)]
214249
mod tests {
215-
use std::sync::{
216-
atomic::{AtomicBool, Ordering},
217-
Arc,
218-
};
250+
use std::sync::{atomic::Ordering, Arc};
219251

220-
use crate::NoopRecorder;
252+
use crate::{with_local_recorder, NoopRecorder};
221253

222254
use super::{Recorder, RecorderOnceCell};
223255

224256
#[test]
225257
fn boxed_recorder_dropped_on_existing_set() {
226258
// This test simply ensures that if a boxed recorder is handed to us to install, and another
227-
// recorder has already been installed, that we drop th new boxed recorder instead of
259+
// recorder has already been installed, that we drop the new boxed recorder instead of
228260
// leaking it.
261+
let recorder_cell = RecorderOnceCell::new();
262+
263+
// This is the first set of the cell, so it should always succeed.
264+
let (first_recorder, _) = test_recorders::TrackOnDropRecorder::new();
265+
let first_set_result = recorder_cell.set(first_recorder);
266+
assert!(first_set_result.is_ok());
267+
268+
// Since the cell is already set, this second set should fail. We'll also then assert that
269+
// our atomic boolean is set to `true`, indicating the drop logic ran for it.
270+
let (second_recorder, was_dropped) = test_recorders::TrackOnDropRecorder::new();
271+
assert!(!was_dropped.load(Ordering::SeqCst));
272+
273+
let second_set_result = recorder_cell.set(second_recorder);
274+
assert!(second_set_result.is_err());
275+
assert!(!was_dropped.load(Ordering::SeqCst));
276+
drop(second_set_result);
277+
assert!(was_dropped.load(Ordering::SeqCst));
278+
}
279+
280+
#[test]
281+
fn blanket_implementations() {
282+
fn is_recorder<T: Recorder>(_recorder: T) {}
283+
284+
let mut local = NoopRecorder;
285+
286+
is_recorder(NoopRecorder);
287+
is_recorder(Arc::new(NoopRecorder));
288+
is_recorder(Box::new(NoopRecorder));
289+
is_recorder(&local);
290+
is_recorder(&mut local);
291+
}
292+
293+
#[test]
294+
fn thread_scoped_recorder_guards() {
295+
// This test ensures that when a recorder is installed through
296+
// `crate::set_default_local_recorder` it will only be valid in the scope of the
297+
// thread.
298+
//
299+
// The goal of the test is to give confidence that no invalid memory
300+
// access errors are present when operating with locally scoped
301+
// recorders.
302+
let t1_recorder = test_recorders::SimpleCounterRecorder::new();
303+
let t2_recorder = test_recorders::SimpleCounterRecorder::new();
304+
let t3_recorder = test_recorders::SimpleCounterRecorder::new();
305+
// Start a new thread scope to take references to each recorder in the
306+
// closures passed to the thread.
307+
std::thread::scope(|s| {
308+
s.spawn(|| {
309+
let _guard = crate::set_default_local_recorder(&t1_recorder);
310+
crate::counter!("t1_counter").increment(1);
311+
});
312+
313+
s.spawn(|| {
314+
with_local_recorder(&t2_recorder, || {
315+
crate::counter!("t2_counter").increment(2);
316+
})
317+
});
318+
319+
s.spawn(|| {
320+
let _guard = crate::set_default_local_recorder(&t3_recorder);
321+
crate::counter!("t3_counter").increment(3);
322+
});
323+
});
324+
325+
assert!(t1_recorder.get_value() == 1);
326+
assert!(t2_recorder.get_value() == 2);
327+
assert!(t3_recorder.get_value() == 3);
328+
}
329+
330+
#[test]
331+
fn local_recorder_restored_when_dropped() {
332+
// This test ensures that any previously installed local recorders are
333+
// restored when the subsequently installed recorder's guard is dropped.
334+
let root_recorder = test_recorders::SimpleCounterRecorder::new();
335+
// Install the root recorder and increment the counter once.
336+
let _guard = crate::set_default_local_recorder(&root_recorder);
337+
crate::counter!("test_counter").increment(1);
338+
339+
// Install a second recorder and increment its counter once.
340+
let next_recorder = test_recorders::SimpleCounterRecorder::new();
341+
let next_guard = crate::set_default_local_recorder(&next_recorder);
342+
crate::counter!("test_counter").increment(1);
343+
let final_recorder = test_recorders::SimpleCounterRecorder::new();
344+
crate::with_local_recorder(&final_recorder, || {
345+
// Final recorder increments the counter by 10. At the end of the
346+
// closure, the guard should be dropped, and `next_recorder`
347+
// restored.
348+
crate::counter!("test_counter").increment(10);
349+
});
350+
// Since `next_recorder` is restored, we can increment it once and check
351+
// that the value is 2 (+1 before and after the closure).
352+
crate::counter!("test_counter").increment(1);
353+
assert!(next_recorder.get_value() == 2);
354+
drop(next_guard);
355+
356+
// At the end, increment the counter again by an arbitrary value. Since
357+
// `next_guard` is dropped, the root recorder is restored.
358+
crate::counter!("test_counter").increment(20);
359+
assert!(root_recorder.get_value() == 21);
360+
}
361+
362+
mod test_recorders {
363+
use std::sync::{
364+
atomic::{AtomicBool, AtomicU64, Ordering},
365+
Arc,
366+
};
367+
368+
use crate::Recorder;
369+
229370
#[derive(Debug)]
230-
struct TrackOnDropRecorder(Arc<AtomicBool>);
371+
// Tracks how many times the recorder was dropped
372+
pub struct TrackOnDropRecorder(Arc<AtomicBool>);
231373

232374
impl TrackOnDropRecorder {
233375
pub fn new() -> (Self, Arc<AtomicBool>) {
@@ -236,6 +378,8 @@ mod tests {
236378
}
237379
}
238380

381+
// === impl TrackOnDropRecorder ===
382+
239383
impl Recorder for TrackOnDropRecorder {
240384
fn describe_counter(
241385
&self,
@@ -282,35 +426,78 @@ mod tests {
282426
}
283427
}
284428

285-
let recorder_cell = RecorderOnceCell::new();
429+
// A simple recorder that only implements `register_counter`.
430+
#[derive(Debug)]
431+
pub struct SimpleCounterRecorder {
432+
state: Arc<AtomicU64>,
433+
}
286434

287-
// This is the first set of the cell, so it should always succeed.
288-
let (first_recorder, _) = TrackOnDropRecorder::new();
289-
let first_set_result = recorder_cell.set(first_recorder);
290-
assert!(first_set_result.is_ok());
435+
impl SimpleCounterRecorder {
436+
pub fn new() -> Self {
437+
Self { state: Arc::new(AtomicU64::default()) }
438+
}
291439

292-
// Since the cell is already set, this second set should fail. We'll also then assert that
293-
// our atomic boolean is set to `true`, indicating the drop logic ran for it.
294-
let (second_recorder, was_dropped) = TrackOnDropRecorder::new();
295-
assert!(!was_dropped.load(Ordering::SeqCst));
440+
pub fn get_value(&self) -> u64 {
441+
self.state.load(Ordering::Acquire)
442+
}
443+
}
296444

297-
let second_set_result = recorder_cell.set(second_recorder);
298-
assert!(second_set_result.is_err());
299-
assert!(!was_dropped.load(Ordering::SeqCst));
300-
drop(second_set_result);
301-
assert!(was_dropped.load(Ordering::SeqCst));
302-
}
445+
struct SimpleCounterHandle {
446+
state: Arc<AtomicU64>,
447+
}
303448

304-
#[test]
305-
fn blanket_implementations() {
306-
fn is_recorder<T: Recorder>(_recorder: T) {}
449+
impl crate::CounterFn for SimpleCounterHandle {
450+
fn increment(&self, value: u64) {
451+
self.state.fetch_add(value, Ordering::Acquire);
452+
}
307453

308-
let mut local = NoopRecorder;
454+
fn absolute(&self, _value: u64) {
455+
unimplemented!()
456+
}
457+
}
309458

310-
is_recorder(NoopRecorder);
311-
is_recorder(Arc::new(NoopRecorder));
312-
is_recorder(Box::new(NoopRecorder));
313-
is_recorder(&local);
314-
is_recorder(&mut local);
459+
// === impl SimpleCounterRecorder ===
460+
461+
impl Recorder for SimpleCounterRecorder {
462+
fn describe_counter(
463+
&self,
464+
_: crate::KeyName,
465+
_: Option<crate::Unit>,
466+
_: crate::SharedString,
467+
) {
468+
}
469+
fn describe_gauge(
470+
&self,
471+
_: crate::KeyName,
472+
_: Option<crate::Unit>,
473+
_: crate::SharedString,
474+
) {
475+
}
476+
fn describe_histogram(
477+
&self,
478+
_: crate::KeyName,
479+
_: Option<crate::Unit>,
480+
_: crate::SharedString,
481+
) {
482+
}
483+
484+
fn register_counter(&self, _: &crate::Key, _: &crate::Metadata<'_>) -> crate::Counter {
485+
crate::Counter::from_arc(Arc::new(SimpleCounterHandle {
486+
state: self.state.clone(),
487+
}))
488+
}
489+
490+
fn register_gauge(&self, _: &crate::Key, _: &crate::Metadata<'_>) -> crate::Gauge {
491+
crate::Gauge::noop()
492+
}
493+
494+
fn register_histogram(
495+
&self,
496+
_: &crate::Key,
497+
_: &crate::Metadata<'_>,
498+
) -> crate::Histogram {
499+
crate::Histogram::noop()
500+
}
501+
}
315502
}
316503
}

0 commit comments

Comments
 (0)