Skip to content

Commit fb7cb4a

Browse files
jswrennhawkw
andauthored
core: add Dispatch::downgrade() and WeakDispatch (#2293)
Allows collectors and subscribers to stash their own `Dispatch` without causing a memory leak. ## Motivation Resolves a shortcoming of #2269: that it's impossible for collectors or subscribers to stash a copy of their own `Dispatch` without creating a reference cycle that would prevent them from being dropped. ## Solution Introduces `WeakDispatch` (analogous to `std::sync::Weak`) that holds a weak pointer to a collector. `WeakDispatch` can be created via `Dispatch::downgrade`, and can be transformed into a `Dispatch` via `WeakDispatch::upgrade`. Co-authored-by: Eliza Weisman <eliza@buoyant.io>
1 parent ed3c9b6 commit fb7cb4a

File tree

4 files changed

+149
-3
lines changed

4 files changed

+149
-3
lines changed

tracing-core/src/collect.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,24 @@ use core::ptr::NonNull;
7979
/// [`event_enabled`]: Collect::event_enabled
8080
pub trait Collect: 'static {
8181
/// Invoked when this collector becomes a [`Dispatch`].
82+
///
83+
/// ## Avoiding Memory Leaks
84+
///
85+
/// Collectors should not store their own [`Dispatch`]. Because the
86+
/// `Dispatch` owns the collector, storing the `Dispatch` within the
87+
/// collector will create a reference count cycle, preventing the `Dispatch`
88+
/// from ever being dropped.
89+
///
90+
/// Instead, when it is necessary to store a cyclical reference to the
91+
/// `Dispatch` within a collector, use [`Dispatch::downgrade`] to convert a
92+
/// `Dispatch` into a [`WeakDispatch`]. This type is analogous to
93+
/// [`std::sync::Weak`], and does not create a reference count cycle. A
94+
/// [`WeakDispatch`] can be stored within a collector without causing a
95+
/// memory leak, and can be [upgraded] into a `Dispatch` temporarily when
96+
/// the `Dispatch` must be accessed by the collector.
97+
///
98+
/// [`WeakDispatch`]: crate::dispatch::WeakDispatch
99+
/// [upgraded]: crate::dispatch::WeakDispatch::upgrade
82100
fn on_register_dispatch(&self, collector: &Dispatch) {
83101
let _ = collector;
84102
}

tracing-core/src/dispatch.rs

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,11 +150,10 @@ use core::{
150150
use std::{
151151
cell::{Cell, RefCell, RefMut},
152152
error,
153-
sync::Weak,
154153
};
155154

156155
#[cfg(feature = "alloc")]
157-
use alloc::sync::Arc;
156+
use alloc::sync::{Arc, Weak};
158157

159158
#[cfg(feature = "alloc")]
160159
use core::ops::Deref;
@@ -169,6 +168,34 @@ pub struct Dispatch {
169168
collector: &'static (dyn Collect + Send + Sync),
170169
}
171170

171+
/// `WeakDispatch` is a version of [`Dispatch`] that holds a non-owning reference
172+
/// to a [collector].
173+
///
174+
/// The collector may be accessed by calling [`WeakDispatch::upgrade`],
175+
/// which returns an `Option<Dispatch>`. If all [`Dispatch`] clones that point
176+
/// at the collector have been dropped, [`WeakDispatch::upgrade`] will return
177+
/// `None`. Otherwise, it will return `Some(Dispatch)`.
178+
///
179+
/// A `WeakDispatch` may be created from a [`Dispatch`] by calling the
180+
/// [`Dispatch::downgrade`] method. The primary use for creating a
181+
/// [`WeakDispatch`] is to allow a collector to hold a cyclical reference to
182+
/// itself without creating a memory leak. See [here] for details.
183+
///
184+
/// This type is analogous to the [`std::sync::Weak`] type, but for a
185+
/// [`Dispatch`] rather than an [`Arc`].
186+
///
187+
/// [collector]: Collect
188+
/// [`Arc`]: std::sync::Arc
189+
/// [here]: Collect#avoiding-memory-leaks
190+
#[derive(Clone)]
191+
pub struct WeakDispatch {
192+
#[cfg(feature = "alloc")]
193+
collector: Kind<Weak<dyn Collect + Send + Sync>>,
194+
195+
#[cfg(not(feature = "alloc"))]
196+
collector: &'static (dyn Collect + Send + Sync),
197+
}
198+
172199
#[cfg(feature = "alloc")]
173200
#[derive(Clone)]
174201
enum Kind<T> {
@@ -577,6 +604,33 @@ impl Dispatch {
577604
me
578605
}
579606

607+
/// Creates a [`WeakDispatch`] from this `Dispatch`.
608+
///
609+
/// A [`WeakDispatch`] is similar to a [`Dispatch`], but it does not prevent
610+
/// the underlying [collector] from being dropped. Instead, it only permits
611+
/// access while other references to the collector exist. This is equivalent
612+
/// to the standard library's [`Arc::downgrade`] method, but for `Dispatch`
613+
/// rather than `Arc`.
614+
///
615+
/// The primary use for creating a [`WeakDispatch`] is to allow a collector
616+
/// to hold a cyclical reference to itself without creating a memory leak.
617+
/// See [here] for details.
618+
///
619+
/// [collector]: Collect
620+
/// [`Arc::downgrade`]: std::sync::Arc::downgrade
621+
/// [here]: Collect#avoiding-memory-leaks
622+
pub fn downgrade(&self) -> WeakDispatch {
623+
#[cfg(feature = "alloc")]
624+
let collector = match &self.collector {
625+
Kind::Global(dispatch) => Kind::Global(*dispatch),
626+
Kind::Scoped(dispatch) => Kind::Scoped(Arc::downgrade(dispatch)),
627+
};
628+
#[cfg(not(feature = "alloc"))]
629+
let collector = self.collector;
630+
631+
WeakDispatch { collector }
632+
}
633+
580634
#[cfg(feature = "std")]
581635
pub(crate) fn registrar(&self) -> Registrar {
582636
Registrar(match self.collector {
@@ -859,6 +913,63 @@ where
859913
}
860914
}
861915

916+
impl WeakDispatch {
917+
/// Attempts to upgrade this `WeakDispatch` to a [`Dispatch`].
918+
///
919+
/// Returns `None` if the referenced `Dispatch` has already been dropped.
920+
///
921+
/// ## Examples
922+
///
923+
/// ```
924+
/// # use tracing_core::collect::NoCollector;
925+
/// # use tracing_core::dispatch::Dispatch;
926+
/// static COLLECTOR: NoCollector = NoCollector::new();
927+
/// let strong = Dispatch::new(COLLECTOR);
928+
/// let weak = strong.downgrade();
929+
///
930+
/// // The strong here keeps it alive, so we can still access the object.
931+
/// assert!(weak.upgrade().is_some());
932+
///
933+
/// drop(strong); // But not any more.
934+
/// assert!(weak.upgrade().is_none());
935+
/// ```
936+
pub fn upgrade(&self) -> Option<Dispatch> {
937+
#[cfg(feature = "alloc")]
938+
let collector = match &self.collector {
939+
Kind::Global(dispatch) => Some(Kind::Global(*dispatch)),
940+
Kind::Scoped(dispatch) => dispatch.upgrade().map(Kind::Scoped),
941+
};
942+
#[cfg(not(feature = "alloc"))]
943+
let collector = Some(self.collector);
944+
945+
collector.map(|collector| Dispatch { collector })
946+
}
947+
}
948+
949+
impl fmt::Debug for WeakDispatch {
950+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
951+
match &self.collector {
952+
#[cfg(feature = "alloc")]
953+
Kind::Global(collector) => f
954+
.debug_tuple("WeakDispatch::Global")
955+
.field(&format_args!("{:p}", collector))
956+
.finish(),
957+
958+
#[cfg(feature = "alloc")]
959+
Kind::Scoped(collector) => f
960+
.debug_tuple("WeakDispatch::Scoped")
961+
.field(&format_args!("{:p}", collector))
962+
.finish(),
963+
964+
#[cfg(not(feature = "alloc"))]
965+
collector => f
966+
.debug_tuple("WeakDispatch::Global")
967+
.field(&format_args!("{:p}", collector))
968+
.finish(),
969+
}
970+
}
971+
}
972+
862973
#[cfg(feature = "std")]
863974
impl Registrar {
864975
pub(crate) fn upgrade(&self) -> Option<Dispatch> {

tracing-subscriber/src/subscribe/mod.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,23 @@ where
719719
/// Performs late initialization when installing this subscriber as a
720720
/// [collector].
721721
///
722+
/// ## Avoiding Memory Leaks
723+
///
724+
/// Subscribers should not store the [`Dispatch`] pointing to the collector
725+
/// that they are a part of. Because the `Dispatch` owns the collector,
726+
/// storing the `Dispatch` within the collector will create a reference
727+
/// count cycle, preventing the `Dispatch` from ever being dropped.
728+
///
729+
/// Instead, when it is necessary to store a cyclical reference to the
730+
/// `Dispatch` within a subscriber, use [`Dispatch::downgrade`] to convert a
731+
/// `Dispatch` into a [`WeakDispatch`]. This type is analogous to
732+
/// [`std::sync::Weak`], and does not create a reference count cycle. A
733+
/// [`WeakDispatch`] can be stored within a subscriber without causing a
734+
/// memory leak, and can be [upgraded] into a `Dispatch` temporarily when
735+
/// the `Dispatch` must be accessed by the subscriber.
736+
///
737+
/// [`WeakDispatch`]: tracing_core::dispatch::WeakDispatch
738+
/// [upgraded]: tracing_core::dispatch::WeakDispatch::upgrade
722739
/// [collector]: tracing_core::Collect
723740
fn on_register_dispatch(&self, collector: &Dispatch) {
724741
let _ = collector;

tracing/src/dispatch.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ pub use tracing_core::dispatch::with_default;
148148
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
149149
pub use tracing_core::dispatch::DefaultGuard;
150150
pub use tracing_core::dispatch::{
151-
get_default, set_global_default, Dispatch, SetGlobalDefaultError,
151+
get_default, set_global_default, Dispatch, SetGlobalDefaultError, WeakDispatch,
152152
};
153153

154154
/// Private API for internal use by tracing's macros.

0 commit comments

Comments
 (0)