Coverage for zenmqtt/instrumentation/monitors.py: 100%
50 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 21:55 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 21:55 +0000
1"""
2Monitors which watch the state of the client and report problems by logs and
3metrics. They don't change the behaviour of the client.
4"""
6import asyncio
7from logging import getLogger
8from typing import Callable, Final, Optional
10from zenmqtt.instrumentation.metrics import MetricsCollector
11from zenmqtt.mqtt.publish import MQTTMessage
13logger = getLogger(__name__)
15# number of messages waiting for the application, starting from which the
16# client warns about a slow application
17BUFFERED_MESSAGES_WARNING: Final[int] = 1000
18# seconds between warnings about a slow application
19_WARNING_INTERVAL: Final[float] = 10.0
20# seconds between reports of the buffer size to metrics while it's above the
21# warning threshold; it changes with every message, the last value is enough
22_REPORT_INTERVAL: Final[float] = 0.1
24# seconds after which a not acknowledged message which holds back acks of
25# later messages is logged
26ACK_BLOCKED_WARNING: Final[float] = 30.0
29def _now() -> float:
30 return asyncio.get_running_loop().time()
33class BufferMonitor:
34 """
35 Watches the size of the buffer of incoming messages: while at least
36 `threshold` messages wait for the application, a warning is logged and the
37 size is reported by metrics.
38 """
40 def __init__(self, metrics: MetricsCollector) -> None:
41 self._metrics = metrics
43 self.threshold = BUFFERED_MESSAGES_WARNING
44 # the buffer reached the threshold
45 self._overloaded = False
46 self._reported_at: Optional[float] = None
47 self._warned_at: Optional[float] = None
49 def update(self, size: int) -> None:
50 """
51 Called on every change of the buffer. Reports the size when it reaches
52 the threshold, then at most every _REPORT_INTERVAL while it's above,
53 and once when it falls below.
54 """
55 if size < self.threshold:
56 if self._overloaded:
57 self._overloaded = False
58 self._metrics.on_messages_buffered(size)
60 return
62 now = _now()
64 if (
65 not self._overloaded
66 or self._reported_at is None
67 or now - self._reported_at >= _REPORT_INTERVAL
68 ):
69 self._metrics.on_messages_buffered(size)
70 self._reported_at = now
72 self._overloaded = True
74 if self._warned_at is None or now - self._warned_at >= _WARNING_INTERVAL:
75 logger.warning(
76 "mqtt_protocol.slow_consumer: %s incoming messages wait for the "
77 "application, it doesn't read client.messages fast enough",
78 size,
79 )
80 self._warned_at = now
83class AckBlockedMonitor:
84 """
85 Acks are sent in the order of receiving (MQTT 5, 4.6), so one message which
86 isn't acknowledged holds back the acks of later ones. Logs a warning once
87 per message when it does so for ACK_BLOCKED_WARNING seconds; one timer for
88 the current first message, not per message.
89 """
91 def __init__(self) -> None:
92 self._message: Optional[MQTTMessage] = None
93 self._timer: Optional[asyncio.TimerHandle] = None
95 def watch(
96 self,
97 message: MQTTMessage,
98 received_at: float,
99 waiting: Callable[[], Optional[int]],
100 ) -> None:
101 """
102 Called when `message` holds back acks of later messages.
104 :param received_at: loop time
105 :param waiting: the number of messages which wait for `message`, None
106 when it doesn't hold back acks anymore
107 """
108 if message is self._message:
109 return
111 if self._timer:
112 self._timer.cancel()
114 self._message = message
115 self._timer = asyncio.get_running_loop().call_at(
116 received_at + ACK_BLOCKED_WARNING, self._warn, received_at, waiting
117 )
119 def _warn(self, received_at: float, waiting: Callable[[], Optional[int]]) -> None:
120 self._timer = None
121 message = self._message
123 # acknowledged meanwhile or received by the previous connection
124 if message is None or (count := waiting()) is None:
125 return
127 logger.warning(
128 "mqtt_protocol.ack_blocked: the message pid:%s topic:%s isn't "
129 "acknowledged for %.0f s; acks are sent in the order of receiving, "
130 "so %s later messages wait for it",
131 message.packet_identifier,
132 message.topic,
133 _now() - received_at,
134 count,
135 )