Skip to content

Commit 0868579

Browse files
mergify[bot]majorgreysKyle-Verhoog
authored
fix(profiling): ensure RLock uses original allocate_lock (#4131) (#4138)
## Description #4108 replaced an internal use of the original `threading.Lock` function rather than the `gevent.threading.Lock` with the original `threading.RLock` function. However, at least in Python 2, there is an indirection with the implementation of `threading._RLock` class where it the `threading._allocate_lock` called is in fact `gevent.threading._allocate_lock`. When the gevent patched lock is used instead, we can encounter deadlocks or even gevent exceptions like the following: ``` ddtrace.profiling.collector.stack._ThreadSpanLinks.get_active_span_from_thread_id File "ddtrace/profiling/_threading.pyx", line 112, in ddtrace.profiling._threading._ThreadLink.get_object File "/usr/local/lib/python2.7/threading.py", line 174, in acquire rc = self.__block.acquire(blocking) File "src/gevent/_semaphore.py", line 198, in gevent._semaphore.Semaphore.acquire (src/gevent/gevent._semaphore.c:4541) File "src/gevent/_semaphore.py", line 226, in gevent._semaphore.Semaphore.acquire (src/gevent/gevent._semaphore.c:4367) File "src/gevent/_semaphore.py", line 166, in gevent._semaphore.Semaphore._do_wait (src/gevent/gevent._semaphore.c:3562) File "/usr/local/lib/python2.7/site-packages/gevent/hub.py", line 630, in switch return RawGreenlet.switch(self) gevent.hub.LoopExit: ('This operation would block forever', <Hub at 0x7fe2bda052d0 epoll pending=0 ref=0 fileno=267>) ``` The added test for the issue: https://app.circleci.com/pipelines/github/DataDog/dd-trace-py/20715/workflows/602ec8d6-d8ba-4191-ada8-0da76adbdc6b/jobs/1405224 ## Checklist - [x] Title must conform to [conventional commit](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional). - [x] Add additional sections for `feat` and `fix` pull requests. - [x] Ensure tests are passing for affected code. - [x] [Library documentation](https://github.com/DataDog/dd-trace-py/tree/1.x/docs) and/or [Datadog's documentation site](https://github.com/DataDog/documentation/) is updated. Link to doc PR in description. ## Reviewer Checklist - [x] Title is accurate. - [x] Description motivates each change. - [x] No unnecessary changes were introduced in this PR. - [x] PR cannot be broken up into smaller PRs. - [x] Avoid breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes unless absolutely necessary. - [x] Tests provided or description of manual testing performed is included in the code or PR. - [x] Release note has been added for fixes and features, or else `changelog/no-changelog` label added. - [x] All relevant GitHub issues are correctly linked. - [ ] Backports are identified and tagged with Mergifyio. - [ ] Add to milestone. (cherry picked from commit b0d8765) # Conflicts: # ddtrace/internal/nogevent.py Co-authored-by: Tahir H. Butt <[email protected]> Co-authored-by: Kyle Verhoog <[email protected]>
1 parent 9960cd7 commit 0868579

File tree

4 files changed

+58
-1
lines changed

4 files changed

+58
-1
lines changed

ddtrace/internal/nogevent.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,43 @@ def is_module_patched(module):
3737
thread_get_ident = get_original(six.moves._thread.__name__, "get_ident")
3838
Thread = get_original("threading", "Thread")
3939
Lock = get_original("threading", "Lock")
40-
RLock = get_original("threading", "RLock")
40+
41+
42+
if six.PY2 and is_module_patched("threading"):
43+
_allocate_lock = get_original("threading", "_allocate_lock")
44+
_threading_RLock = get_original("threading", "_RLock")
45+
_threading_Verbose = get_original("threading", "_Verbose")
46+
47+
class _RLock(_threading_RLock):
48+
"""Patched RLock to ensure threading._allocate_lock is called rather than
49+
gevent.threading._allocate_lock if patching has occurred. This is not
50+
necessary in Python 3 where the RLock function uses the _CRLock so is
51+
unaffected by gevent patching.
52+
"""
53+
54+
def __init__(self, verbose=None):
55+
# We want to avoid calling the RLock init as it will allocate a gevent lock
56+
# That means we have to reproduce the code from threading._RLock.__init__ here
57+
# https://github.com/python/cpython/blob/8d21aa21f2cbc6d50aab3f420bb23be1d081dac4/Lib/threading.py#L132-L136
58+
_threading_Verbose.__init__(self, verbose)
59+
self.__block = _allocate_lock()
60+
self.__owner = None
61+
self.__count = 0
62+
63+
def RLock(*args, **kwargs):
64+
return _RLock(*args, **kwargs)
65+
66+
else:
67+
# We do not patch RLock in Python 3 however for < 3.7 the C implementation of
68+
# RLock might not be available as the _thread module is optional. In that
69+
# case, the Python implementation will be used. This means there is still
70+
# the possibility that RLock in Python 3 will cause problems for gevent with
71+
# ddtrace profiling enabled though it remains an open question when that
72+
# would be the case for the supported platforms.
73+
# https://github.com/python/cpython/blob/c19983125a42a4b4958b11a26ab5e03752c956fc/Lib/threading.py#L38-L41
74+
# https://github.com/python/cpython/blob/c19983125a42a4b4958b11a26ab5e03752c956fc/Doc/library/_thread.rst#L26-L27
75+
RLock = get_original("threading", "RLock")
76+
4177

4278
is_threading_patched = is_module_patched("threading")
4379

docs/spelling_wordlist.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ rq
133133
runnable
134134
runtime
135135
runtime-id
136+
RLock
136137
sanic
137138
screenshots
138139
serializable
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
fixes:
3+
- |
4+
profiling: internal use of RLock needs to ensure original threading locks are used rather than gevent threading lock. Because of an indirection in the initialization of the original RLock, we end up getting an underlying gevent lock. We work around this behavior with gevent by creating a patched RLock for use internally.

tests/profiling/test_nogevent.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import os
2+
3+
import pytest
4+
import six
5+
6+
from ddtrace.internal import nogevent
7+
8+
9+
TESTING_GEVENT = os.getenv("DD_PROFILE_TEST_GEVENT", False)
10+
11+
12+
@pytest.mark.skipif(not TESTING_GEVENT or six.PY3, reason="Not testing gevent or testing on Python 3")
13+
def test_nogevent_rlock():
14+
import gevent
15+
16+
assert not isinstance(nogevent.RLock()._RLock__block, gevent.thread.LockType)

0 commit comments

Comments
 (0)