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}