diff --git a/requests_mock/mocker.py b/requests_mock/mocker.py index 3b787eb..095230d 100644 --- a/requests_mock/mocker.py +++ b/requests_mock/mocker.py @@ -10,7 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +import contextlib import functools +import sys import threading import types @@ -30,7 +32,26 @@ _original_send = requests.Session.send -_send_lock = threading.Lock() +# NOTE(phodge): we need to use an RLock (reentrant lock) here because +# requests.Session.send() is reentrant. See further comments where we +# monkeypatch get_adapter() +_send_lock = threading.RLock() + + +@contextlib.contextmanager +def threading_rlock(timeout): + kwargs = {} + if sys.version_info.major >= 3: + # python2 doesn't support the timeout argument + kwargs['timeout'] = timeout + + if not _send_lock.acquire(**kwargs): + raise Exception("Could not acquire threading lock - possible deadlock scenario") + + try: + yield + finally: + _send_lock.release() def _is_bound_method(method): @@ -134,8 +155,17 @@ def _fake_send(session, request, **kwargs): # are multiple threads running - one thread could restore the # original get_adapter() just as a second thread is about to # execute _original_send() below - with _send_lock: + with threading_rlock(timeout=10): # mock get_adapter + # + # NOTE(phodge): requests.Session.send() is actually + # reentrant due to how it resolves redirects with nested + # calls to send(), however the reentry occurs _after_ the + # call to self.get_adapter(), so it doesn't matter that we + # will restore _last_get_adapter before a nested send() has + # completed as long as we monkeypatch get_adapter() each + # time immediately before calling original send() like we + # are doing here. _set_method(session, "get_adapter", _fake_get_adapter) # NOTE(jamielennox): self._last_send vs _original_send. Whilst it