Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix threading by carrying forward span reference in use_scope #3851

Merged
merged 1 commit into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh
- `span.containing_transaction` has been removed. Use `span.root_span` instead.
- `continue_from_headers`, `continue_from_environ` and `from_traceparent` have been removed, please use top-level API `sentry_sdk.continue_trace` instead.
- `PropagationContext` constructor no longer takes a `dynamic_sampling_context` but takes a `baggage` object instead.
- `ThreadingIntegration` no longer takes the `propagate_hub` argument.

### Deprecated

Expand Down
32 changes: 30 additions & 2 deletions sentry_sdk/integrations/opentelemetry/contextvars_context.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from typing import cast, TYPE_CHECKING

from opentelemetry.trace import set_span_in_context
from opentelemetry.context import Context, get_value, set_value
from opentelemetry.context.contextvars_context import ContextVarsRuntimeContext

Expand All @@ -9,25 +12,50 @@
SENTRY_USE_ISOLATION_SCOPE_KEY,
)

if TYPE_CHECKING:
from typing import Optional
from sentry_sdk.integrations.opentelemetry.scope import PotelScope


class SentryContextVarsRuntimeContext(ContextVarsRuntimeContext):
def attach(self, context):
# type: (Context) -> object
scopes = get_value(SENTRY_SCOPES_KEY, context)

should_fork_isolation_scope = context.pop(
SENTRY_FORK_ISOLATION_SCOPE_KEY, False
)
should_fork_isolation_scope = cast("bool", should_fork_isolation_scope)

should_use_isolation_scope = context.pop(SENTRY_USE_ISOLATION_SCOPE_KEY, None)
should_use_isolation_scope = cast(
"Optional[PotelScope]", should_use_isolation_scope
)

should_use_current_scope = context.pop(SENTRY_USE_CURRENT_SCOPE_KEY, None)
should_use_current_scope = cast(
"Optional[PotelScope]", should_use_current_scope
)

if scopes and isinstance(scopes, tuple):
if scopes:
scopes = cast("tuple[PotelScope, PotelScope]", scopes)
(current_scope, isolation_scope) = scopes
else:
current_scope = sentry_sdk.get_current_scope()
isolation_scope = sentry_sdk.get_isolation_scope()

new_context = context

if should_use_current_scope:
new_scope = should_use_current_scope

# the main case where we use use_scope is for
# scope propagation in the ThreadingIntegration
# so we need to carry forward the span reference explicitly too
span = should_use_current_scope.span
if span:
new_context = set_span_in_context(span._otel_span, new_context)

else:
new_scope = current_scope.fork()

Expand All @@ -40,5 +68,5 @@ def attach(self, context):

new_scopes = (new_scope, new_isolation_scope)

new_context = set_value(SENTRY_SCOPES_KEY, new_scopes, context)
new_context = set_value(SENTRY_SCOPES_KEY, new_scopes, new_context)
return super().attach(new_context)
20 changes: 4 additions & 16 deletions sentry_sdk/integrations/threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from sentry_sdk.utils import (
event_from_exception,
capture_internal_exceptions,
logger,
reraise,
)

Expand All @@ -27,22 +26,10 @@
class ThreadingIntegration(Integration):
identifier = "threading"

def __init__(self, propagate_hub=None, propagate_scope=True):
# type: (Optional[bool], bool) -> None
if propagate_hub is not None:
logger.warning(
"Deprecated: propagate_hub is deprecated. This will be removed in the future."
)

# Note: propagate_hub did not have any effect on propagation of scope data
# scope data was always propagated no matter what the value of propagate_hub was
# This is why the default for propagate_scope is True

def __init__(self, propagate_scope=True):
# type: (bool) -> None
self.propagate_scope = propagate_scope

if propagate_hub is not None:
self.propagate_scope = propagate_hub

@staticmethod
def setup_once():
# type: () -> None
Expand Down Expand Up @@ -99,7 +86,8 @@ def _run_old_run_func():
with sentry_sdk.use_scope(current_scope_to_use):
return _run_old_run_func()
else:
return _run_old_run_func()
with sentry_sdk.isolation_scope():
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the old logic here was actually wrong but kinda worked because of some fallback code in the scope implementation.

