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}