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().and_then(|mut hub| {
74            // We use `try_with` to access the `THREAD_HUB`, intentionally ignoring any errors that
75            // result. If `try_with` errors, this is because the `THREAD_HUB` local key has been
76            // destroyed, which means that there is nothing to swap with, making this operation
77            // pointless. Further, the destruction of the thread-local indicates that the thread
78            // is likely shutting down.
79            THREAD_HUB
80                .try_with(|thread_hub| {
81                    let mut thread_hub = thread_hub.borrow_mut();
82                    std::mem::swap(&mut *thread_hub, &mut hub);
83                    hub
84                })
85                .ok()
86        })
87    }
88}
89
90impl Drop for SwitchGuard {
91    fn drop(&mut self) {
92        let _ = self.swap();
93    }
94}
95
96#[derive(Debug)]
97pub(crate) struct HubImpl {
98    pub(crate) stack: Arc<RwLock<Stack>>,
99}
100
101impl HubImpl {
102    pub(crate) fn with<F: FnOnce(&Stack) -> R, R>(&self, f: F) -> R {
103        let guard = self.stack.read().unwrap_or_else(PoisonError::into_inner);
104        f(&guard)
105    }
106
107    pub(crate) fn with_mut<F: FnOnce(&mut Stack) -> R, R>(&self, f: F) -> R {
108        let mut guard = self.stack.write().unwrap_or_else(PoisonError::into_inner);
109        f(&mut guard)
110    }
111
112    pub(crate) fn is_active_and_usage_safe(&self) -> bool {
113        let guard = match self.stack.read() {
114            Err(err) => err.into_inner(),
115            Ok(guard) => guard,
116        };
117
118        guard.top().client.as_ref().is_some_and(|c| c.is_enabled())
119    }
120}
121
122impl Hub {
123    /// Creates a new hub from the given client and scope.
124    pub fn new(client: Option<Arc<Client>>, scope: Arc<Scope>) -> Hub {
125        Hub {
126            inner: HubImpl {
127                stack: Arc::new(RwLock::new(Stack::from_client_and_scope(client, scope))),
128            },
129            last_event_id: RwLock::new(None),
130        }
131    }
132
133    /// Creates a new hub based on the top scope of the given hub.
134    pub fn new_from_top<H: AsRef<Hub>>(other: H) -> Hub {
135        let hub = other.as_ref();
136        hub.inner.with(|stack| {
137            let top = stack.top();
138            Hub::new(top.client.clone(), top.scope.clone())
139        })
140    }
141
142    /// Returns the current, thread-local hub.
143    ///
144    /// Invoking this will return the current thread-local hub.  The first
145    /// time it is called on a thread, a new thread-local hub will be
146    /// created based on the topmost scope of the hub on the main thread as
147    /// returned by [`Hub::main`].  If the main thread did not yet have a
148    /// hub it will be created when invoking this function.
149    ///
150    /// To have control over which hub is installed as the current
151    /// thread-local hub, use [`Hub::run`].
152    ///
153    /// This method is unavailable if the client implementation is disabled.
154    /// When using the minimal API set use [`Hub::with_active`] instead.
155    pub fn current() -> Arc<Hub> {
156        THREAD_HUB.with_borrow(Arc::clone)
157    }
158
159    /// Returns the main thread's hub.
160    ///
161    /// This is similar to [`Hub::current`] but instead of picking the
162    /// current thread's hub it returns the main thread's hub instead.
163    pub fn main() -> Arc<Hub> {
164        PROCESS_HUB.hub.clone()
165    }
166
167    /// Invokes the callback with the default hub.
168    #[deprecated = "Use `Hub::current` instead; this function offers no performance benefit."]
169    pub fn with<F, R>(f: F) -> R
170    where
171        F: FnOnce(&Arc<Hub>) -> R,
172    {
173        f(&Hub::current())
174    }
175
176    /// Invokes the callback with a reference to the thread hub.
177    ///
178    /// This is potentially more performant than [`Hub::current`] as we avoid an [`Arc::clone`],
179    /// but it holds a borrow to the [`THREAD_HUB`]'s `RefCell` for the duration of the callback.
180    /// It is therefore essential to avoid calling [`Hub::run`], [`SwitchGuard::new`], or anything
181    /// else that mutably borrows the [`THREAD_HUB`] during the callback, e.g. any user-supplied
182    /// callbacks.
183    ///
184    /// # Panics
185    /// Panics if the [`THREAD_HUB`] is mutably borrowed at any point during the callback.
186    pub(crate) fn with_current<F, R>(f: F) -> R
187    where
188        F: FnOnce(&Hub) -> R,
189    {
190        THREAD_HUB.with_borrow(|hub| f(hub))
191    }
192
193    /// Binds a hub to the current thread for the duration of the call.
194    ///
195    /// During the execution of `f` the given hub will be installed as the
196    /// thread-local hub.  So any call to [`Hub::current`] during this time
197    /// will return the provided hub.
198    ///
199    /// Once the function is finished executing, including after it
200    /// panicked, the original hub is re-installed if one was present.
201    pub fn run<F: FnOnce() -> R, R>(hub: Arc<Hub>, f: F) -> R {
202        let _guard = SwitchGuard::new(hub);
203        f()
204    }
205
206    /// Returns the currently bound client.
207    pub fn client(&self) -> Option<Arc<Client>> {
208        self.inner.with(|stack| stack.top().client.clone())
209    }
210
211    /// Binds a new client to the hub.
212    pub fn bind_client(&self, client: Option<Arc<Client>>) {
213        self.inner.with_mut(|stack| {
214            stack.top_mut().client = client;
215        })
216    }
217
218    pub(crate) fn is_active_and_usage_safe(&self) -> bool {
219        self.inner.is_active_and_usage_safe()
220    }
221
222    pub(crate) fn with_current_scope<F: FnOnce(&Scope) -> R, R>(&self, f: F) -> R {
223        self.inner.with(|stack| f(&stack.top().scope))
224    }
225
226    pub(crate) fn with_current_scope_mut<F: FnOnce(&mut Scope) -> R, R>(&self, f: F) -> R {
227        self.inner
228            .with_mut(|stack| f(Arc::make_mut(&mut stack.top_mut().scope)))
229    }
230}
231
232/// Helper struct for storing the [`PROCESS_HUB`].
233struct ProcessHub {
234    /// The process's main hub.
235    hub: Arc<Hub>,
236    /// The thread on which the main hub was initialized.
237    thread: ThreadId,
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    /// Regression test for [`Hub::with`], ensuring that the `RefCell` borrow is not held during the callback.
245    ///
246    /// If we hold the `RefCell` borrow during the callback, this would panic.
247    #[test]
248    fn hub_run_inside_with_scope() {
249        let outer_hub = Arc::new(Hub::new(None, Default::default()));
250        let inner_hub = Arc::new(Hub::new(None, Default::default()));
251
252        Hub::run(outer_hub, || {
253            #[expect(deprecated)] // We are intentionally testing deprecated functionality
254            Hub::with(|_| Hub::run(inner_hub, || {}));
255        });
256    }
257}