Skip to content

Commit a19124d

Browse files
committed
Add deadlock detection for ValueClass-es
Signed-off-by: Przemysław Suliga <[email protected]>
1 parent eb874cf commit a19124d

File tree

2 files changed

+36
-5
lines changed

2 files changed

+36
-5
lines changed

prometheus_client/values.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,31 @@
11
import os
2-
from threading import Lock
2+
from threading import Lock, RLock
33
import warnings
44

55
from .mmap_dict import mmap_key, MmapedDict
66

77

8+
class _WarningRLock:
9+
"""A wrapper around threading.RLock that detects possible deadlocks.
10+
11+
Raises a RuntimeError when it detects attempts to re-enter the critical
12+
section from a single thread.
13+
"""
14+
15+
def __init__(self):
16+
self._rlock = RLock()
17+
self._lock = Lock()
18+
19+
def __enter__(self):
20+
self._rlock.acquire()
21+
if not self._lock.acquire(blocking=False):
22+
raise RuntimeError('Attempt to enter a non reentrant context from a single thread.')
23+
24+
def __exit__(self, exc_type, exc_value, traceback):
25+
self._lock.release()
26+
self._rlock.release()
27+
28+
829
class MutexValue:
930
"""A float protected by a mutex."""
1031

@@ -13,7 +34,7 @@ class MutexValue:
1334
def __init__(self, typ, metric_name, name, labelnames, labelvalues, help_text, **kwargs):
1435
self._value = 0.0
1536
self._exemplar = None
16-
self._lock = Lock()
37+
self._lock = _WarningRLock()
1738

1839
def inc(self, amount):
1940
with self._lock:
@@ -47,10 +68,9 @@ def MultiProcessValue(process_identifier=os.getpid):
4768
files = {}
4869
values = []
4970
pid = {'value': process_identifier()}
50-
# Use a single global lock when in multi-processing mode
51-
# as we presume this means there is no threading going on.
71+
# Use a single global lock when in multi-processing mode.
5272
# This avoids the need to also have mutexes in __MmapDict.
53-
lock = Lock()
73+
lock = _WarningRLock()
5474

5575
class MmapedValue:
5676
"""A float protected by a mutex backed by a per-process mmaped file."""

tests/test_core.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,17 @@ def test_exemplar_too_long(self):
135135
'y123456': '7+15 characters',
136136
})
137137

138+
def test_single_thread_deadlock_detection(self):
139+
counter = self.counter
140+
141+
class Tracked(float):
142+
def __radd__(self, other):
143+
counter.inc(10)
144+
return self + other
145+
146+
expected_msg = 'Attempt to enter a non reentrant context from a single thread.'
147+
self.assertRaisesRegex(RuntimeError, expected_msg, counter.inc, Tracked(100))
148+
138149

139150
class TestDisableCreated(unittest.TestCase):
140151
def setUp(self):

0 commit comments

Comments
 (0)