Skip to content

Commit 6169b08

Browse files
fix issue #946
1 parent ddf0e1f commit 6169b08

4 files changed

Lines changed: 276 additions & 8 deletions

File tree

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use std::borrow::Cow;
22
use std::cell::RefCell;
3-
use std::collections::{BTreeMap, HashMap};
3+
use std::collections::BTreeMap;
44
use std::sync::Arc;
55

66
use bitflags::bitflags;
77
use sentry_core::protocol::Value;
8-
use sentry_core::{Breadcrumb, HubSwitchGuard, TransactionOrSpan};
8+
use sentry_core::{Breadcrumb, Hub, HubSwitchGuard, TransactionOrSpan};
99
use tracing_core::field::Visit;
1010
use tracing_core::{span, Event, Field, Level, Metadata, Subscriber};
1111
use tracing_subscriber::layer::{Context, Layer};
@@ -16,6 +16,9 @@ use crate::SENTRY_NAME_FIELD;
1616
use crate::SENTRY_OP_FIELD;
1717
use crate::SENTRY_TRACE_FIELD;
1818
use crate::TAGS_PREFIX;
19+
use span_guard_stack::SpanGuardStack;
20+
21+
mod span_guard_stack;
1922

2023
bitflags! {
2124
/// The action that Sentry should perform for a given [`Event`]
@@ -350,19 +353,20 @@ where
350353

351354
let extensions = span.extensions();
352355
if let Some(data) = extensions.get::<SentrySpanData>() {
353-
let guard = HubSwitchGuard::new(data.hub.clone());
356+
let hub = Arc::new(Hub::new_from_top(data.hub.clone()));
357+
let guard = HubSwitchGuard::new(hub.clone());
354358
SPAN_GUARDS.with(|guards| {
355-
guards.borrow_mut().insert(id.clone(), guard);
359+
guards.borrow_mut().push(id.clone(), guard);
356360
});
357-
data.hub.configure_scope(|scope| {
361+
hub.configure_scope(|scope| {
358362
scope.set_span(Some(data.sentry_span.clone()));
359363
});
360364
}
361365
}
362366

363367
/// Set exited span's parent as *current* sentry span.
364368
fn on_exit(&self, id: &span::Id, ctx: Context<'_, S>) {
365-
let _guard = SPAN_GUARDS.with(|guards| guards.borrow_mut().remove(id));
369+
let _guard = SPAN_GUARDS.with(|guards| guards.borrow_mut().pop(id.clone()));
366370

367371
let span = match ctx.span(id) {
368372
Some(span) => span,
@@ -511,8 +515,7 @@ thread_local! {
511515
static VISITOR_BUFFER: RefCell<String> = const { RefCell::new(String::new()) };
512516
/// Hub switch guards keyed by span ID. Stored in thread-local so guards are
513517
/// always dropped on the same thread where they were created.
514-
static SPAN_GUARDS: RefCell<HashMap<span::Id, HubSwitchGuard>> =
515-
RefCell::new(HashMap::new());
518+
static SPAN_GUARDS: RefCell<SpanGuardStack> = RefCell::new(SpanGuardStack::new());
516519
}
517520

518521
/// Records all span fields into a `BTreeMap`, reusing a mutable `String` as buffer.
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
//! This module contains code for stack-like storage for `HubSwitchGuard`s keyed
2+
//! by tracing span ID.
3+
4+
use std::collections::hash_map::Entry;
5+
use std::collections::HashMap;
6+
7+
use sentry_core::HubSwitchGuard;
8+
use tracing_core::span::Id as SpanId;
9+
10+
/// Holds per-span stacks of `HubSwitchGuard`s to handle span re-entrancy.
11+
///
12+
/// Each time a span is entered, we should push a new guard onto the stack.
13+
/// When the span exits, we should pop the guard from the stack.
14+
pub(super) struct SpanGuardStack {
15+
/// The map of span IDs to their respective guard stacks.
16+
guards: HashMap<SpanId, Vec<HubSwitchGuard>>,
17+
}
18+
19+
impl SpanGuardStack {
20+
/// Creates an empty guard stack map.
21+
pub(super) fn new() -> Self {
22+
Self {
23+
guards: HashMap::new(),
24+
}
25+
}
26+
27+
/// Pushes a guard for the given span ID, creating the stack if needed.
28+
pub(super) fn push(&mut self, id: SpanId, guard: HubSwitchGuard) {
29+
self.guards.entry(id).or_default().push(guard);
30+
}
31+
32+
/// Pops the most recent guard for the span ID, removing the stack when empty.
33+
pub(super) fn pop(&mut self, id: SpanId) -> Option<HubSwitchGuard> {
34+
match self.guards.entry(id) {
35+
Entry::Occupied(mut entry) => {
36+
let stack = entry.get_mut();
37+
let guard = stack.pop();
38+
if stack.is_empty() {
39+
entry.remove();
40+
}
41+
guard
42+
}
43+
Entry::Vacant(_) => None,
44+
}
45+
}
46+
47+
/// Removes all guards for the span ID without returning them.
48+
///
49+
/// This function guarantees that the guards are dropped in LIFO order.
50+
/// That way, the hub which was active when the span was first entered
51+
/// will be the one active after this function returns.
52+
///
53+
/// Typically, remove should only get called once the span is fully
54+
/// exited, so this removal order guarantee is mostly just defensive.
55+
pub(super) fn remove(&mut self, id: &SpanId) {
56+
self.guards
57+
.remove(id)
58+
.into_iter()
59+
.flatten()
60+
.rev() // <- we drop in reverse order
61+
.for_each(drop);
62+
}
63+
}
64+
65+
#[cfg(test)]
66+
mod tests {
67+
use super::SpanGuardStack;
68+
use sentry_core::{Hub, HubSwitchGuard};
69+
use std::sync::Arc;
70+
use tracing_core::span::Id as SpanId;
71+
72+
#[test]
73+
fn pop_is_lifo() {
74+
let initial = Hub::current();
75+
let hub_a = Arc::new(Hub::new_from_top(initial.clone()));
76+
let hub_b = Arc::new(Hub::new_from_top(hub_a.clone()));
77+
78+
let mut stack = SpanGuardStack::new();
79+
let id = SpanId::from_u64(1);
80+
81+
stack.push(id.clone(), HubSwitchGuard::new(hub_a.clone()));
82+
assert!(Arc::ptr_eq(&Hub::current(), &hub_a));
83+
84+
stack.push(id.clone(), HubSwitchGuard::new(hub_b.clone()));
85+
assert!(Arc::ptr_eq(&Hub::current(), &hub_b));
86+
87+
drop(stack.pop(id.clone()).expect("guard for hub_b"));
88+
assert!(Arc::ptr_eq(&Hub::current(), &hub_a));
89+
90+
drop(stack.pop(id.clone()).expect("guard for hub_a"));
91+
assert!(Arc::ptr_eq(&Hub::current(), &initial));
92+
93+
assert!(stack.pop(id).is_none());
94+
}
95+
96+
#[test]
97+
fn remove_drops_all_guards_in_lifo_order() {
98+
let initial = Hub::current();
99+
let hub_a = Arc::new(Hub::new_from_top(initial.clone()));
100+
let hub_b = Arc::new(Hub::new_from_top(hub_a.clone()));
101+
102+
assert!(!Arc::ptr_eq(&hub_b, &initial));
103+
assert!(!Arc::ptr_eq(&hub_a, &initial));
104+
assert!(!Arc::ptr_eq(&hub_a, &hub_b));
105+
106+
let mut stack = SpanGuardStack::new();
107+
let id = SpanId::from_u64(2);
108+
109+
stack.push(id.clone(), HubSwitchGuard::new(hub_a.clone()));
110+
assert!(Arc::ptr_eq(&Hub::current(), &hub_a));
111+
112+
stack.push(id.clone(), HubSwitchGuard::new(hub_b.clone()));
113+
assert!(Arc::ptr_eq(&Hub::current(), &hub_b));
114+
115+
stack.remove(&id);
116+
assert!(Arc::ptr_eq(&Hub::current(), &initial));
117+
}
118+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
mod shared;
2+
3+
use sentry::protocol::Context;
4+
use std::thread;
5+
use std::time::Duration;
6+
7+
#[test]
8+
fn cross_thread_span_entries_share_transaction() {
9+
let transport = shared::init_sentry(1.0);
10+
11+
let span = tracing::info_span!("foo");
12+
let span2 = span.clone();
13+
14+
let handle1 = thread::spawn(move || {
15+
let _guard = span.enter();
16+
let _bar_span = tracing::info_span!("bar").entered();
17+
thread::sleep(Duration::from_millis(100));
18+
});
19+
20+
let handle2 = thread::spawn(move || {
21+
thread::sleep(Duration::from_millis(10));
22+
let _guard = span2.enter();
23+
let _baz_span = tracing::info_span!("baz").entered();
24+
thread::sleep(Duration::from_millis(50));
25+
});
26+
27+
handle1.join().unwrap();
28+
handle2.join().unwrap();
29+
30+
let data = transport.fetch_and_clear_envelopes();
31+
let transactions: Vec<_> = data
32+
.into_iter()
33+
.flat_map(|envelope| {
34+
envelope
35+
.items()
36+
.filter_map(|item| match item {
37+
sentry::protocol::EnvelopeItem::Transaction(transaction) => {
38+
Some(transaction.clone())
39+
}
40+
_ => None,
41+
})
42+
.collect::<Vec<_>>()
43+
})
44+
.collect();
45+
46+
assert_eq!(
47+
transactions.len(),
48+
1,
49+
"expected a single transaction for cross-thread span entries"
50+
);
51+
52+
let transaction = &transactions[0];
53+
assert_eq!(transaction.name.as_deref(), Some("foo"));
54+
55+
let trace = match transaction
56+
.contexts
57+
.get("trace")
58+
.expect("transaction should include trace context")
59+
{
60+
Context::Trace(trace) => trace,
61+
unexpected => panic!("expected trace context but got {unexpected:?}"),
62+
};
63+
64+
let bar_span = transaction
65+
.spans
66+
.iter()
67+
.find(|span| span.description.as_deref() == Some("bar"))
68+
.expect("expected span \"bar\" to be recorded in the transaction");
69+
let baz_span = transaction
70+
.spans
71+
.iter()
72+
.find(|span| span.description.as_deref() == Some("baz"))
73+
.expect("expected span \"baz\" to be recorded in the transaction");
74+
75+
assert_eq!(bar_span.parent_span_id, Some(trace.span_id));
76+
assert_eq!(baz_span.parent_span_id, Some(trace.span_id));
77+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
use sentry::protocol::EnvelopeItem;
2+
3+
mod shared;
4+
5+
/// Ensures re-entering the same span does not corrupt the current tracing state,
6+
/// so subsequent spans are still recorded under a single transaction.
7+
#[test]
8+
fn reentering_span_preserves_parent() {
9+
let transport = shared::init_sentry(1.0);
10+
11+
{
12+
// Create a span and enter it, then re-enter the same span to simulate
13+
// common async polling behavior where a span can be entered multiple times.
14+
let span_a = tracing::info_span!("a");
15+
let _enter_a = span_a.enter();
16+
{
17+
let _reenter_a = span_a.enter();
18+
}
19+
20+
// Create another span while the original span is still entered to ensure
21+
// it is recorded on the same transaction rather than starting a new one.
22+
let span_b = tracing::info_span!("b");
23+
{
24+
let _enter_b = span_b.enter();
25+
}
26+
}
27+
28+
let transactions: Vec<_> = transport
29+
.fetch_and_clear_envelopes()
30+
.into_iter()
31+
.flat_map(|envelope| envelope.into_items())
32+
.filter_map(|item| match item {
33+
EnvelopeItem::Transaction(transaction) => Some(transaction),
34+
_ => None,
35+
})
36+
.collect();
37+
38+
assert_eq!(
39+
transactions.len(),
40+
1,
41+
"expected a single transaction when reentering a span"
42+
);
43+
44+
let transaction = &transactions[0];
45+
assert_eq!(transaction.name.as_deref(), Some("a"));
46+
47+
let trace = match transaction
48+
.contexts
49+
.get("trace")
50+
.expect("transaction should include trace context")
51+
{
52+
sentry::protocol::Context::Trace(trace) => trace,
53+
unexpected => panic!("expected trace context but got {unexpected:?}"),
54+
};
55+
56+
let b_span = transaction
57+
.spans
58+
.iter()
59+
.find(|span| span.description.as_deref() == Some("b"))
60+
.expect("expected span \"b\" to be recorded in the transaction");
61+
62+
assert_eq!(b_span.parent_span_id, Some(trace.span_id));
63+
assert!(
64+
!transaction
65+
.spans
66+
.iter()
67+
.any(|span| span.description.as_deref() == Some("a")),
68+
"expected the transaction root span not to be duplicated in span list"
69+
);
70+
}

0 commit comments

Comments
 (0)