Skip to content

Commit bede81b

Browse files
committed
Fix race condition and silent failure in bounded prometheus series tracker
- Hold tracker lock around metric.labels() and track_series() so a concurrent eviction cannot remove the just-created Prometheus child before the caller observes/increments it. - Only commit tracker state changes (popitem / del) when the underlying metric.remove() call actually succeeds (or the child was already gone), so silent removal failures cannot let cardinality drift above max_series.
1 parent 0bfa9b7 commit bede81b

2 files changed

Lines changed: 31 additions & 15 deletions

File tree

litellm/integrations/prometheus.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -994,9 +994,10 @@ def _get_labeled_metric(
994994
metric_name: DEFINED_PROMETHEUS_METRICS,
995995
labels: Dict[str, Optional[str]],
996996
) -> Any:
997-
labeled_metric = metric.labels(**labels)
998-
self._track_bounded_prometheus_metric_series(metric, metric_name, labels)
999-
return labeled_metric
997+
with self._bounded_prometheus_series_tracker.lock:
998+
labeled_metric = metric.labels(**labels)
999+
self._track_bounded_prometheus_metric_series(metric, metric_name, labels)
1000+
return labeled_metric
10001001

10011002
def _track_bounded_prometheus_metric_series(
10021003
self,

litellm/integrations/prometheus_helpers/bounded_prometheus_series_tracker.py

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class BoundedPrometheusSeriesTracker:
1717
def __init__(self) -> None:
1818
self._series: Dict[str, OrderedDict[tuple[Optional[str], ...], float]] = {}
1919
self._last_ttl_cleanup: Dict[str, float] = {}
20-
self._lock = RLock()
20+
self.lock = RLock()
2121

2222
def track_series(
2323
self,
@@ -33,7 +33,7 @@ def track_series(
3333

3434
now = time.monotonic()
3535

36-
with self._lock:
36+
with self.lock:
3737
series = self._series.setdefault(metric_name, OrderedDict())
3838
series[label_values] = now
3939
series.move_to_end(label_values)
@@ -53,12 +53,16 @@ def track_series(
5353

5454
if max_series is not None and max_series > 0:
5555
while len(series) > max_series:
56-
tracked_label_values, _ = series.popitem(last=False)
57-
self._remove_metric_child(metric, tracked_label_values)
56+
tracked_label_values = next(iter(series))
57+
if not self._remove_metric_child(metric, tracked_label_values):
58+
break
59+
del series[tracked_label_values]
5860
elif max_series is not None:
5961
while series:
60-
tracked_label_values, _ = series.popitem(last=False)
61-
self._remove_metric_child(metric, tracked_label_values)
62+
tracked_label_values = next(iter(series))
63+
if not self._remove_metric_child(metric, tracked_label_values):
64+
break
65+
del series[tracked_label_values]
6266

6367
def _should_run_ttl_cleanup(
6468
self,
@@ -82,15 +86,26 @@ def _remove_metric_series(
8286
series: OrderedDict[tuple[Optional[str], ...], float],
8387
label_values: tuple[Optional[str], ...],
8488
) -> None:
85-
if label_values in series:
86-
del series[label_values]
87-
self._remove_metric_child(metric, label_values)
89+
if self._remove_metric_child(metric, label_values):
90+
series.pop(label_values, None)
8891

8992
@staticmethod
9093
def _remove_metric_child(
9194
metric: Any, label_values: tuple[Optional[str], ...]
92-
) -> None:
95+
) -> bool:
96+
"""
97+
Remove the Prometheus child for ``label_values`` and report whether the
98+
tracker should commit the matching state change.
99+
100+
Returns ``True`` when the child is no longer present in Prometheus
101+
(either it was just removed or it was already gone), and ``False`` when
102+
``metric.remove()`` raised an unexpected error and the child likely
103+
still exists.
104+
"""
93105
try:
94106
metric.remove(*label_values)
95-
except (AttributeError, KeyError, ValueError):
96-
pass
107+
return True
108+
except KeyError:
109+
return True
110+
except (AttributeError, ValueError):
111+
return False

0 commit comments

Comments
 (0)