Skip to content

Commit dc3fc54

Browse files
skulitomclaude
andauthored
Avoid parent lock for existing metric labelsets (#1216)
* Avoid parent lock for existing metric labelsets Signed-off-by: skulitom <artemskulimovskiy@gmail.com> * Comment the lock-free label lookup Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: skulitom <artemskulimovskiy@gmail.com> --------- Signed-off-by: skulitom <artemskulimovskiy@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 2aac0e0 commit dc3fc54

2 files changed

Lines changed: 60 additions & 0 deletions

File tree

‎prometheus_client/metrics.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,13 @@ def labels(self: T, *labelvalues: object, **labelkwargs: object) -> T:
178178
if len(labelvalues) != len(self._labelnames):
179179
raise ValueError('Incorrect label count')
180180
str_labelvalues = tuple(str(l) for l in labelvalues)
181+
182+
# Lock-free fast path for existing children; a miss re-checks under the lock.
183+
try:
184+
return self._metrics[str_labelvalues]
185+
except KeyError:
186+
pass
187+
181188
with self._lock:
182189
if str_labelvalues not in self._metrics:
183190

‎tests/test_core.py‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from concurrent.futures import ThreadPoolExecutor
22
import os
3+
from threading import Barrier, Event
34
import time
45
import unittest
56

@@ -621,6 +622,58 @@ def test_child(self):
621622
self.two_labels.labels('x', 'y').inc(2)
622623
self.assertEqual(2, self.registry.get_sample_value('two_total', {'a': 'x', 'b': 'y'}))
623624

625+
def test_existing_child_during_other_child_creation(self):
626+
started = Event()
627+
release = Event()
628+
629+
class SlowCounter(Counter):
630+
def _metric_init(self):
631+
if self._labelvalues == ('slow',):
632+
started.set()
633+
if not release.wait(timeout=10):
634+
raise RuntimeError('Child creation was not released')
635+
super()._metric_init()
636+
637+
counter = SlowCounter('slow', 'help', ['l'], registry=self.registry)
638+
existing = counter.labels('existing')
639+
with ThreadPoolExecutor(max_workers=2) as pool:
640+
creating = pool.submit(counter.labels, 'slow')
641+
try:
642+
self.assertTrue(started.wait(timeout=5))
643+
lookup = pool.submit(counter.labels, 'existing')
644+
self.assertIs(existing, lookup.result(timeout=5))
645+
finally:
646+
release.set()
647+
self.assertIs(creating.result(timeout=5), counter.labels('slow'))
648+
649+
def test_concurrent_child_creation_and_updates(self):
650+
barrier = Barrier(8)
651+
652+
def increment():
653+
barrier.wait(timeout=5)
654+
child = self.counter.labels('shared')
655+
for _ in range(100):
656+
self.counter.labels(l='shared').inc()
657+
return child
658+
659+
with ThreadPoolExecutor(max_workers=8) as pool:
660+
futures = [pool.submit(increment) for _ in range(8)]
661+
children = [future.result(timeout=5) for future in futures]
662+
663+
self.assertTrue(all(child is children[0] for child in children))
664+
self.assertEqual(800, self.registry.get_sample_value('c_total', {'l': 'shared'}))
665+
666+
def test_recreate_child_after_removal(self):
667+
for remove in (lambda: self.counter.remove('x'), self.counter.clear,
668+
lambda: self.counter.remove_by_labels({'l': 'x'})):
669+
original = self.counter.labels('x')
670+
original.inc()
671+
remove()
672+
replacement = self.counter.labels('x')
673+
self.assertIsNot(original, replacement)
674+
self.assertIs(replacement, self.counter.labels(l='x'))
675+
self.assertEqual(0, self.registry.get_sample_value('c_total', {'l': 'x'}))
676+
624677
def test_remove(self):
625678
self.counter.labels('x').inc()
626679
self.counter.labels('y').inc(2)

0 commit comments

Comments
 (0)