Skip to content

Commit 0d0ca56

Browse files
authored
Merge branch 'master' into ivana/toxgen/again
2 parents 6561482 + 2d392af commit 0d0ca56

File tree

3 files changed

+151
-5
lines changed

3 files changed

+151
-5
lines changed

sentry_sdk/integrations/threading.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import sys
2+
import warnings
23
from functools import wraps
34
from threading import Thread, current_thread
45

@@ -49,6 +50,15 @@ def setup_once():
4950
# type: () -> None
5051
old_start = Thread.start
5152

53+
try:
54+
from django import VERSION as django_version # noqa: N811
55+
import channels # type: ignore[import-not-found]
56+
57+
channels_version = channels.__version__
58+
except ImportError:
59+
django_version = None
60+
channels_version = None
61+
5262
@wraps(old_start)
5363
def sentry_start(self, *a, **kw):
5464
# type: (Thread, *Any, **Any) -> Any
@@ -57,8 +67,27 @@ def sentry_start(self, *a, **kw):
5767
return old_start(self, *a, **kw)
5868

5969
if integration.propagate_scope:
60-
isolation_scope = sentry_sdk.get_isolation_scope()
61-
current_scope = sentry_sdk.get_current_scope()
70+
if (
71+
sys.version_info < (3, 9)
72+
and channels_version is not None
73+
and channels_version < "4.0.0"
74+
and django_version is not None
75+
and django_version >= (3, 0)
76+
and django_version < (4, 0)
77+
):
78+
warnings.warn(
79+
"There is a known issue with Django channels 2.x and 3.x when using Python 3.8 or older. "
80+
"(Async support is emulated using threads and some Sentry data may be leaked between those threads.) "
81+
"Please either upgrade to Django channels 4.0+, use Django's async features "
82+
"available in Django 3.1+ instead of Django channels, or upgrade to Python 3.9+.",
83+
stacklevel=2,
84+
)
85+
isolation_scope = sentry_sdk.get_isolation_scope()
86+
current_scope = sentry_sdk.get_current_scope()
87+
88+
else:
89+
isolation_scope = sentry_sdk.get_isolation_scope().fork()
90+
current_scope = sentry_sdk.get_current_scope().fork()
6291
else:
6392
isolation_scope = None
6493
current_scope = None

tests/integrations/django/asgi/test_asgi.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,25 @@ async def test_basic(sentry_init, capture_events, application):
3838

3939
events = capture_events()
4040

41-
comm = HttpCommunicator(application, "GET", "/view-exc?test=query")
42-
response = await comm.get_response()
43-
await comm.wait()
41+
import channels # type: ignore[import-not-found]
42+
43+
if (
44+
sys.version_info < (3, 9)
45+
and channels.__version__ < "4.0.0"
46+
and django.VERSION >= (3, 0)
47+
and django.VERSION < (4, 0)
48+
):
49+
# We emit a UserWarning for channels 2.x and 3.x on Python 3.8 and older
50+
# because the async support was not really good back then and there is a known issue.
51+
# See the TreadingIntegration for details.
52+
with pytest.warns(UserWarning):
53+
comm = HttpCommunicator(application, "GET", "/view-exc?test=query")
54+
response = await comm.get_response()
55+
await comm.wait()
56+
else:
57+
comm = HttpCommunicator(application, "GET", "/view-exc?test=query")
58+
response = await comm.get_response()
59+
await comm.wait()
4460

4561
assert response["status"] == 500
4662

tests/integrations/threading/test_threading.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import gc
22
from concurrent import futures
3+
from textwrap import dedent
34
from threading import Thread
45

56
import pytest
@@ -172,3 +173,103 @@ def target():
172173
assert Thread.run.__qualname__ == original_run.__qualname__
173174
assert t.run.__name__ == "run"
174175
assert t.run.__qualname__ == original_run.__qualname__
176+
177+
178+
@pytest.mark.parametrize(
179+
"propagate_scope",
180+
(True, False),
181+
ids=["propagate_scope=True", "propagate_scope=False"],
182+
)
183+
def test_scope_data_not_leaked_in_threads(sentry_init, propagate_scope):
184+
sentry_init(
185+
integrations=[ThreadingIntegration(propagate_scope=propagate_scope)],
186+
)
187+
188+
sentry_sdk.set_tag("initial_tag", "initial_value")
189+
initial_iso_scope = sentry_sdk.get_isolation_scope()
190+
191+
def do_some_work():
192+
# check if we have the initial scope data propagated into the thread
193+
if propagate_scope:
194+
assert sentry_sdk.get_isolation_scope()._tags == {
195+
"initial_tag": "initial_value"
196+
}
197+
else:
198+
assert sentry_sdk.get_isolation_scope()._tags == {}
199+
200+
# change data in isolation scope in thread
201+
sentry_sdk.set_tag("thread_tag", "thread_value")
202+
203+
t = Thread(target=do_some_work)
204+
t.start()
205+
t.join()
206+
207+
# check if the initial scope data is not modified by the started thread
208+
assert initial_iso_scope._tags == {
209+
"initial_tag": "initial_value"
210+
}, "The isolation scope in the main thread should not be modified by the started thread."
211+
212+
213+
@pytest.mark.parametrize(
214+
"propagate_scope",
215+
(True, False),
216+
ids=["propagate_scope=True", "propagate_scope=False"],
217+
)
218+
def test_spans_from_multiple_threads(
219+
sentry_init, capture_events, render_span_tree, propagate_scope
220+
):
221+
sentry_init(
222+
traces_sample_rate=1.0,
223+
integrations=[ThreadingIntegration(propagate_scope=propagate_scope)],
224+
)
225+
events = capture_events()
226+
227+
def do_some_work(number):
228+
with sentry_sdk.start_span(
229+
op=f"inner-run-{number}", name=f"Thread: child-{number}"
230+
):
231+
pass
232+
233+
threads = []
234+
235+
with sentry_sdk.start_transaction(op="outer-trx"):
236+
for number in range(5):
237+
with sentry_sdk.start_span(
238+
op=f"outer-submit-{number}", name="Thread: main"
239+
):
240+
t = Thread(target=do_some_work, args=(number,))
241+
t.start()
242+
threads.append(t)
243+
244+
for t in threads:
245+
t.join()
246+
247+
(event,) = events
248+
if propagate_scope:
249+
assert render_span_tree(event) == dedent(
250+
"""\
251+
- op="outer-trx": description=null
252+
- op="outer-submit-0": description="Thread: main"
253+
- op="inner-run-0": description="Thread: child-0"
254+
- op="outer-submit-1": description="Thread: main"
255+
- op="inner-run-1": description="Thread: child-1"
256+
- op="outer-submit-2": description="Thread: main"
257+
- op="inner-run-2": description="Thread: child-2"
258+
- op="outer-submit-3": description="Thread: main"
259+
- op="inner-run-3": description="Thread: child-3"
260+
- op="outer-submit-4": description="Thread: main"
261+
- op="inner-run-4": description="Thread: child-4"\
262+
"""
263+
)
264+
265+
elif not propagate_scope:
266+
assert render_span_tree(event) == dedent(
267+
"""\
268+
- op="outer-trx": description=null
269+
- op="outer-submit-0": description="Thread: main"
270+
- op="outer-submit-1": description="Thread: main"
271+
- op="outer-submit-2": description="Thread: main"
272+
- op="outer-submit-3": description="Thread: main"
273+
- op="outer-submit-4": description="Thread: main"\
274+
"""
275+
)

0 commit comments

Comments
 (0)