From 52abb610dbdf1751cc745720055119ca1656ec16 Mon Sep 17 00:00:00 2001 From: Qiying Wang <781345688@qq.com> Date: Fri, 3 May 2024 01:28:41 +0800 Subject: [PATCH] Remove thread lock by loading RuntimeContext explicitly. (#3763) --- CHANGELOG.md | 2 + .../src/opentelemetry/context/__init__.py | 91 +++++++++---------- .../tests/context/test_context.py | 19 ++++ 3 files changed, 62 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23077ff4b64..dd9b0b19edb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Remove thread lock by loading RuntimeContext explicitly. + ([#3763](https://github.com/open-telemetry/opentelemetry-python/pull/3763)) - Update proto version to v1.2.0 ([#3844](https://github.com/open-telemetry/opentelemetry-python/pull/3844)) - Add to_json method to ExponentialHistogram diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 91133884f68..0a2785ab607 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -13,9 +13,7 @@ # limitations under the License. import logging -import threading import typing -from functools import wraps from os import environ from uuid import uuid4 @@ -25,54 +23,50 @@ from opentelemetry.util._importlib_metadata import entry_points logger = logging.getLogger(__name__) -_RUNTIME_CONTEXT = None # type: typing.Optional[_RuntimeContext] -_RUNTIME_CONTEXT_LOCK = threading.Lock() -_F = typing.TypeVar("_F", bound=typing.Callable[..., typing.Any]) - -def _load_runtime_context(func: _F) -> _F: - """A decorator used to initialize the global RuntimeContext +def _load_runtime_context() -> _RuntimeContext: + """Initialize the RuntimeContext Returns: - A wrapper of the decorated method. + An instance of RuntimeContext. """ - @wraps(func) # type: ignore[misc] - def wrapper( - *args: typing.Tuple[typing.Any, typing.Any], - **kwargs: typing.Dict[typing.Any, typing.Any], - ) -> typing.Optional[typing.Any]: - global _RUNTIME_CONTEXT # pylint: disable=global-statement - - with _RUNTIME_CONTEXT_LOCK: - if _RUNTIME_CONTEXT is None: - # FIXME use a better implementation of a configuration manager - # to avoid having to get configuration values straight from - # environment variables - default_context = "contextvars_context" - - configured_context = environ.get( - OTEL_PYTHON_CONTEXT, default_context - ) # type: str - try: - - _RUNTIME_CONTEXT = next( # type: ignore - iter( # type: ignore - entry_points( # type: ignore - group="opentelemetry_context", - name=configured_context, - ) - ) - ).load()() - - except Exception: # pylint: disable=broad-except - logger.exception( - "Failed to load context: %s", configured_context - ) - return func(*args, **kwargs) # type: ignore[misc] - - return typing.cast(_F, wrapper) # type: ignore[misc] + # FIXME use a better implementation of a configuration manager + # to avoid having to get configuration values straight from + # environment variables + default_context = "contextvars_context" + + configured_context = environ.get( + OTEL_PYTHON_CONTEXT, default_context + ) # type: str + + try: + return next( # type: ignore + iter( # type: ignore + entry_points( # type: ignore + group="opentelemetry_context", + name=configured_context, + ) + ) + ).load()() + except Exception: # pylint: disable=broad-except + logger.exception( + "Failed to load context: %s, fallback to %s", + configured_context, + default_context, + ) + return next( # type: ignore + iter( # type: ignore + entry_points( # type: ignore + group="opentelemetry_context", + name=default_context, + ) + ) + ).load()() + + +_RUNTIME_CONTEXT = _load_runtime_context() def create_key(keyname: str) -> str: @@ -125,7 +119,6 @@ def set_value( return Context(new_values) -@_load_runtime_context # type: ignore def get_current() -> Context: """To access the context associated with program execution, the Context API provides a function which takes no arguments @@ -134,10 +127,9 @@ def get_current() -> Context: Returns: The current `Context` object. """ - return _RUNTIME_CONTEXT.get_current() # type:ignore + return _RUNTIME_CONTEXT.get_current() -@_load_runtime_context # type: ignore def attach(context: Context) -> object: """Associates a Context with the caller's current execution unit. Returns a token that can be used to restore the previous Context. @@ -148,10 +140,9 @@ def attach(context: Context) -> object: Returns: A token that can be used with `detach` to reset the context. """ - return _RUNTIME_CONTEXT.attach(context) # type:ignore + return _RUNTIME_CONTEXT.attach(context) -@_load_runtime_context # type: ignore def detach(token: object) -> None: """Resets the Context associated with the caller's current execution unit to the value it had before attaching a specified Context. @@ -160,7 +151,7 @@ def detach(token: object) -> None: token: The Token that was returned by a previous call to attach a Context. """ try: - _RUNTIME_CONTEXT.detach(token) # type: ignore + _RUNTIME_CONTEXT.detach(token) except Exception: # pylint: disable=broad-except logger.exception("Failed to detach context") diff --git a/opentelemetry-api/tests/context/test_context.py b/opentelemetry-api/tests/context/test_context.py index b90c011b992..18f6f68a514 100644 --- a/opentelemetry-api/tests/context/test_context.py +++ b/opentelemetry-api/tests/context/test_context.py @@ -13,9 +13,12 @@ # limitations under the License. import unittest +from unittest.mock import patch from opentelemetry import context from opentelemetry.context.context import Context +from opentelemetry.context.contextvars_context import ContextVarsRuntimeContext +from opentelemetry.environment_variables import OTEL_PYTHON_CONTEXT def _do_work() -> str: @@ -74,3 +77,19 @@ def test_set_current(self): context.detach(token) self.assertEqual("yyy", context.get_value("a")) + + +class TestInitContext(unittest.TestCase): + def test_load_runtime_context_default(self): + ctx = context._load_runtime_context() # pylint: disable=W0212 + self.assertIsInstance(ctx, ContextVarsRuntimeContext) + + @patch.dict("os.environ", {OTEL_PYTHON_CONTEXT: "contextvars_context"}) + def test_load_runtime_context(self): # type: ignore[misc] + ctx = context._load_runtime_context() # pylint: disable=W0212 + self.assertIsInstance(ctx, ContextVarsRuntimeContext) + + @patch.dict("os.environ", {OTEL_PYTHON_CONTEXT: "foo"}) + def test_load_runtime_context_fallback(self): # type: ignore[misc] + ctx = context._load_runtime_context() # pylint: disable=W0212 + self.assertIsInstance(ctx, ContextVarsRuntimeContext)