Skip to content

Commit 2ebdf7c

Browse files
committed
Execution context - use thread locals if gevent/eventlet has monkey patched it #453
* Extract out logic to decide which backing to use for execution_context into a function init_execution_context * Check if gevent or eventlet has monkey patched _threading.local, if it has then use elasticapm.context.threadlocal as the backing.
1 parent a9d565b commit 2ebdf7c

File tree

4 files changed

+63
-4
lines changed

4 files changed

+63
-4
lines changed

elasticapm/context/__init__.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,40 @@
2727
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
2828
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
2929
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
31+
32+
def init_execution_context():
33+
# If _threading_local has been monkeypatched (by gevent or eventlet), then
34+
# we should assume it's use as this will be the most "green-thread safe"
35+
if threading_local_monkey_patched():
36+
from elasticapm.context.threadlocal import execution_context
37+
38+
return execution_context
39+
40+
try:
41+
from elasticapm.context.contextvars import execution_context
42+
except ImportError:
43+
from elasticapm.context.threadlocal import execution_context
44+
return execution_context
45+
46+
47+
def threading_local_monkey_patched():
48+
# Returns True if thread locals have been patched by either gevent of
49+
# eventlet
50+
try:
51+
from gevent.monkey import is_object_patched
52+
except ImportError:
53+
pass
54+
else:
55+
if is_object_patched("_threading", "local"):
56+
return True
57+
58+
try:
59+
from eventlet.patcher import is_monkey_patched
60+
except ImportError:
61+
pass
62+
else:
63+
if is_monkey_patched("thread"):
64+
return True
65+
66+
return False

elasticapm/traces.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737

3838
from elasticapm.conf import constants
3939
from elasticapm.conf.constants import SPAN, TRANSACTION
40+
from elasticapm.context import init_execution_context
4041
from elasticapm.utils import compat, encoding, get_name_from_func
4142
from elasticapm.utils.disttracing import TraceParent, TracingOptions
4243

@@ -51,10 +52,7 @@
5152
TAG_RE = re.compile('[.*"]')
5253

5354

54-
try:
55-
from elasticapm.context.contextvars import execution_context
56-
except ImportError:
57-
from elasticapm.context.threadlocal import execution_context
55+
execution_context = init_execution_context()
5856

5957

6058
class Transaction(object):

tests/context/__init__.py

Whitespace-only changes.

tests/context/test_context.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import sys
2+
3+
import elasticapm.context
4+
from elasticapm.context.threadlocal import ThreadLocalContext
5+
6+
7+
def test_execution_context_backing():
8+
execution_context = elasticapm.context.init_execution_context()
9+
10+
if sys.version_info[0] == 3 and sys.version_info[1] >= 7:
11+
from elasticapm.context.contextvars import ContextVarsContext
12+
13+
assert isinstance(execution_context, ContextVarsContext)
14+
else:
15+
assert isinstance(execution_context, ThreadLocalContext)
16+
17+
18+
def test_execution_context_monkeypatched(monkeypatch):
19+
with monkeypatch.context() as m:
20+
m.setattr(elasticapm.context, "threading_local_monkey_patched", lambda: True)
21+
execution_context = elasticapm.context.init_execution_context()
22+
23+
# Should always use ThreadLocalContext when thread local is monkey patched
24+
assert isinstance(execution_context, ThreadLocalContext)

0 commit comments

Comments
 (0)