Skip to main content

sentry_core/
hub_impl.rs

1use std::cell::RefCell;
2use std::marker::PhantomData;
3use std::sync::{Arc, LazyLock, MutexGuard, PoisonError, RwLock};
4use std::thread;
5use std::thread::ThreadId;
6
7use crate::Scope;
8use crate::{scope::Stack, Client, Hub};
9
10static PROCESS_HUB: LazyLock<ProcessHub> = LazyLock::new(|| ProcessHub {
11    hub: Arc::new(Hub::new(None, Arc::new(Default::default()))),
12    thread: thread::current().id(),
13});
14
15thread_local! {
16    /// The [`Hub`] associated with this thread.
17    ///
18    /// On the thread on which the [`PROCESS_HUB`] is initialized, the [`THREAD_HUB`] and
19    /// [`PROCESS_HUB`] are identical, i.e. `Arc::ptr_eq(&PROCESS_HUB, &THREAD_HUB)` is true.
20    /// On any other thread, the [`THREAD_HUB`] is created as a new hub based off of the
21    /// [`PROCESS_HUB`].
22    static THREAD_HUB: RefCell<Arc<Hub>> = if thread::current().id() == PROCESS_HUB.thread {
23        PROCESS_HUB.hub.clone()
24    } else {
25        Hub::new_from_top(&PROCESS_HUB.hub).into()
26    }.into()
27}
28
29/// A guard that temporarily swaps the active hub in thread-local storage.
30///
31/// This type is `!Send` because it manages thread-local state and must be
32/// dropped on the same thread where it was created.
33pub struct SwitchGuard {
34    inner: Option<Arc<Hub>>,
35    /// Makes this type `!Send` while keeping it `Sync`.
36    ///
37    /// ```rust
38    /// # use sentry_core::HubSwitchGuard as SwitchGuard;
39    /// trait AssertSync: Sync {}
40    ///
41    /// impl AssertSync for SwitchGuard {}
42    /// ```
43    ///
44    /// ```rust,compile_fail
45    /// # use sentry_core::HubSwitchGuard as SwitchGuard;
46    /// trait AssertSend: Send {}
47    ///
48    /// impl AssertSend for SwitchGuard {}
49    /// ```
50    _not_send: PhantomData<MutexGuard<'static, ()>>,
51}
52
53impl SwitchGuard {
54    /// Swaps the current thread's Hub by the one provided
55    /// and returns a guard that, when dropped, replaces it
56    /// to the previous one.
57    pub fn new(mut hub: Arc<Hub>) -> Self {
58        let inner = THREAD_HUB.with(|thread_hub| {
59            let mut thread_hub = thread_hub.borrow_mut();
60            if std::ptr::eq(thread_hub.as_ref(), hub.as_ref()) {
61                return None;
62            }
63            std::mem::swap(&mut *thread_hub, &mut hub);
64            Some(hub)
65        });
66        SwitchGuard {
67            inner,
68            _not_send: PhantomData,
69        }
70    }
71
72    fn swap(&mut self) -> Option<Arc<Hub>> {
73        self.inner.take().map(|mut hub| {
74            THREAD_HUB.with(|thread_hub| {
75                let mut thread_hub = thread_hub.borrow_mut();
76                std::mem::swap(&mut *thread_hub, &mut hub);
77                hub
78            })
79        })
80    }
81}
82
83impl Drop for SwitchGuard {
84    fn drop(&mut self) {
85        let _ = self.swap();
86    }
87}
88
89#[derive(Debug)]
90pub(crate) struct HubImpl {
91    pub(crate) stack: Arc<RwLock<Stack>>,
92}
93
94impl HubImpl {
95    pub(crate) fn with<F: FnOnce(&Stack) -> R, R>(&self, f: F) -> R {
96        let guard = self.stack.read().unwrap_or_else(PoisonError::into_inner);
97        f(&guard)
98    }
99
100    pub(crate) fn with_mut<F: FnOnce(&mut Stack) -> R, R>(&self, f: F) -> R {
101        let mut guard = self.stack.write().unwrap_or_else(PoisonError::into_inner);
102        f(&mut guard)
103    }
104
105    pub(crate) fn is_active_and_usage_safe(&self) -> bool {
106        let guard = match self.stack.read() {
107            Err(err) => err.into_inner(),
108            Ok(guard) => guard,
109        };
110
111        guard.top().client.as_ref().is_some_and(|c| c.is_enabled())
112    }
113}
114
115impl Hub {
116    /// Creates a new hub from the given client and scope.
117    pub fn new(client: Option<Arc<Client>>, scope: Arc<Scope>) -> Hub {
118        Hub {
119            inner: HubImpl {
120                stack: Arc::new(RwLock::new(Stack::from_client_and_scope(client, scope))),
121            },
122            last_event_id: RwLock::new(None),
123        }
124    }
125
126    /// Creates a new hub based on the top scope of the given hub.
127    pub fn new_from_top<H: AsRef<Hub>>(other: H) -> Hub {
128        let hub = other.as_ref();
129        hub.inner.with(|stack| {
130            let top = stack.top();
131            Hub::new(top.client.clone(), top.scope.clone())
132        })
133    }
134
135    /// Returns the current, thread-local hub.
136    ///
137    /// Invoking this will return the current thread-local hub.  The first
138    /// time it is called on a thread, a new thread-local hub will be
139    /// created based on the topmost scope of the hub on the main thread as
140    /// returned by [`Hub::main`].  If the main thread did not yet have a
141    /// hub it will be created when invoking this function.
142    ///
143    /// To have control over which hub is installed as the current
144    /// thread-local hub, use [`Hub::run`].
145    ///
146    /// This method is unavailable if the client implementation is disabled.
147    /// When using the minimal API set use [`Hub::with_active`] instead.
148    pub fn current() -> Arc<Hub> {
149        THREAD_HUB.with_borrow(Arc::clone)
150    }
151
152    /// Returns the main thread's hub.
153    ///
154    /// This is similar to [`Hub::current`] but instead of picking the
155    /// current thread's hub it returns the main thread's hub instead.
156    pub fn main() -> Arc<Hub> {
157        PROCESS_HUB.hub.clone()
158    }
159
160    /// Invokes the callback with the default hub.
161    #[deprecated = "Use `Hub::current` instead; this function offers no performance benefit."]
162    pub fn with<F, R>(f: F) -> R
163    where
164        F: FnOnce(&Arc<Hub>) -> R,
165    {
166        f(&Hub::current())
167    }
168
169    /// Invokes the callback with a reference to the thread hub.
170    ///
171    /// This is potentially more performant than [`Hub::current`] as we avoid an [`Arc::clone`],
172    /// but it holds a borrow to the [`THREAD_HUB`]'s `RefCell` for the duration of the callback.
173    /// It is therefore essential to avoid calling [`Hub::run`], [`SwitchGuard::new`], or anything
174    /// else that mutably borrows the [`THREAD_HUB`] during the callback, e.g. any user-supplied
175    /// callbacks.
176    ///
177    /// # Panics
178    /// Panics if the [`THREAD_HUB`] is mutably borrowed at any point during the callback.
179    pub(crate) fn with_current<F, R>(f: F) -> R
180    where
181        F: FnOnce(&Hub) -> R,
182    {
183        THREAD_HUB.with_borrow(|hub| f(hub))
184    }
185
186    /// Binds a hub to the current thread for the duration of the call.
187    ///
188    /// During the execution of `f` the given hub will be installed as the
189    /// thread-local hub.  So any call to [`Hub::current`] during this time
190    /// will return the provided hub.
191    ///
192    /// Once the function is finished executing, including after it
193    /// panicked, the original hub is re-installed if one was present.
194    pub fn run<F: FnOnce() -> R, R>(hub: Arc<Hub>, f: F) -> R {
195        let _guard = SwitchGuard::new(hub);
196        f()
197    }
198
199    /// Returns the currently bound client.
200    pub fn client(&self) -> Option<Arc<Client>> {
201        self.inner.with(|stack| stack.top().client.clone())
202    }
203
204    /// Binds a new client to the hub.
205    pub fn bind_client(&self, client: Option<Arc<Client>>) {
206        self.inner.with_mut(|stack| {
207            stack.top_mut().client = client;
208        })
209    }
210
211    pub(crate) fn is_active_and_usage_safe(&self) -> bool {
212        self.inner.is_active_and_usage_safe()
213    }
214
215    pub(crate) fn with_current_scope<F: FnOnce(&Scope) -> R, R>(&self, f: F) -> R {
216        self.inner.with(|stack| f(&stack.top().scope))
217    }
218
219    pub(crate) fn with_current_scope_mut<F: FnOnce(&mut Scope) -> R, R>(&self, f: F) -> R {
220        self.inner
221            .with_mut(|stack| f(Arc::make_mut(&mut stack.top_mut().scope)))
222    }
223}
224
225/// Helper struct for storing the [`PROCESS_HUB`].
226struct ProcessHub {
227    /// The process's main hub.
228    hub: Arc<Hub>,
229    /// The thread on which the main hub was initialized.
230    thread: ThreadId,
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    /// Regression test for [`Hub::with`], ensuring that the `RefCell` borrow is not held during the callback.
238    ///
239    /// If we hold the `RefCell` borrow during the callback, this would panic.
240    #[test]
241    fn hub_run_inside_with_scope() {
242        let outer_hub = Arc::new(Hub::new(None, Default::default()));
243        let inner_hub = Arc::new(Hub::new(None, Default::default()));
244
245        Hub::run(outer_hub, || {
246            #[expect(deprecated)] // We are intentionally testing deprecated functionality
247            Hub::with(|_| Hub::run(inner_hub, || {}));
248        });
249    }
250}