Skip to content

Commit 4d1b814

Browse files
authored
ref(uwsgi): Warn if uWSGI is set up without proper thread support (#2738)
1 parent 6f4fda5 commit 4d1b814

File tree

6 files changed

+125
-41
lines changed

6 files changed

+125
-41
lines changed

sentry_sdk/_compat.py

+57-12
Original file line numberDiff line numberDiff line change
@@ -140,29 +140,74 @@ def __new__(metacls, name, this_bases, d):
140140
return type.__new__(MetaClass, "temporary_class", (), {})
141141

142142

143-
def check_thread_support():
144-
# type: () -> None
143+
def check_uwsgi_thread_support():
144+
# type: () -> bool
145+
# We check two things here:
146+
#
147+
# 1. uWSGI doesn't run in threaded mode by default -- issue a warning if
148+
# that's the case.
149+
#
150+
# 2. Additionally, if uWSGI is running in preforking mode (default), it needs
151+
# the --py-call-uwsgi-fork-hooks option for the SDK to work properly. This
152+
# is because any background threads spawned before the main process is
153+
# forked are NOT CLEANED UP IN THE CHILDREN BY DEFAULT even if
154+
# --enable-threads is on. One has to explicitly provide
155+
# --py-call-uwsgi-fork-hooks to force uWSGI to run regular cpython
156+
# after-fork hooks that take care of cleaning up stale thread data.
145157
try:
146158
from uwsgi import opt # type: ignore
147159
except ImportError:
148-
return
160+
return True
161+
162+
from sentry_sdk.consts import FALSE_VALUES
163+
164+
def enabled(option):
165+
# type: (str) -> bool
166+
value = opt.get(option, False)
167+
if isinstance(value, bool):
168+
return value
169+
170+
if isinstance(value, bytes):
171+
try:
172+
value = value.decode()
173+
except Exception:
174+
pass
175+
176+
return value and str(value).lower() not in FALSE_VALUES
149177

150178
# When `threads` is passed in as a uwsgi option,
151179
# `enable-threads` is implied on.
152-
if "threads" in opt:
153-
return
180+
threads_enabled = "threads" in opt or enabled("enable-threads")
181+
fork_hooks_on = enabled("py-call-uwsgi-fork-hooks")
182+
lazy_mode = enabled("lazy-apps") or enabled("lazy")
154183

155-
# put here because of circular import
156-
from sentry_sdk.consts import FALSE_VALUES
184+
if lazy_mode and not threads_enabled:
185+
from warnings import warn
157186

158-
if str(opt.get("enable-threads", "0")).lower() in FALSE_VALUES:
187+
warn(
188+
Warning(
189+
"IMPORTANT: "
190+
"We detected the use of uWSGI without thread support. "
191+
"This might lead to unexpected issues. "
192+
'Please run uWSGI with "--enable-threads" for full support.'
193+
)
194+
)
195+
196+
return False
197+
198+
elif not lazy_mode and (not threads_enabled or not fork_hooks_on):
159199
from warnings import warn
160200

161201
warn(
162202
Warning(
163-
"We detected the use of uwsgi with disabled threads. "
164-
"This will cause issues with the transport you are "
165-
"trying to use. Please enable threading for uwsgi. "
166-
'(Add the "enable-threads" flag).'
203+
"IMPORTANT: "
204+
"We detected the use of uWSGI in preforking mode without "
205+
"thread support. This might lead to crashing workers. "
206+
'Please run uWSGI with both "--enable-threads" and '
207+
'"--py-call-uwsgi-fork-hooks" for full support.'
167208
)
168209
)
210+
211+
return False
212+
213+
return True

sentry_sdk/client.py

+27-24
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@
44
import random
55
import socket
66

7-
from sentry_sdk._compat import datetime_utcnow, string_types, text_type, iteritems
7+
from sentry_sdk._compat import (
8+
datetime_utcnow,
9+
string_types,
10+
text_type,
11+
iteritems,
12+
check_uwsgi_thread_support,
13+
)
814
from sentry_sdk.utils import (
915
capture_internal_exceptions,
1016
current_stacktrace,
@@ -18,7 +24,7 @@
1824
)
1925
from sentry_sdk.serializer import serialize
2026
from sentry_sdk.tracing import trace, has_tracing_enabled
21-
from sentry_sdk.transport import make_transport
27+
from sentry_sdk.transport import HttpTransport, make_transport
2228
from sentry_sdk.consts import (
2329
DEFAULT_MAX_VALUE_LENGTH,
2430
DEFAULT_OPTIONS,
@@ -249,28 +255,15 @@ def _capture_envelope(envelope):
249255

250256
self.metrics_aggregator = None # type: Optional[MetricsAggregator]
251257
experiments = self.options.get("_experiments", {})
252-
if experiments.get("enable_metrics", True) or experiments.get(
253-
"force_enable_metrics", False
254-
):
255-
try:
256-
import uwsgi # type: ignore
257-
except ImportError:
258-
uwsgi = None
259-
260-
if uwsgi is not None and not experiments.get(
261-
"force_enable_metrics", False
262-
):
263-
logger.warning("Metrics currently not supported with uWSGI.")
264-
265-
else:
266-
from sentry_sdk.metrics import MetricsAggregator
267-
268-
self.metrics_aggregator = MetricsAggregator(
269-
capture_func=_capture_envelope,
270-
enable_code_locations=bool(
271-
experiments.get("metric_code_locations", True)
272-
),
273-
)
258+
if experiments.get("enable_metrics", True):
259+
from sentry_sdk.metrics import MetricsAggregator
260+
261+
self.metrics_aggregator = MetricsAggregator(
262+
capture_func=_capture_envelope,
263+
enable_code_locations=bool(
264+
experiments.get("metric_code_locations", True)
265+
),
266+
)
274267

275268
max_request_body_size = ("always", "never", "small", "medium")
276269
if self.options["max_request_body_size"] not in max_request_body_size:
@@ -316,6 +309,16 @@ def _capture_envelope(envelope):
316309

317310
self._setup_instrumentation(self.options.get("functions_to_trace", []))
318311

312+
if (
313+
self.monitor
314+
or self.metrics_aggregator
315+
or has_profiling_enabled(self.options)
316+
or isinstance(self.transport, HttpTransport)
317+
):
318+
# If we have anything on that could spawn a background thread, we
319+
# need to check if it's safe to use them.
320+
check_uwsgi_thread_support()
321+
319322
@property
320323
def dsn(self):
321324
# type: () -> Optional[str]

sentry_sdk/consts.py

-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
"transport_zlib_compression_level": Optional[int],
4747
"transport_num_pools": Optional[int],
4848
"enable_metrics": Optional[bool],
49-
"force_enable_metrics": Optional[bool],
5049
"metrics_summary_sample_rate": Optional[float],
5150
"should_summarize_metric": Optional[Callable[[str, MetricTags], bool]],
5251
"before_emit_metric": Optional[Callable[[str, MetricTags], bool]],

sentry_sdk/hub.py

-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import copy
22
import sys
3-
43
from contextlib import contextmanager
54

65
from sentry_sdk._compat import with_metaclass

sentry_sdk/worker.py

-2
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(object):
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

+41-1
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
import subprocess
66
import sys
77
import time
8-
98
from textwrap import dedent
9+
1010
from sentry_sdk import (
1111
Hub,
1212
Client,
@@ -1316,3 +1316,43 @@ def test_error_sampler(_, sentry_init, capture_events, test_config):
13161316

13171317
# Ensure two arguments (the event and hint) were passed to the sampler function
13181318
assert len(test_config.sampler_function_mock.call_args[0]) == 2
1319+
1320+
1321+
@pytest.mark.forked
1322+
@pytest.mark.parametrize(
1323+
"opt,missing_flags",
1324+
[
1325+
# lazy mode with enable-threads, no warning
1326+
[{"enable-threads": True, "lazy-apps": True}, []],
1327+
[{"enable-threads": "true", "lazy-apps": b"1"}, []],
1328+
# preforking mode with enable-threads and py-call-uwsgi-fork-hooks, no warning
1329+
[{"enable-threads": True, "py-call-uwsgi-fork-hooks": True}, []],
1330+
[{"enable-threads": b"true", "py-call-uwsgi-fork-hooks": b"on"}, []],
1331+
# lazy mode, no enable-threads, warning
1332+
[{"lazy-apps": True}, ["--enable-threads"]],
1333+
[{"enable-threads": b"false", "lazy-apps": True}, ["--enable-threads"]],
1334+
[{"enable-threads": b"0", "lazy": True}, ["--enable-threads"]],
1335+
# preforking mode, no enable-threads or py-call-uwsgi-fork-hooks, warning
1336+
[{}, ["--enable-threads", "--py-call-uwsgi-fork-hooks"]],
1337+
[{"processes": b"2"}, ["--enable-threads", "--py-call-uwsgi-fork-hooks"]],
1338+
[{"enable-threads": True}, ["--py-call-uwsgi-fork-hooks"]],
1339+
[{"enable-threads": b"1"}, ["--py-call-uwsgi-fork-hooks"]],
1340+
[
1341+
{"enable-threads": b"false"},
1342+
["--enable-threads", "--py-call-uwsgi-fork-hooks"],
1343+
],
1344+
[{"py-call-uwsgi-fork-hooks": True}, ["--enable-threads"]],
1345+
],
1346+
)
1347+
def test_uwsgi_warnings(sentry_init, recwarn, opt, missing_flags):
1348+
uwsgi = mock.MagicMock()
1349+
uwsgi.opt = opt
1350+
with mock.patch.dict("sys.modules", uwsgi=uwsgi):
1351+
sentry_init(profiles_sample_rate=1.0)
1352+
if missing_flags:
1353+
assert len(recwarn) == 1
1354+
record = recwarn.pop()
1355+
for flag in missing_flags:
1356+
assert flag in str(record.message)
1357+
else:
1358+
assert not recwarn

0 commit comments

Comments
 (0)