From 8b36740bad82d421b4e93f2b78611eb6958c36e9 Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Fri, 16 Jan 2026 20:35:30 +0000 Subject: [PATCH] Fix `InFlightGauge` typing to allow upgrading to `prometheus_client` 0.24 (#19379) Fixes #19375 `prometheus_client` 0.24 makes `Collector` a generic type. Previously, `InFlightGauge` inherited from both `Generic[MetricsEntry]` and `Collector`, resulting in the error `TypeError: cannot create a consistent MRO` when using `prometheus_client` >= 0.24. This behaviour of disallowing multiple `Generic` inheritance is more strictly enforced starting with python 3.14, but can still lead to issues with earlier versions of python. This PR separates runtime and typing inheritance for `InFlightGauge`: - Runtime: `InFlightGauge` inherits only from `Collector` - Typing: `InFlightGauge` is generic This preserves static typing, avoids MRO conflicts, and supports both `prometheus_client` <0.24 and >=0.24. I have tested these changes out locally with `prometheus_client` 0.23.1 & 0.24 on python 3.14 while sending a bunch of messages over federation and watching a grafana dashboard configured to show `synapse_util_metrics_block_in_flight_total` & `synapse_util_metrics_block_in_flight_real_time_sum` (the only metric setup to use `InFlightGauge`) and things are working in each case. https://github.com/element-hq/synapse/blob/a1e9abc7df3e6c43a95cba059348546a4c9d4491/synapse/util/metrics.py#L112-L119 ### Pull Request Checklist * [X] Pull request is based on the develop branch * [X] Pull request includes a [changelog file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog). The entry should: - Be a short description of your change which makes sense to users. "Fixed a bug that prevented receiving messages from other servers." instead of "Moved X method from `EventStore` to `EventWorkerStore`.". - Use markdown where necessary, mostly for `code blocks`. - End with either a period (.) or an exclamation mark (!). - Start with a capital letter. - Feel free to credit yourself, by adding a sentence "Contributed by @github_username." or "Contributed by [Your Name]." to the end of the entry. * [X] [Code style](https://element-hq.github.io/synapse/latest/code_style.html) is correct (run the [linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters)) --- changelog.d/19379.bugfix | 1 + scripts-dev/mypy_synapse_plugin.py | 1 + synapse/metrics/__init__.py | 33 ++++++++++++++++++++++++------ synapse/util/metrics.py | 7 +++++++ tests/metrics/test_metrics.py | 8 ++++++++ 5 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 changelog.d/19379.bugfix diff --git a/changelog.d/19379.bugfix b/changelog.d/19379.bugfix new file mode 100644 index 0000000000..6de9543bcc --- /dev/null +++ b/changelog.d/19379.bugfix @@ -0,0 +1 @@ +Fix `InFlightGauge` typing to allow upgrading to `prometheus_client` 0.24. diff --git a/scripts-dev/mypy_synapse_plugin.py b/scripts-dev/mypy_synapse_plugin.py index 24794a1925..7fe4d6cd86 100644 --- a/scripts-dev/mypy_synapse_plugin.py +++ b/scripts-dev/mypy_synapse_plugin.py @@ -133,6 +133,7 @@ prometheus_metric_fullname_to_label_arg_map: Mapping[str, ArgLocation | None] = "prometheus_client.metrics.Info": ArgLocation("labelnames", 2), "prometheus_client.metrics.Enum": ArgLocation("labelnames", 2), "synapse.metrics.LaterGauge": ArgLocation("labelnames", 2), + "synapse.metrics._InFlightGaugeRuntime": ArgLocation("labels", 2), "synapse.metrics.InFlightGauge": ArgLocation("labels", 2), "synapse.metrics.GaugeBucketCollector": ArgLocation("labelnames", 2), "prometheus_client.registry.Collector": None, diff --git a/synapse/metrics/__init__.py b/synapse/metrics/__init__.py index b5ad6581e1..ba4334e372 100644 --- a/synapse/metrics/__init__.py +++ b/synapse/metrics/__init__.py @@ -27,6 +27,8 @@ import platform import threading from importlib import metadata from typing import ( + TYPE_CHECKING, + Any, Callable, Generic, Iterable, @@ -262,8 +264,12 @@ shutdown. MetricsEntry = TypeVar("MetricsEntry") -class InFlightGauge(Generic[MetricsEntry], Collector): - """Tracks number of things (e.g. requests, Measure blocks, etc) in flight +class _InFlightGaugeRuntime(Collector): + """ + Runtime class for InFlightGauge. Contains all actual logic. + Does not inherit from Generic to avoid method resolution order (MRO) conflicts. + + Tracks number of things (e.g. requests, Measure blocks, etc) in flight at any given time. Each InFlightGauge will create a metric called `_total` that counts @@ -292,16 +298,20 @@ class InFlightGauge(Generic[MetricsEntry], Collector): # Create a class which have the sub_metrics values as attributes, which # default to 0 on initialization. Used to pass to registered callbacks. - self._metrics_class: type[MetricsEntry] = attr.make_class( + self._metrics_class = attr.make_class( "_MetricsEntry", attrs={x: attr.ib(default=0) for x in sub_metrics}, slots=True, ) # Counts number of in flight blocks for a given set of label values - self._registrations: dict[ - tuple[str, ...], set[Callable[[MetricsEntry], None]] - ] = {} + # `Callable` should be of type `Callable[[MetricsEntry], None]`, but + # `MetricsEntry` has no meaning in this context without the higher level + # `InFlightGauge` typing information. + # Instead, the typing is enforced by having `_registrations` be private and all + # accessor functions have proper `Callable[[MetricsEntry], None]` type + # annotations. + self._registrations: dict[tuple[str, ...], set[Callable[[Any], None]]] = {} # Protects access to _registrations self._lock = threading.Lock() @@ -398,6 +408,17 @@ class InFlightGauge(Generic[MetricsEntry], Collector): yield gauge +if TYPE_CHECKING: + + class InFlightGauge(_InFlightGaugeRuntime, Generic[MetricsEntry]): + """ + Typing-only generic wrapper. + Provides InFlightGauge[T] support to type checkers. + """ +else: + InFlightGauge = _InFlightGaugeRuntime + + class GaugeHistogramMetricFamilyWithLabels(GaugeHistogramMetricFamily): """ Custom version of `GaugeHistogramMetricFamily` from `prometheus_client` that allows diff --git a/synapse/util/metrics.py b/synapse/util/metrics.py index 3daba79124..5e86939e37 100644 --- a/synapse/util/metrics.py +++ b/synapse/util/metrics.py @@ -19,6 +19,13 @@ # # +# These imports are necessary for python <= 3.13 in order for the `InFlightGauge` type +# annotations not to be evaluated at runtime. +# Starting with python 3.14, annotations are lazily evaluated by default, which is the +# behaviour we desire. +# More info here: https://docs.python.org/3/reference/compound_stmts.html#annotations +from __future__ import annotations + import logging from functools import wraps from types import TracebackType diff --git a/tests/metrics/test_metrics.py b/tests/metrics/test_metrics.py index 084eba3a5a..174b165679 100644 --- a/tests/metrics/test_metrics.py +++ b/tests/metrics/test_metrics.py @@ -18,6 +18,14 @@ # [This file includes modifications made by New Vector Limited] # # + +# These imports are necessary for python <= 3.13 in order for the `InFlightGauge` type +# annotations not to be evaluated at runtime. +# Starting with python 3.14, annotations are lazily evaluated by default, which is the +# behaviour we desire. +# More info here: https://docs.python.org/3/reference/compound_stmts.html#annotations +from __future__ import annotations + from typing import NoReturn, Protocol from prometheus_client.core import Sample