refactor(discord): state the dispatch-liveness rationale once

The #109521 explanation (socket_event_type fires for every parsed
DISPATCH frame and is not debug-gated unlike on_socket_raw_receive;
op-11 ACKs carry no event type) was written out four times: knob init,
stamp init, the on_socket_event_type handler, and _read_websocket_health.
Keep the authoritative paragraph on the handler that actually stamps, and
point the other three sites at it so a future edit has one place to go.

Drop the math.isfinite(event_silence) half of the silence guard. Both
operands are our own perf_counter floats, so their difference cannot be
non-finite; the ack_age guard is different because _last_ack comes from
discord.py. math stays imported for the remaining finiteness checks.
This commit is contained in:
kshitijk4poor
2026-09-15 00:41:58 +05:30
committed by kshitij
parent b4a5ce62e7
commit 909731dc71
+21 -21
View File
@@ -1071,16 +1071,11 @@ class DiscordAdapter(DiscordMediaMixin, BasePlatformAdapter):
self._max_latency_seconds = self._finite_positive_config_float(
"websocket_max_latency_seconds", 30.0,
)
# Dispatch-side liveness (#109521 incident 2): an ESTAB socket can keep ACKing
# heartbeats (op 11, no event type) while zero DISPATCH events are parsed, so every
# transport-side sample reads healthy for hours. ``socket_event_type`` fires for every
# parsed DISPATCH frame and is NOT gated behind ``enable_debug_events`` (unlike
# ``on_socket_raw_receive`` — verified against discord.py 2.7.1 ``gateway.py``:
# ``received_message`` calls ``self._dispatch('socket_event_type', event)`` before the
# op-code switch). 0 disables this dimension alone; ack-age/latency still guard.
# Default 4h mirrors the field-proven operator bound from the incident report; quiet
# guilds can go hours without a single DISPATCH event (typing/reaction/presence), so
# a short bound would force reconnect loops on healthy-but-idle installs (#109782).
# Dispatch-side liveness bound (#109521; rationale on ``on_socket_event_type``).
# 0 disables this dimension alone; ack-age/latency still guard. Default 4h mirrors the
# field-proven operator bound from the incident report; quiet guilds can go hours
# without a single DISPATCH event, so a short bound would force reconnect loops on
# healthy-but-idle installs (#109782).
self._event_max_silence_seconds = self._finite_positive_config_float(
"websocket_event_max_silence_seconds", 14400.0,
)
@@ -1088,10 +1083,9 @@ class DiscordAdapter(DiscordMediaMixin, BasePlatformAdapter):
self._liveness_notification_task: Optional[asyncio.Task] = None
# True while disconnect() intentionally closes discord.py (done callback: shutdown vs crash).
self._disconnecting = False
# Last DISPATCH frame's monotonic stamp (#109521): ticked by ``on_socket_event_type``
# (fires for every parsed DISPATCH event, not debug-gated) and read by the liveness
# probe's ``event_silence`` dimension. ``None`` means "no event yet on this connection"
# and is not treated as silence (on_ready often arrives in bursts).
# Last DISPATCH frame's monotonic stamp, ticked by ``on_socket_event_type`` (see its
# rationale) and read by ``_read_websocket_health``. ``None`` = no event yet on this
# connection, which is not silence.
self._last_dispatched_event_monotonic: Optional[float] = None
self._missed_message_backfill_task: Optional[asyncio.Task] = None
from hermes_constants import get_hermes_home
@@ -1287,11 +1281,15 @@ class DiscordAdapter(DiscordMediaMixin, BasePlatformAdapter):
@self._client.event
async def on_socket_event_type(event_type: str):
# Dispatch-side liveness stamp (#109521): discord.py dispatches this for every
# parsed DISPATCH frame on every connection — no ``enable_debug_events`` needed
# (unlike ``on_socket_raw_receive``). Heartbeat ACKs (op 11) return before the
# dispatch, so an ACKing-but-deaf socket leaves this stamp frozen while every
# transport-side check reads healthy.
# Dispatch-side liveness stamp (#109521 incident 2): an ESTAB socket can keep
# ACKing heartbeats (op 11, no event type) while zero DISPATCH events are parsed,
# so every transport-side sample reads healthy for hours. discord.py dispatches
# ``socket_event_type`` for every parsed DISPATCH frame on every connection and it
# is NOT gated behind ``enable_debug_events`` (unlike ``on_socket_raw_receive`` —
# verified against discord.py 2.7.1 ``gateway.py``: ``received_message`` calls
# ``self._dispatch('socket_event_type', event)`` before the op-code switch).
# Heartbeat ACKs return before that dispatch, so an ACKing-but-deaf socket leaves
# this stamp frozen while every transport-side check reads healthy.
adapter_self._last_dispatched_event_monotonic = time.perf_counter()
@self._client.event
@@ -1693,12 +1691,14 @@ class DiscordAdapter(DiscordMediaMixin, BasePlatformAdapter):
# explicit 0 disables this dimension alone and ack-age/latency keep guarding (the #109782
# regression put the knob in the probe's all-or-nothing startup guard, killing the whole
# watchdog). ``None`` = no DISPATCH event yet on this connection: not silence (the
# not_ready check above still covers the pre-ready window).
# not_ready check above still covers the pre-ready window). Why the stamp is trustworthy:
# see ``on_socket_event_type``. No finiteness guard here: both operands are our own
# perf_counter floats (``ack_age`` differs — ``_last_ack`` is discord.py's).
if self._event_max_silence_seconds > 0:
last_event = self._last_dispatched_event_monotonic
if last_event is not None:
event_silence = time.perf_counter() - last_event
if not math.isfinite(event_silence) or event_silence > self._event_max_silence_seconds:
if event_silence > self._event_max_silence_seconds:
return False, "event_silence"
return True, "healthy"