From f1b71690c260e9235375cbd40a7a15df1f57ca3d Mon Sep 17 00:00:00 2001 From: Alex Boten <223565+codeboten@users.noreply.github.com> Date: Thu, 18 Jun 2026 13:28:15 -0700 Subject: [PATCH 1/2] opentelemetry-sdk: revert RLock back to Lock With the changes in clean_attributes, the original issue that required the reentrant lock in the first place is no longer an issue. Added a test that was run with the old code to validate that the regression does not occur. Signed-off-by: Alex Boten <223565+codeboten@users.noreply.github.com> --- .../src/opentelemetry/attributes/__init__.py | 2 +- .../tests/attributes/test_attributes.py | 44 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/opentelemetry-api/src/opentelemetry/attributes/__init__.py b/opentelemetry-api/src/opentelemetry/attributes/__init__.py index 15519ce0c2c..9c1b83b91e6 100644 --- a/opentelemetry-api/src/opentelemetry/attributes/__init__.py +++ b/opentelemetry-api/src/opentelemetry/attributes/__init__.py @@ -262,7 +262,7 @@ def __init__( MutableMapping[str, types.AnyValue] | OrderedDict[str, types.AnyValue] ) = {} - self._lock = threading.RLock() + self._lock = threading.Lock() if attributes: for key, value in attributes.items(): self[key] = value diff --git a/opentelemetry-api/tests/attributes/test_attributes.py b/opentelemetry-api/tests/attributes/test_attributes.py index fb6259f8c47..6313b69386e 100644 --- a/opentelemetry-api/tests/attributes/test_attributes.py +++ b/opentelemetry-api/tests/attributes/test_attributes.py @@ -4,6 +4,8 @@ # type: ignore import copy +import logging +import threading import unittest import unittest.mock from collections.abc import MutableSequence @@ -283,6 +285,48 @@ def test_locking(self): for num in range(100): self.assertEqual(bdict[str(num)], num) + def test_no_deadlock_on_reentrant_logging(self): + """Regression test for #3858. + + The deadlock scenario: a logging handler intercepts the warning + emitted by _clean_attribute for an invalid value and calls __setitem__ + on the same BoundedAttributes instance from the same thread. + With _clean_attribute called inside the lock this caused a deadlock. + With _clean_attribute called before the lock is acquired, no deadlock + occurs. + """ + bdict = BoundedAttributes(immutable=False) + + class ReentrantHandler(logging.Handler): + def emit(self, _record): + # Simulates Sentry intercepting the OTel warning and writing + # back into the same BoundedAttributes on the same thread. + bdict["reentrant.key"] = "set_by_handler" + + otel_logger = logging.getLogger("opentelemetry.attributes") + handler = ReentrantHandler() + otel_logger.addHandler(handler) + try: + completed = threading.Event() + + def run(): + # None is an invalid attribute value and triggers _logger.warning + # in _clean_attribute, which fires the ReentrantHandler above. + bdict["trigger.key"] = None + completed.set() + + t = threading.Thread(target=run, daemon=True) + t.start() + t.join(timeout=2.0) + + self.assertTrue( + completed.is_set(), + "Deadlock detected: __setitem__ did not complete within 2s", + ) + self.assertEqual(bdict.get("reentrant.key"), "set_by_handler") + finally: + otel_logger.removeHandler(handler) + # pylint: disable=no-self-use def test_extended_attributes(self): bdict = BoundedAttributes(extended_attributes=True, immutable=False) From 8f7289bff918cbcef7ffceacc14730c23f59580b Mon Sep 17 00:00:00 2001 From: Alex Boten <223565+codeboten@users.noreply.github.com> Date: Thu, 18 Jun 2026 13:34:36 -0700 Subject: [PATCH 2/2] changelog Signed-off-by: Alex Boten <223565+codeboten@users.noreply.github.com> --- .changelog/5329.changed | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changelog/5329.changed diff --git a/.changelog/5329.changed b/.changelog/5329.changed new file mode 100644 index 00000000000..545518c6f3e --- /dev/null +++ b/.changelog/5329.changed @@ -0,0 +1 @@ +opentelemetry-sdk: revert RLock back to Lock \ No newline at end of file