|
4 | 4 | # type: ignore |
5 | 5 |
|
6 | 6 | import copy |
| 7 | +import logging |
| 8 | +import threading |
7 | 9 | import unittest |
8 | 10 | import unittest.mock |
9 | 11 | from collections.abc import MutableSequence |
@@ -269,19 +271,47 @@ def test_immutable(self): |
269 | 271 | with self.assertRaises(TypeError): |
270 | 272 | bdict["should-not-work"] = "dict immutable" |
271 | 273 |
|
272 | | - def test_locking(self): |
273 | | - """Supporting test case for a commit titled: Fix class BoundedAttributes to have RLock rather than Lock. See #3858. |
274 | | - The change was introduced because __iter__ of the class BoundedAttributes holds lock, and we observed some deadlock symptoms |
275 | | - in the codebase. This test case is to verify that the fix works as expected. |
| 274 | + def test_no_deadlock_on_reentrant_logging(self): |
| 275 | + """Regression test for #3858. |
| 276 | +
|
| 277 | + The deadlock scenario: a logging handler intercepts the warning |
| 278 | + emitted by _clean_attribute for an invalid value and calls __setitem__ |
| 279 | + on the same BoundedAttributes instance from the same thread. |
| 280 | + With _clean_attribute called inside the lock this caused a deadlock. |
| 281 | + With _clean_attribute called before the lock is acquired, no deadlock |
| 282 | + occurs. |
276 | 283 | """ |
277 | 284 | bdict = BoundedAttributes(immutable=False) |
278 | 285 |
|
279 | | - with bdict._lock: # pylint: disable=protected-access |
280 | | - for num in range(100): |
281 | | - bdict[str(num)] = num |
282 | | - |
283 | | - for num in range(100): |
284 | | - self.assertEqual(bdict[str(num)], num) |
| 286 | + class ReentrantHandler(logging.Handler): |
| 287 | + def emit(self, _record): |
| 288 | + # Simulates Sentry intercepting the OTel warning and writing |
| 289 | + # back into the same BoundedAttributes on the same thread. |
| 290 | + bdict["reentrant.key"] = "set_by_handler" |
| 291 | + |
| 292 | + otel_logger = logging.getLogger("opentelemetry.attributes") |
| 293 | + handler = ReentrantHandler() |
| 294 | + otel_logger.addHandler(handler) |
| 295 | + try: |
| 296 | + completed = threading.Event() |
| 297 | + |
| 298 | + def run(): |
| 299 | + # None is an invalid attribute value and triggers _logger.warning |
| 300 | + # in _clean_attribute, which fires the ReentrantHandler above. |
| 301 | + bdict["trigger.key"] = None |
| 302 | + completed.set() |
| 303 | + |
| 304 | + thread = threading.Thread(target=run, daemon=True) |
| 305 | + thread.start() |
| 306 | + thread.join(timeout=2.0) |
| 307 | + |
| 308 | + self.assertTrue( |
| 309 | + completed.is_set(), |
| 310 | + "Deadlock detected: __setitem__ did not complete within 2s", |
| 311 | + ) |
| 312 | + self.assertEqual(bdict.get("reentrant.key"), "set_by_handler") |
| 313 | + finally: |
| 314 | + otel_logger.removeHandler(handler) |
285 | 315 |
|
286 | 316 | # pylint: disable=no-self-use |
287 | 317 | def test_extended_attributes(self): |
|
0 commit comments