Skip to content

Commit e7161eb

Browse files
committed
Merge branch 'master' into sentry-sdk-2.0
2 parents fa24e49 + 4d1b814 commit e7161eb

File tree

5 files changed

+118
-38
lines changed

5 files changed

+118
-38
lines changed

sentry_sdk/_compat.py

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,29 +24,74 @@ def __new__(metacls, name, this_bases, d):
2424
return type.__new__(MetaClass, "temporary_class", (), {})
2525

2626

27-
def check_thread_support():
28-
# type: () -> None
27+
def check_uwsgi_thread_support():
28+
# type: () -> bool
29+
# We check two things here:
30+
#
31+
# 1. uWSGI doesn't run in threaded mode by default -- issue a warning if
32+
# that's the case.
33+
#
34+
# 2. Additionally, if uWSGI is running in preforking mode (default), it needs
35+
# the --py-call-uwsgi-fork-hooks option for the SDK to work properly. This
36+
# is because any background threads spawned before the main process is
37+
# forked are NOT CLEANED UP IN THE CHILDREN BY DEFAULT even if
38+
# --enable-threads is on. One has to explicitly provide
39+
# --py-call-uwsgi-fork-hooks to force uWSGI to run regular cpython
40+
# after-fork hooks that take care of cleaning up stale thread data.
2941
try:
3042
from uwsgi import opt # type: ignore
3143
except ImportError:
32-
return
44+
return True
45+
46+
from sentry_sdk.consts import FALSE_VALUES
47+
48+
def enabled(option):
49+
# type: (str) -> bool
50+
value = opt.get(option, False)
51+
if isinstance(value, bool):
52+
return value
53+
54+
if isinstance(value, bytes):
55+
try:
56+
value = value.decode()
57+
except Exception:
58+
pass
59+
60+
return value and str(value).lower() not in FALSE_VALUES
3361

3462
# When `threads` is passed in as a uwsgi option,
3563
# `enable-threads` is implied on.
36-
if "threads" in opt:
37-
return
64+
threads_enabled = "threads" in opt or enabled("enable-threads")
65+
fork_hooks_on = enabled("py-call-uwsgi-fork-hooks")
66+
lazy_mode = enabled("lazy-apps") or enabled("lazy")
3867

39-
# put here because of circular import
40-
from sentry_sdk.consts import FALSE_VALUES
68+
if lazy_mode and not threads_enabled:
69+
from warnings import warn
4170

42-
if str(opt.get("enable-threads", "0")).lower() in FALSE_VALUES:
71+
warn(
72+
Warning(
73+
"IMPORTANT: "
74+
"We detected the use of uWSGI without thread support. "
75+
"This might lead to unexpected issues. "
76+
'Please run uWSGI with "--enable-threads" for full support.'
77+
)
78+
)
79+
80+
return False
81+
82+
elif not lazy_mode and (not threads_enabled or not fork_hooks_on):
4383
from warnings import warn
4484

4585
warn(
4686
Warning(
47-
"We detected the use of uwsgi with disabled threads. "
48-
"This will cause issues with the transport you are "
49-
"trying to use. Please enable threading for uwsgi. "
50-
'(Add the "enable-threads" flag).'
87+
"IMPORTANT: "
88+
"We detected the use of uWSGI in preforking mode without "
89+
"thread support. This might lead to crashing workers. "
90+
'Please run uWSGI with both "--enable-threads" and '
91+
'"--py-call-uwsgi-fork-hooks" for full support.'
5192
)
5293
)
94+
95+
return False
96+
97+
return True

sentry_sdk/client.py

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from datetime import datetime, timezone
66
from importlib import import_module
77

