Skip to content

Commit 87e5749

Browse files
authored
fix(transport): Detect eventlet's Queue monkeypatch and work around it (getsentry#484)
* fix(transport): Detect eventlet's Queue monkeypatch and work around it * fix: Remove nonsense changes * doc: Add comment * fix: Fix tests under PyPy * ref: Unify greenlet/eventlet fixtures
1 parent ebc00b2 commit 87e5749

File tree

5 files changed

+73
-30
lines changed

5 files changed

+73
-30
lines changed

sentry_sdk/worker.py

+20-3
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,33 @@ def _timed_queue_join(self, timeout):
4545
# type: (float) -> bool
4646
deadline = time() + timeout
4747
queue = self._queue
48-
queue.all_tasks_done.acquire() # type: ignore
48+
49+
real_all_tasks_done = getattr(
50+
queue, "all_tasks_done", None
51+
) # type: Optional[Any]
52+
if real_all_tasks_done is not None:
53+
real_all_tasks_done.acquire()
54+
all_tasks_done = real_all_tasks_done # type: Optional[Any]
55+
elif queue.__module__.startswith("eventlet."):
56+
all_tasks_done = getattr(queue, "_cond", None)
57+
else:
58+
all_tasks_done = None
59+
4960
try:
5061
while queue.unfinished_tasks: # type: ignore
5162
delay = deadline - time()
5263
if delay <= 0:
5364
return False
54-
queue.all_tasks_done.wait(timeout=delay) # type: ignore
65+
if all_tasks_done is not None:
66+
all_tasks_done.wait(timeout=delay)
67+
else:
68+
# worst case, we just poll the number of remaining tasks
69+
sleep(0.1)
70+
5571
return True
5672
finally:
57-
queue.all_tasks_done.release() # type: ignore
73+
if real_all_tasks_done is not None:
74+
real_all_tasks_done.release() # type: ignore
5875

5976
def start(self):
6077
# type: () -> None

test-requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ Werkzeug==0.15.3
66
pytest-localserver==0.4.1
77
pytest-cov==2.6.0
88
gevent
9+
eventlet

tests/conftest.py

+32
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
import pytest
66

7+
import gevent
8+
import eventlet
9+
710
import sentry_sdk
811
from sentry_sdk._compat import reraise, string_types, iteritems
912
from sentry_sdk.transport import Transport
@@ -100,6 +103,9 @@ def _capture_internal_warnings():
100103
if "Something has already installed a non-asyncio" in str(warning.message):
101104
continue
102105

106+
if "dns.hash" in str(warning.message) or "dns/namedict" in warning.filename:
107+
continue
108+
103109
raise AssertionError(warning)
104110

105111

@@ -235,3 +241,29 @@ def read_event(self):
235241

236242
def read_flush(self):
237243
assert self.file.readline() == b"flush\n"
244+
245+
246+
# scope=session ensures that fixture is run earlier
247+
@pytest.fixture(scope="session", params=[None, "eventlet", "gevent"])
248+
def maybe_monkeypatched_threading(request):
249+
if request.param == "eventlet":
250+
try:
251+
eventlet.monkey_patch()
252+
except AttributeError as e:
253+
if "'thread.RLock' object has no attribute" in str(e):
254+
# https://bitbucket.org/pypy/pypy/issues/2962/gevent-cannot-patch-rlock-under-pypy-27-7
255+
pytest.skip("https://github.com/eventlet/eventlet/issues/546")
256+
else:
257+
raise
258+
elif request.param == "gevent":
259+
try:
260+
gevent.monkey.patch_all()
261+
except Exception as e:
262+
if "_RLock__owner" in str(e):
263+
pytest.skip("https://github.com/gevent/gevent/issues/1380")
264+
else:
265+
raise
266+
else:
267+
assert request.param is None
268+
269+
return request.param

tests/test_transport.py

+14-2
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,19 @@ def inner(*args, **kwargs):
2222

2323

2424
@pytest.mark.parametrize("debug", (True, False))
25-
def test_transport_works(httpserver, request, capsys, caplog, debug, make_client):
25+
@pytest.mark.parametrize("client_flush_method", ["close", "flush"])
26+
def test_transport_works(
27+
httpserver,
28+
request,
29+
capsys,
30+
caplog,
31+
debug,
32+
make_client,
33+
client_flush_method,
34+
maybe_monkeypatched_threading,
35+
):
2636
httpserver.serve_content("ok", 200)
37+
2738
caplog.set_level(logging.DEBUG)
2839

2940
client = make_client(
@@ -34,7 +45,8 @@ def test_transport_works(httpserver, request, capsys, caplog, debug, make_client
3445

3546
add_breadcrumb(level="info", message="i like bread", timestamp=datetime.now())
3647
capture_message("löl")
37-
client.close()
48+
49+
getattr(client, client_flush_method)()
3850

3951
out, err = capsys.readouterr()
4052
assert not err and not out

tests/utils/test_contextvars.py

+6-25
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,18 @@
11
import random
22
import time
33

4-
import pytest
5-
import gevent
6-
74

85
from sentry_sdk.utils import _is_threading_local_monkey_patched
96

107

11-
def try_gevent_patch_all():
12-
try:
13-
gevent.monkey.patch_all()
14-
except Exception as e:
15-
if "_RLock__owner" in str(e):
16-
pytest.skip("https://github.com/gevent/gevent/issues/1380")
17-
else:
18-
raise
19-
20-
21-
def test_gevent_is_patched():
22-
try_gevent_patch_all()
23-
assert _is_threading_local_monkey_patched()
24-
25-
26-
def test_gevent_is_not_patched():
27-
assert not _is_threading_local_monkey_patched()
28-
8+
def test_thread_local_is_patched(maybe_monkeypatched_threading):
9+
if maybe_monkeypatched_threading is None:
10+
assert not _is_threading_local_monkey_patched()
11+
else:
12+
assert _is_threading_local_monkey_patched()
2913

30-
@pytest.mark.parametrize("with_gevent", [True, False])
31-
def test_leaks(with_gevent):
32-
if with_gevent:
33-
try_gevent_patch_all()
3414

15+
def test_leaks(maybe_monkeypatched_threading):
3516
import threading
3617

3718
# Need to explicitly call _get_contextvars because the SDK has already

0 commit comments

Comments
 (0)