Skip to content

Commit 3d0aa8b

Browse files
committed
don't use ChilledQueue if queue.Queue has been monkeypatched (#435)
this should fix issues with eventlet. To this effect, a bare bones test for eventlet was added. closes #435
1 parent 5a287a4 commit 3d0aa8b

File tree

9 files changed

+99
-11
lines changed

9 files changed

+99
-11
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Unreleased
44
[Check the diff](https://github.com/elastic/apm-agent-python/compare/v4.2.0...master)
55
* fixed an issue with the certificate pinning feature introduced in 4.2.0 (#433, #434)
6+
* fixed incompatibility with eventlet introduced in 4.2.0 (#435, #436)
67

78
## v4.2.0
89
[Check the diff](https://github.com/elastic/apm-agent-python/compare/v4.1.0...v4.2.0)

conftest.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@
3030
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
3131
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3232

33+
try:
34+
import eventlet
35+
36+
eventlet.monkey_patch()
37+
except ImportError:
38+
pass
39+
3340
import sys
3441
from os.path import abspath, dirname, join
3542

elasticapm/transport/base.py

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ def __init__(
8888
self._max_flush_time = max_flush_time
8989
self._max_buffer_size = max_buffer_size
9090
self._queued_data = None
91-
self._event_queue = ChilledQueue(maxsize=10000, chill_until=queue_chill_count, max_chill_time=queue_chill_time)
91+
self._event_queue = self._init_event_queue(chill_until=queue_chill_count, max_chill_time=queue_chill_time)
92+
self._is_chilled_queue = isinstance(self._event_queue, ChilledQueue)
9293
self._event_process_thread = threading.Thread(target=self._process_queue, name="eapm event processor thread")
9394
self._event_process_thread.daemon = True
9495
self._last_flush = timeit.default_timer()
@@ -105,18 +106,14 @@ def __init__(
105106
def queue(self, event_type, data, flush=False):
106107
try:
107108
self._flushed.clear()
108-
self._event_queue.put((event_type, data, flush), block=False, chill=not (event_type == "close" or flush))
109+
kwargs = {"chill": not (event_type == "close" or flush)} if self._is_chilled_queue else {}
110+
self._event_queue.put((event_type, data, flush), block=False, **kwargs)
111+
109112
except compat.queue.Full:
110113
logger.warning("Event of type %s dropped due to full event queue", event_type)
111114

112115
def _process_queue(self):
113-
def init_buffer():
114-
buffer = gzip.GzipFile(fileobj=compat.BytesIO(), mode="w", compresslevel=self._compress_level)
115-
data = (self._json_serializer({"metadata": self._metadata}) + "\n").encode("utf-8")
116-
buffer.write(data)
117-
return buffer
118-
119-
buffer = init_buffer()
116+
buffer = self._init_buffer()
120117
buffer_written = False
121118
# add some randomness to timeout to avoid stampedes of several workers that are booted at the same time
122119
max_flush_time = self._max_flush_time * random.uniform(0.9, 1.1) if self._max_flush_time else None
@@ -166,11 +163,34 @@ def init_buffer():
166163
if buffer_written:
167164
self._flush(buffer)
168165
self._last_flush = timeit.default_timer()
169-
buffer = init_buffer()
166+
buffer = self._init_buffer()
170167
buffer_written = False
171168
max_flush_time = self._max_flush_time * random.uniform(0.9, 1.1) if self._max_flush_time else None
172169
self._flushed.set()
173170

171+
def _init_buffer(self):
172+
buffer = gzip.GzipFile(fileobj=compat.BytesIO(), mode="w", compresslevel=self._compress_level)
173+
data = (self._json_serializer({"metadata": self._metadata}) + "\n").encode("utf-8")
174+
buffer.write(data)
175+
return buffer
176+
177+
def _init_event_queue(self, chill_until, max_chill_time):
178+
# some libraries like eventlet monkeypatch queue.Queue and switch out the implementation.
179+
# In those cases we can't rely on internals of queue.Queue to be there, so we simply use
180+
# their queue and forgo the optimizations of ChilledQueue. In the case of eventlet, this
181+
# isn't really a loss, because the main reason for ChilledQueue (avoiding context switches
182+
# due to the event processor thread being woken up all the time) is not an issue.
183+
if all(
184+
(
185+
hasattr(compat.queue.Queue, "not_full"),
186+
hasattr(compat.queue.Queue, "not_empty"),
187+
hasattr(compat.queue.Queue, "unfinished_tasks"),
188+
)
189+
):
190+
return ChilledQueue(maxsize=10000, chill_until=chill_until, max_chill_time=max_chill_time)
191+
else:
192+
return compat.queue.Queue(maxsize=10000)
193+
174194
def _flush(self, buffer):
175195
"""
176196
Flush the queue. This method should only be called from the event processing queue

tests/.jenkins_exclude.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,6 @@ exclude:
121121
# opentracing
122122
- PYTHON_VERSION: python-3.3
123123
FRAMEWORK: opentracing-newest
124+
# eventlet
125+
- PYTHON_VERSION: pypy-2 #currently fails on pypy2
126+
FRAMEWORK: eventlet-newest

tests/.jenkins_framework.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@ FRAMEWORK:
2727
- elasticsearch-6
2828
- cassandra-newest
2929
- psutil-newest
30+
- eventlet-newest
3031

tests/.jenkins_framework_full.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,5 @@ FRAMEWORK:
5555
- psutil-newest
5656
- psutil-5.0
5757
- psutil-4.0
58-
58+
- eventlet-newest
5959

tests/contrib/test_eventlet.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# BSD 3-Clause License
2+
#
3+
# Copyright (c) 2019, Elasticsearch BV
4+
# All rights reserved.
5+
#
6+
# Redistribution and use in source and binary forms, with or without
7+
# modification, are permitted provided that the following conditions are met:
8+
#
9+
# * Redistributions of source code must retain the above copyright notice, this
10+
# list of conditions and the following disclaimer.
11+
#
12+
# * Redistributions in binary form must reproduce the above copyright notice,
13+
# this list of conditions and the following disclaimer in the documentation
14+
# and/or other materials provided with the distribution.
15+
#
16+
# * Neither the name of the copyright holder nor the names of its
17+
# contributors may be used to endorse or promote products derived from
18+
# this software without specific prior written permission.
19+
#
20+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
31+
import pytest # isort:skip
32+
33+
eventlet = pytest.importorskip("eventlet") # isort:skip
34+
35+
import os
36+
37+
import elasticapm
38+
from elasticapm.conf import constants
39+
from eventlet.patcher import is_monkey_patched
40+
41+
pytestmark = pytest.mark.eventlet
42+
43+
44+
def test_transaction_with_eventlet(sending_elasticapm_client):
45+
assert is_monkey_patched(os)
46+
transaction = sending_elasticapm_client.begin_transaction("test")
47+
with elasticapm.capture_span("bla"):
48+
pass
49+
sending_elasticapm_client.end_transaction("test", "OK")
50+
sending_elasticapm_client.close()
51+
assert len(sending_elasticapm_client.httpserver.requests) == 1
52+
assert sending_elasticapm_client.httpserver.payloads[0][1][constants.SPAN]
53+
assert sending_elasticapm_client.httpserver.payloads[0][2][constants.TRANSACTION]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
eventlet
2+
-r requirements-base.txt

tests/scripts/envs/eventlet.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export PYTEST_MARKER="-m eventlet"

0 commit comments

Comments
 (0)