8+
from sentry_sdk._compat import check_uwsgi_thread_support
89
from sentry_sdk.utils import (
910
capture_internal_exceptions,
1011
current_stacktrace,
@@ -18,7 +19,7 @@
1819
)
1920
from sentry_sdk.serializer import serialize
2021
from sentry_sdk.tracing import trace
21-
from sentry_sdk.transport import make_transport
22+
from sentry_sdk.transport import HttpTransport, make_transport
2223
from sentry_sdk.consts import (
2324
DEFAULT_MAX_VALUE_LENGTH,
2425
DEFAULT_OPTIONS,
@@ -229,28 +230,15 @@ def _capture_envelope(envelope):
229230

230231
self.metrics_aggregator = None # type: Optional[MetricsAggregator]
231232
experiments = self.options.get("_experiments", {})
232-
if experiments.get("enable_metrics", True) or experiments.get(
233-
"force_enable_metrics", False
234-
):
235-
try:
236-
import uwsgi # type: ignore
237-
except ImportError:
238-
uwsgi = None
239-
240-
if uwsgi is not None and not experiments.get(
241-
"force_enable_metrics", False
242-
):
243-
logger.warning("Metrics currently not supported with uWSGI.")
244-
245-
else:
246-
from sentry_sdk.metrics import MetricsAggregator
247-
248-
self.metrics_aggregator = MetricsAggregator(
249-
capture_func=_capture_envelope,
250-
enable_code_locations=bool(
251-
experiments.get("metric_code_locations", True)
252-
),
253-
)
233+
if experiments.get("enable_metrics", True):
234+
from sentry_sdk.metrics import MetricsAggregator
235+
236+
self.metrics_aggregator = MetricsAggregator(
237+
capture_func=_capture_envelope,
238+
enable_code_locations=bool(
239+
experiments.get("metric_code_locations", True)
240+
),
241+
)
254242

255243
max_request_body_size = ("always", "never", "small", "medium")
256244
if self.options["max_request_body_size"] not in max_request_body_size:
@@ -296,6 +284,16 @@ def _capture_envelope(envelope):
296284

297285
self._setup_instrumentation(self.options.get("functions_to_trace", []))
298286

287+
if (
288+
self.monitor
289+
or self.metrics_aggregator
290+
or has_profiling_enabled(self.options)
291+
or isinstance(self.transport, HttpTransport)
292+
):
293+
# If we have anything on that could spawn a background thread, we
294+
# need to check if it's safe to use them.
295+
check_uwsgi_thread_support()
296+
299297
@property
300298
def dsn(self):
301299
# type: () -> Optional[str]

sentry_sdk/consts.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ class EndpointType(Enum):
5656
"transport_zlib_compression_level": Optional[int],
5757
"transport_num_pools": Optional[int],
5858
"enable_metrics": Optional[bool],
59-
"force_enable_metrics": Optional[bool],
6059
"metrics_summary_sample_rate": Optional[float],
6160
"should_summarize_metric": Optional[Callable[[str, MetricTags], bool]],
6261
"before_emit_metric": Optional[Callable[[str, MetricTags], bool]],

sentry_sdk/worker.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import threading
33

44
from time import sleep, time
5-
from sentry_sdk._compat import check_thread_support
65
from sentry_sdk._queue import Queue, FullError
76
from sentry_sdk.utils import logger
87
from sentry_sdk.consts import DEFAULT_QUEUE_SIZE
@@ -21,7 +20,6 @@
2120
class BackgroundWorker:
2221
def __init__(self, queue_size=DEFAULT_QUEUE_SIZE):
2322
# type: (int) -> None
24-
check_thread_support()
2523
self._queue = Queue(queue_size) # type: Queue
2624
self._lock = threading.Lock()
2725
self._thread = None # type: Optional[threading.Thread]

tests/test_client.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,3 +1162,43 @@ def test_error_sampler(_, sentry_init, capture_events, test_config):
11621162

11631163
# Ensure two arguments (the event and hint) were passed to the sampler function
11641164
assert len(test_config.sampler_function_mock.call_args[0]) == 2
1165+
1166+
1167+
@pytest.mark.forked
1168+
@pytest.mark.parametrize(
1169+
"opt,missing_flags",
1170+
[
1171+
# lazy mode with enable-threads, no warning
1172+
[{"enable-threads": True, "lazy-apps": True}, []],
1173+
[{"enable-threads": "true", "lazy-apps": b"1"}, []],
1174+
# preforking mode with enable-threads and py-call-uwsgi-fork-hooks, no warning
1175+
[{"enable-threads": True, "py-call-uwsgi-fork-hooks": True}, []],
1176+
[{"enable-threads": b"true", "py-call-uwsgi-fork-hooks": b"on"}, []],
1177+
# lazy mode, no enable-threads, warning
1178+
[{"lazy-apps": True}, ["--enable-threads"]],
1179+
[{"enable-threads": b"false", "lazy-apps": True}, ["--enable-threads"]],
1180+
[{"enable-threads": b"0", "lazy": True}, ["--enable-threads"]],
1181+
# preforking mode, no enable-threads or py-call-uwsgi-fork-hooks, warning
1182+
[{}, ["--enable-threads", "--py-call-uwsgi-fork-hooks"]],
1183+
[{"processes": b"2"}, ["--enable-threads", "--py-call-uwsgi-fork-hooks"]],
1184+
[{"enable-threads": True}, ["--py-call-uwsgi-fork-hooks"]],
1185+
[{"enable-threads": b"1"}, ["--py-call-uwsgi-fork-hooks"]],
1186+
[
1187+
{"enable-threads": b"false"},
1188+
["--enable-threads", "--py-call-uwsgi-fork-hooks"],
1189+
],
1190+
[{"py-call-uwsgi-fork-hooks": True}, ["--enable-threads"]],
1191+
],
1192+
)
1193+
def test_uwsgi_warnings(sentry_init, recwarn, opt, missing_flags):
1194+
uwsgi = mock.MagicMock()
1195+
uwsgi.opt = opt
1196+
with mock.patch.dict("sys.modules", uwsgi=uwsgi):
1197+
sentry_init(profiles_sample_rate=1.0)
1198+
if missing_flags:
1199+
assert len(recwarn) == 1
1200+
record = recwarn.pop()
1201+
for flag in missing_flags:
1202+
assert flag in str(record.message)
1203+
else:
1204+
assert not recwarn

0 commit comments

Comments
 (0)