diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 45e5376d1c..4d83397dbf 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1348,6 +1348,7 @@ def _is_contextvars_broken() -> bool: except ImportError: pass + modules_before_import = set(sys.modules.keys()) try: import greenlet from eventlet.patcher import is_monkey_patched # type: ignore @@ -1364,6 +1365,18 @@ def _is_contextvars_broken() -> bool: return True except ImportError: pass + except Exception: + # Importing eventlet/greenlet can fail in unexpected ways depending on + # which combination of eventlet, greenlet, and other monkeypatched + # modules (e.g. dnspython, httpcore) happen to be installed. When that + # happens, the partially-imported module can be left behind in + # sys.modules, which would make subsequent imports of it silently + # reuse the broken module instead of retrying the import. Clean up + # any modules that got added during the failed import attempt. + # See https://github.com/getsentry/sentry-python/issues/7202. + modules_after_import = set(sys.modules.keys()) + for module_name in modules_after_import - modules_before_import: + del sys.modules[module_name] return False diff --git a/tests/utils/test_contextvars.py b/tests/utils/test_contextvars.py index 50881314c1..f18f915311 100644 --- a/tests/utils/test_contextvars.py +++ b/tests/utils/test_contextvars.py @@ -1,5 +1,8 @@ +import builtins import random +import sys import time +import types from unittest import mock import pytest @@ -50,3 +53,38 @@ def test_leaks(maybe_monkeypatched_threading): @mock.patch("sentry_sdk.utils._is_contextvars_broken", return_value=True) def test_leaks_when_is_contextvars_broken_is_false(maybe_monkeypatched_threading): _run_contextvar_threaded_test() + + +def test_is_contextvars_broken_survives_eventlet_attributeerror(monkeypatch): + """ + Regression test for https://github.com/getsentry/sentry-python/issues/7202 + + Importing eventlet/greenlet can raise errors other than ImportError + depending on which combination of eventlet, greenlet, and other + monkeypatched modules (e.g. dnspython, httpcore) happen to be installed. + _is_contextvars_broken() should not crash in that case, and it should not + leave a broken partially-imported module behind in sys.modules. + """ + from sentry_sdk import utils + + monkeypatch.delitem(sys.modules, "greenlet", raising=False) + monkeypatch.delitem(sys.modules, "eventlet", raising=False) + monkeypatch.delitem(sys.modules, "eventlet.patcher", raising=False) + + real_import = builtins.__import__ + + def fake_import(name, *args, **kwargs): + if name == "greenlet": + sys.modules["greenlet"] = types.ModuleType("greenlet") + raise AttributeError("module 'dns.rdtypes' has no attribute 'ANY'") + return real_import(name, *args, **kwargs) + + with mock.patch("builtins.__import__", side_effect=fake_import): + with mock.patch( + "gevent.monkey.is_object_patched", return_value=False, create=True + ): + with mock.patch.dict(sys.modules, {"gevent": None}, clear=False): + result = utils._is_contextvars_broken() + + assert result is False + assert "greenlet" not in sys.modules