Skip to content

Commit

Permalink
Remove thread lock by loading RuntimeContext explicitly. (#3763)
Browse files Browse the repository at this point in the history
  • Loading branch information
WqyJh authored May 2, 2024
1 parent d285b7f commit 52abb61
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 50 deletions.
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
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"})
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)

0 comments on commit 52abb61

Please sign in to comment.