Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove thread lock by loading RuntimeContext explicitly. #3763

Merged
merged 12 commits into from
May 2, 2024
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
91 changes: 41 additions & 50 deletions opentelemetry-api/src/opentelemetry/context/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
WqyJh marked this conversation as resolved.
Show resolved Hide resolved
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:
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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")

Expand Down
19 changes: 19 additions & 0 deletions opentelemetry-api/tests/context/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"})
WqyJh marked this conversation as resolved.
Show resolved Hide resolved
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)