diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 8493b037c3..d516d08887 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -26,6 +26,8 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh ## Removed +- Dropped support for gevent versions below 20.9. +- Dropped support for greenlet versions below 0.4.17. - The SDK no longer supports Python 3.6. The oldest supported version is now 3.7. - The `enable_tracing` option was removed. Use `traces_sample_rate=1.0` instead. - The deprecated `push_scope` and `configure_scope` APIs have been removed. Use `with new_scope():` to push a new scope and `scope = get_current_scope()` to retrieve the current scope instead. diff --git a/sentry_sdk/_init_implementation.py b/sentry_sdk/_init_implementation.py index 923fcf6df8..3051ae010b 100644 --- a/sentry_sdk/_init_implementation.py +++ b/sentry_sdk/_init_implementation.py @@ -1,7 +1,9 @@ +import re import warnings from typing import TYPE_CHECKING import sentry_sdk +from sentry_sdk.utils import logger, parse_version if TYPE_CHECKING: from typing import Any, ContextManager, Optional @@ -41,11 +43,32 @@ def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: c.close() -def _check_python_deprecations() -> None: - # Since we're likely to deprecate Python versions in the future, I'm keeping - # this handy function around. Use this to detect the Python version used and - # to output logger.warning()s if it's deprecated. - pass +def _check_version_deprecations() -> None: + try: + import gevent + + gevent_version = tuple( + int(part) for part in re.split(r"a|b|rc|\.", gevent.__version__)[:2] + ) + if gevent_version < (20, 9): + logger.warning( + "sentry-sdk 3.x supports gevent 20.9.0 or newer. " + "Please upgrade gevent or downgrade to sentry-sdk 2.x." + ) + except Exception: + pass + + try: + import greenlet + + greenlet_version = parse_version(greenlet.__version__) + if greenlet_version is not None and greenlet_version < (0, 4, 17): + logger.warning( + "sentry-sdk 3.x supports greenlet 0.4.17 or newer. " + "Please upgrade greenlet or downgrade to sentry-sdk 2.x." + ) + except Exception: + pass def _init(*args: "Optional[str]", **kwargs: "Any") -> "ContextManager[Any]": @@ -55,7 +78,7 @@ def _init(*args: "Optional[str]", **kwargs: "Any") -> "ContextManager[Any]": """ client = sentry_sdk.Client(*args, **kwargs) sentry_sdk.get_global_scope().set_client(client) - _check_python_deprecations() + _check_version_deprecations() rv = _InitGuard(client) return rv diff --git a/sentry_sdk/ai/monitoring.py b/sentry_sdk/ai/monitoring.py index 05aabd96a9..a8cce22cb0 100644 --- a/sentry_sdk/ai/monitoring.py +++ b/sentry_sdk/ai/monitoring.py @@ -1,6 +1,7 @@ import inspect import sys import warnings +from contextvars import ContextVar from functools import wraps from typing import TYPE_CHECKING @@ -11,14 +12,16 @@ from sentry_sdk.traces import StreamedSpan from sentry_sdk.tracing import Span from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ContextVar, capture_internal_exceptions, reraise +from sentry_sdk.utils import capture_internal_exceptions, reraise if TYPE_CHECKING: from typing import Any, Awaitable, Callable, Optional, TypeVar, Union F = TypeVar("F", bound=Union[Callable[..., Any], Callable[..., Awaitable[Any]]]) -_ai_pipeline_name = ContextVar("ai_pipeline_name", default=None) +_ai_pipeline_name: "ContextVar[Optional[str]]" = ContextVar( + "ai_pipeline_name", default=None +) def set_ai_pipeline_name(name: "Optional[str]") -> None: diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index f865cbac97..fb452d21dc 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -7,6 +7,7 @@ import uuid import warnings from collections.abc import Iterable, Mapping +from contextvars import ContextVar from datetime import datetime, timezone from importlib import import_module from typing import TYPE_CHECKING, Dict, List, cast, overload @@ -44,7 +45,6 @@ ) from sentry_sdk.utils import ( AnnotatedValue, - ContextVar, capture_internal_exceptions, current_stacktrace, datetime_from_isoformat, @@ -87,7 +87,7 @@ I = TypeVar("I", bound=Integration) # noqa: E741 -_client_init_debug = ContextVar("client_init_debug") +_client_init_debug: "ContextVar[bool]" = ContextVar("client_init_debug") SDK_INFO: "SDKInfo" = { diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py index 858bf273f2..eac6cb901a 100644 --- a/sentry_sdk/integrations/aiohttp.py +++ b/sentry_sdk/integrations/aiohttp.py @@ -41,8 +41,6 @@ should_propagate_trace, ) from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, SENSITIVE_DATA_SUBSTITUTE, AnnotatedValue, _register_control_flow_exception, @@ -108,14 +106,6 @@ def setup_once() -> None: version = parse_version(AIOHTTP_VERSION) _check_minimum_version(AioHttpIntegration, version) - if not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - raise DidNotEnable( - "The aiohttp integration for Sentry requires Python 3.7+ " - " or aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE - ) - # In the aiohttp integration, all of their HTTP responses are Exceptions. # Because they have to be raised and handled by the framework, we need to # register the exceptions as control flow exceptions so that we don't diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index abb0748820..12351e6e8c 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -7,6 +7,7 @@ import inspect import sys from contextlib import nullcontext +from contextvars import ContextVar from copy import deepcopy from functools import partial from typing import TYPE_CHECKING @@ -42,9 +43,6 @@ ) from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, - ContextVar, _get_installed_modules, capture_internal_exceptions, event_from_exception, @@ -62,7 +60,9 @@ from sentry_sdk.tracing import Span -_asgi_middleware_applied = ContextVar("sentry_asgi_middleware_applied") +_asgi_middleware_applied: "ContextVar[bool]" = ContextVar( + "sentry_asgi_middleware_applied" +) _DEFAULT_TRANSACTION_NAME = "generic ASGI request" @@ -114,7 +114,6 @@ class SentryAsgiMiddleware: def __init__( self, app: "Any", - unsafe_context_data: bool = False, transaction_style: str = "endpoint", mechanism_type: str = "asgi", span_origin: str = "manual", @@ -127,15 +126,7 @@ def __init__( data to sent events and basic handling for exceptions bubbling up through the middleware. - :param unsafe_context_data: Disable errors when a proper contextvars installation could not be found. We do not recommend changing this from the default. """ - if not unsafe_context_data and not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - raise RuntimeError( - "The ASGI middleware for Sentry requires Python 3.7+ " - "or the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE - ) if transaction_style not in TRANSACTION_STYLE_VALUES: raise ValueError( "Invalid value for transaction_style: %s (must be in %s)" diff --git a/sentry_sdk/integrations/dedupe.py b/sentry_sdk/integrations/dedupe.py index a0e9014666..a0cc88081f 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -1,13 +1,14 @@ import weakref +from contextvars import ContextVar from typing import TYPE_CHECKING import sentry_sdk from sentry_sdk.integrations import Integration from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.utils import ContextVar, logger +from sentry_sdk.utils import logger if TYPE_CHECKING: - from typing import Optional + from typing import Any, Optional from sentry_sdk._types import Event, Hint @@ -16,7 +17,7 @@ class DedupeIntegration(Integration): identifier = "dedupe" def __init__(self) -> None: - self._last_seen = ContextVar("last-seen") + self._last_seen: "ContextVar[Any]" = ContextVar("last-seen") @staticmethod def setup_once() -> None: diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 0f8dfcd24a..b5bd550be5 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -23,15 +23,12 @@ record_sql_queries, ) from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, SENSITIVE_DATA_SUBSTITUTE, AnnotatedValue, capture_internal_exceptions, ensure_integration_enabled, event_from_exception, has_data_collection_enabled, - logger, transaction_from_function, walk_exception_chain, ) @@ -346,19 +343,6 @@ def _patch_channels() -> None: except ImportError: return - if not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - # - # We cannot hard-raise here because channels may not be used at all in - # the current process. That is the case when running traditional WSGI - # workers in gunicorn+gevent and the websocket stuff in a separate - # process. - logger.warning( - "We detected that you are using Django channels 2.0." - + CONTEXTVARS_ERROR_MESSAGE - ) - from sentry_sdk.integrations.django.asgi import patch_channels_asgi_handler_impl patch_channels_asgi_handler_impl(AsgiHandler) @@ -370,16 +354,6 @@ def _patch_django_asgi_handler() -> None: except ImportError: return - if not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - # - # We cannot hard-raise here because Django's ASGI stuff may not be used - # at all. - logger.warning( - "We detected that you are using Django 3." + CONTEXTVARS_ERROR_MESSAGE - ) - from sentry_sdk.integrations.django.asgi import patch_django_asgi_handler_impl patch_django_asgi_handler_impl(ASGIHandler) diff --git a/sentry_sdk/integrations/django/asgi.py b/sentry_sdk/integrations/django/asgi.py index f3b1fd0898..4c5ac57175 100644 --- a/sentry_sdk/integrations/django/asgi.py +++ b/sentry_sdk/integrations/django/asgi.py @@ -99,7 +99,6 @@ async def sentry_patched_asgi_handler( middleware = SentryAsgiMiddleware( old_app.__get__(self, cls), - unsafe_context_data=True, span_origin=DjangoIntegration.origin, http_methods_to_capture=integration.http_methods_to_capture, )._run_asgi3 @@ -154,7 +153,6 @@ async def sentry_patched_asgi_handler( middleware = SentryAsgiMiddleware( lambda _scope: old_app.__get__(self, cls), - unsafe_context_data=True, span_origin=DjangoIntegration.origin, http_methods_to_capture=integration.http_methods_to_capture, ) diff --git a/sentry_sdk/integrations/django/middleware.py b/sentry_sdk/integrations/django/middleware.py index 01ed8962e0..dcd114e795 100644 --- a/sentry_sdk/integrations/django/middleware.py +++ b/sentry_sdk/integrations/django/middleware.py @@ -2,6 +2,7 @@ Create spans from Django middleware invocations """ +from contextvars import ContextVar from functools import wraps from typing import TYPE_CHECKING @@ -11,7 +12,6 @@ from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import ( - ContextVar, capture_internal_exceptions, transaction_from_function, ) @@ -24,7 +24,7 @@ F = TypeVar("F", bound=Callable[..., Any]) -_import_string_should_wrap_middleware = ContextVar( +_import_string_should_wrap_middleware: "ContextVar[bool]" = ContextVar( "import_string_should_wrap_middleware" ) diff --git a/sentry_sdk/integrations/litestar.py b/sentry_sdk/integrations/litestar.py index 8a3f09ffa0..dd80505132 100644 --- a/sentry_sdk/integrations/litestar.py +++ b/sentry_sdk/integrations/litestar.py @@ -89,7 +89,6 @@ def __init__( ) -> None: super().__init__( app=app, - unsafe_context_data=False, transaction_style="endpoint", mechanism_type="asgi", span_origin=span_origin, diff --git a/sentry_sdk/integrations/sanic.py b/sentry_sdk/integrations/sanic.py index 9f1ece76c8..3e91361ce5 100644 --- a/sentry_sdk/integrations/sanic.py +++ b/sentry_sdk/integrations/sanic.py @@ -19,8 +19,6 @@ from sentry_sdk.tracing import TransactionSource from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, capture_internal_exceptions, ensure_integration_enabled, event_from_exception, @@ -80,14 +78,6 @@ def setup_once() -> None: SanicIntegration.version = parse_version(SANIC_VERSION) _check_minimum_version(SanicIntegration, SanicIntegration.version) - if not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - raise DidNotEnable( - "The sanic integration for Sentry requires Python 3.7+ " - " or the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE - ) - if SANIC_VERSION.startswith("0.8."): # Sanic 0.8 and older creates a logger named "root" and puts a # stringified version of every exception in there (without exc_info), diff --git a/sentry_sdk/integrations/starlite.py b/sentry_sdk/integrations/starlite.py index 6ea93cce99..52ad51d464 100644 --- a/sentry_sdk/integrations/starlite.py +++ b/sentry_sdk/integrations/starlite.py @@ -75,7 +75,6 @@ def __init__( ) -> None: super().__init__( app=app, - unsafe_context_data=False, transaction_style="endpoint", mechanism_type="asgi", span_origin=span_origin, diff --git a/sentry_sdk/integrations/tornado.py b/sentry_sdk/integrations/tornado.py index e71bf94d12..96c2298139 100644 --- a/sentry_sdk/integrations/tornado.py +++ b/sentry_sdk/integrations/tornado.py @@ -19,8 +19,6 @@ from sentry_sdk.tracing import TransactionSource from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, AnnotatedValue, capture_internal_exceptions, ensure_integration_enabled, @@ -54,14 +52,6 @@ class TornadoIntegration(Integration): def setup_once() -> None: _check_minimum_version(TornadoIntegration, TORNADO_VERSION) - if not HAS_REAL_CONTEXTVARS: - # Tornado is async. We better have contextvars or we're going to leak - # state between requests. - raise DidNotEnable( - "The tornado integration for Sentry requires Python 3.7+ or the aiocontextvars package" - + CONTEXTVARS_ERROR_MESSAGE - ) - ignore_logger("tornado.access") old_execute = RequestHandler._execute diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index e0f9f40060..0001ff2591 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -1,5 +1,6 @@ import sys from contextlib import nullcontext +from contextvars import ContextVar from functools import partial from typing import TYPE_CHECKING @@ -18,7 +19,6 @@ from sentry_sdk.tracing import Span, TransactionSource from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import ( - ContextVar, capture_internal_exceptions, event_from_exception, has_data_collection_enabled, @@ -56,7 +56,9 @@ def __call__( pass -_wsgi_middleware_applied = ContextVar("sentry_wsgi_middleware_applied") +_wsgi_middleware_applied: "ContextVar[bool]" = ContextVar( + "sentry_wsgi_middleware_applied" +) _DEFAULT_TRANSACTION_NAME = "generic WSGI request" diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 4ac12e9310..2e3343440d 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -4,6 +4,7 @@ import warnings from collections import deque from contextlib import contextmanager +from contextvars import ContextVar from copy import copy, deepcopy from datetime import datetime, timezone from enum import Enum @@ -47,7 +48,6 @@ is_ignored_span, ) from sentry_sdk.utils import ( - ContextVar, capture_internal_exception, capture_internal_exceptions, datetime_from_isoformat, @@ -116,11 +116,15 @@ # This is used to isolate data for different requests or users. # The isolation scope is usually created by integrations, but may also # be created manually -_isolation_scope = ContextVar("isolation_scope", default=None) +_isolation_scope: "ContextVar[Optional[Scope]]" = ContextVar( + "isolation_scope", default=None +) # Holds data for the active span. # This can be used to manually add additional data to a span. -_current_scope = ContextVar("current_scope", default=None) +_current_scope: "ContextVar[Optional[Scope]]" = ContextVar( + "current_scope", default=None +) global_event_processors: "List[EventProcessor]" = [] @@ -462,8 +466,9 @@ def get_client(cls) -> "sentry_sdk.client.BaseClient": If no client is available a :py:class:`sentry_sdk.client.NonRecordingClient` is returned. """ current_scope = _current_scope.get() + try: - client = current_scope.client + client = current_scope.client # type: ignore[union-attr] except AttributeError: client = None @@ -472,7 +477,7 @@ def get_client(cls) -> "sentry_sdk.client.BaseClient": isolation_scope = _isolation_scope.get() try: - client = isolation_scope.client + client = isolation_scope.client # type: ignore[union-attr] except AttributeError: client = None diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 8ceca87d94..1a7b45278f 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -5,7 +5,6 @@ import logging import math import os -import random import re import subprocess import sys @@ -13,6 +12,7 @@ import time from collections import namedtuple from contextlib import contextmanager +from contextvars import ContextVar from datetime import datetime, timezone from decimal import Decimal from functools import partial, partialmethod, wraps @@ -1318,133 +1318,6 @@ def parse_version(version: str) -> "Optional[Tuple[int, ...]]": return release_tuple -def _is_contextvars_broken() -> bool: - """ - Returns whether gevent/eventlet have patched the stdlib in a way where thread locals are now more "correct" than contextvars. - """ - try: - import gevent - from gevent.monkey import is_object_patched - - # Get the MAJOR and MINOR version numbers of Gevent - version_tuple = tuple( - [int(part) for part in re.split(r"a|b|rc|\.", gevent.__version__)[:2]] - ) - if is_object_patched("threading", "local"): - # Gevent 20.9.0 depends on Greenlet 0.4.17 which natively handles switching - # context vars when greenlets are switched, so, Gevent 20.9.0+ is all fine. - # Ref: https://github.com/gevent/gevent/blob/83c9e2ae5b0834b8f84233760aabe82c3ba065b4/src/gevent/monkey.py#L604-L609 - # Gevent 20.5, that doesn't depend on Greenlet 0.4.17 with native support - # for contextvars, is able to patch both thread locals and contextvars, in - # that case, check if contextvars are effectively patched. - if ( - # Gevent 20.9.0+ - (sys.version_info >= (3, 7) and version_tuple >= (20, 9)) - # Gevent 20.5.0+ or Python < 3.7 - or (is_object_patched("contextvars", "ContextVar")) - ): - return False - - return True - except ImportError: - pass - - try: - import greenlet - from eventlet.patcher import is_monkey_patched # type: ignore - - greenlet_version = parse_version(greenlet.__version__) - - if greenlet_version is None: - logger.error( - "Internal error in Sentry SDK: Could not parse Greenlet version from greenlet.__version__." - ) - return False - - if is_monkey_patched("thread") and greenlet_version < (0, 5): - return True - except ImportError: - pass - - return False - - -def _make_threadlocal_contextvars(local: type) -> type: - class ContextVar: - # Super-limited impl of ContextVar - - def __init__(self, name: str, default: "Any" = None) -> None: - self._name = name - self._default = default - self._local = local() - self._original_local = local() - - def get(self, default: "Any" = None) -> "Any": - return getattr(self._local, "value", default or self._default) - - def set(self, value: "Any") -> "Any": - token = str(random.getrandbits(64)) - original_value = self.get() - setattr(self._original_local, token, original_value) - self._local.value = value - return token - - def reset(self, token: "Any") -> None: - self._local.value = getattr(self._original_local, token) - # delete the original value (this way it works in Python 3.6+) - del self._original_local.__dict__[token] - - return ContextVar - - -def _get_contextvars() -> "Tuple[bool, type]": - """ - Figure out the "right" contextvars installation to use. Returns a - `contextvars.ContextVar`-like class with a limited API. - - See https://docs.sentry.io/platforms/python/contextvars/ for more information. - """ - if not _is_contextvars_broken(): - # aiocontextvars is a PyPI package that ensures that the contextvars - # backport (also a PyPI package) works with asyncio under Python 3.6 - # - # Import it if available. - if sys.version_info < (3, 7): - # `aiocontextvars` is absolutely required for functional - # contextvars on Python 3.6. - try: - from aiocontextvars import ContextVar - - return True, ContextVar - except ImportError: - pass - else: - # On Python 3.7 contextvars are functional. - try: - from contextvars import ContextVar - - return True, ContextVar - except ImportError: - pass - - # Fall back to basic thread-local usage. - - from threading import local - - return False, _make_threadlocal_contextvars(local) - - -HAS_REAL_CONTEXTVARS, ContextVar = _get_contextvars() - -CONTEXTVARS_ERROR_MESSAGE = """ - -With asyncio/ASGI applications, the Sentry SDK requires a functional -installation of `contextvars` to avoid leaking scope/context data across -requests. - -Please refer to https://docs.sentry.io/platforms/python/contextvars/ for more information. -""" - _is_sentry_internal_task = ContextVar("is_sentry_internal_task", default=False) # These exceptions won't set the span status to error if they occur. Use @@ -1502,7 +1375,7 @@ def transaction_from_function(func: "Callable[..., Any]") -> "Optional[str]": return qualname_from_function(func) -disable_capture_event = ContextVar("disable_capture_event") +disable_capture_event: "ContextVar[bool]" = ContextVar("disable_capture_event") class ServerlessTimeoutWarning(Exception): # noqa: N818 diff --git a/tests/utils/test_contextvars.py b/tests/utils/test_contextvars.py deleted file mode 100644 index 50881314c1..0000000000 --- a/tests/utils/test_contextvars.py +++ /dev/null @@ -1,52 +0,0 @@ -import random -import time -from unittest import mock - -import pytest - - -def _run_contextvar_threaded_test(): - import threading - - # Need to explicitly call _get_contextvars because the SDK has already - # decided upon gevent on import. - from sentry_sdk import utils - - _, ContextVar = utils._get_contextvars() # noqa: N806 - - ts = [] - - var = ContextVar("test_contextvar_leaks") - - success = [] - - def run(): - value = int(random.random() * 1000) - var.set(value) - - for _ in range(100): - time.sleep(0) - assert var.get(None) == value - - success.append(1) - - for _ in range(20): - t = threading.Thread(target=run) - t.start() - ts.append(t) - - for t in ts: - t.join() - - assert len(success) == 20 - - -@pytest.mark.forked -def test_leaks(maybe_monkeypatched_threading): - _run_contextvar_threaded_test() - - -@pytest.mark.forked -@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()