From b0210f5ffe84254758bc05d5c944e3687c06b11e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Suliga?= Date: Sun, 1 Dec 2024 20:07:43 +0100 Subject: [PATCH] Add deadlock detection for ValueClass-es MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Przemysław Suliga --- prometheus_client/values.py | 31 ++++++++++++++++++++++++++----- tests/test_core.py | 11 +++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/prometheus_client/values.py b/prometheus_client/values.py index 6ff85e3b..f3f59986 100644 --- a/prometheus_client/values.py +++ b/prometheus_client/values.py @@ -1,10 +1,32 @@ import os -from threading import Lock +from threading import Lock, RLock import warnings from .mmap_dict import mmap_key, MmapedDict +class _WarningRLock: + """A wrapper around threading.RLock that detects possible deadlocks. + + Raises a RuntimeError when it detects attempts to re-enter the critical + section from a single thread. + """ + + def __init__(self): + self._rlock = RLock() + self._lock = Lock() + + def __enter__(self): + self._rlock.acquire() + if not self._lock.acquire(blocking=False): + self._rlock.release() + raise RuntimeError('Attempt to enter a non reentrant context from a single thread.') + + def __exit__(self, exc_type, exc_value, traceback): + self._lock.release() + self._rlock.release() + + class MutexValue: """A float protected by a mutex.""" @@ -13,7 +35,7 @@ class MutexValue: def __init__(self, typ, metric_name, name, labelnames, labelvalues, help_text, **kwargs): self._value = 0.0 self._exemplar = None - self._lock = Lock() + self._lock = _WarningRLock() def inc(self, amount): with self._lock: @@ -47,10 +69,9 @@ def MultiProcessValue(process_identifier=os.getpid): files = {} values = [] pid = {'value': process_identifier()} - # Use a single global lock when in multi-processing mode - # as we presume this means there is no threading going on. + # Use a single global lock when in multi-processing mode. # This avoids the need to also have mutexes in __MmapDict. - lock = Lock() + lock = _WarningRLock() class MmapedValue: """A float protected by a mutex backed by a per-process mmaped file.""" diff --git a/tests/test_core.py b/tests/test_core.py index 056d8e58..0920049f 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -135,6 +135,17 @@ def test_exemplar_too_long(self): 'y123456': '7+15 characters', }) + def test_single_thread_deadlock_detection(self): + counter = self.counter + + class Tracked(float): + def __radd__(self, other): + counter.inc(10) + return self + other + + expected_msg = 'Attempt to enter a non reentrant context from a single thread.' + self.assertRaisesRegex(RuntimeError, expected_msg, counter.inc, Tracked(100)) + class TestDisableCreated(unittest.TestCase): def setUp(self):