Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changelog/5329.changed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
opentelemetry-sdk: revert RLock back to Lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 44 additions & 0 deletions opentelemetry-api/tests/attributes/test_attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
# type: ignore

import copy
import logging
import threading
import unittest
import unittest.mock
from collections.abc import MutableSequence
Expand Down Expand Up @@ -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)
Expand Down
Loading