Semantically, if we don't explicitly propagate the scopes, we should ALWAYS have created a new isolation scope for the new thread.

return _run_old_run_func()

return run # type: ignore

Expand Down
7 changes: 4 additions & 3 deletions sentry_sdk/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
SENTRY_TRACE_HEADER_NAME,
NoOpSpan,
Span,
POTelSpan,
Transaction,
)
from sentry_sdk.utils import (
Expand Down Expand Up @@ -669,7 +670,7 @@ def clear(self):
self.clear_breadcrumbs()
self._should_capture = True # type: bool

self._span = None # type: Optional[Span]
self._span = None # type: Optional[POTelSpan]
self._session = None # type: Optional[Session]
self._force_auto_session_tracking = None # type: Optional[bool]

Expand Down Expand Up @@ -777,13 +778,13 @@ def set_user(self, value):

@property
def span(self):
# type: () -> Optional[Span]
# type: () -> Optional[POTelSpan]
"""Get current tracing span."""
return self._span

@span.setter
def span(self, span):
# type: (Optional[Span]) -> None
# type: (Optional[POTelSpan]) -> None
"""Set current tracing span."""
self._span = span

Expand Down
4 changes: 2 additions & 2 deletions sentry_sdk/tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1280,11 +1280,11 @@ def __eq__(self, other):
def __repr__(self):
# type: () -> str
return (
"<%s(op=%r, description:%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, origin=%r)>"
"<%s(op=%r, name:%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, origin=%r)>"
% (
self.__class__.__name__,
self.op,
self.description,
self.name,
self.trace_id,
self.span_id,
self.parent_span_id,
Expand Down
20 changes: 10 additions & 10 deletions tests/integrations/threading/test_threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ def crash():
assert not events


@pytest.mark.parametrize("propagate_hub", (True, False))
def test_propagates_hub(sentry_init, capture_events, propagate_hub):
@pytest.mark.parametrize("propagate_scope", (True, False))
def test_propagates_scope(sentry_init, capture_events, propagate_scope):
sentry_init(
default_integrations=False,
integrations=[ThreadingIntegration(propagate_hub=propagate_hub)],
integrations=[ThreadingIntegration(propagate_scope=propagate_scope)],
)
events = capture_events()

Expand All @@ -65,33 +65,33 @@ def stage2():
assert exception["mechanism"]["type"] == "threading"
assert not exception["mechanism"]["handled"]

if propagate_hub:
if propagate_scope:
assert event["tags"]["stage1"] == "true"
else:
assert "stage1" not in event.get("tags", {})


@pytest.mark.parametrize("propagate_hub", (True, False))
def test_propagates_threadpool_hub(sentry_init, capture_events, propagate_hub):
@pytest.mark.parametrize("propagate_scope", (True, False))
def test_propagates_threadpool_scope(sentry_init, capture_events, propagate_scope):
sentry_init(
traces_sample_rate=1.0,
integrations=[ThreadingIntegration(propagate_hub=propagate_hub)],
integrations=[ThreadingIntegration(propagate_scope=propagate_scope)],
)
events = capture_events()

def double(number):
with sentry_sdk.start_span(op="task", name=str(number)):
with sentry_sdk.start_span(op="task", name=str(number), only_if_parent=True):
return number * 2

with sentry_sdk.start_transaction(name="test_handles_threadpool"):
with sentry_sdk.start_span(name="test_handles_threadpool"):
with futures.ThreadPoolExecutor(max_workers=1) as executor:
tasks = [executor.submit(double, number) for number in [1, 2, 3, 4]]
for future in futures.as_completed(tasks):
print("Getting future value!", future.result())

sentry_sdk.flush()

if propagate_hub:
if propagate_scope:
assert len(events) == 1
(event,) = events
assert event["spans"][0]["trace_id"] == event["spans"][1]["trace_id"]
Expand Down
Loading