From 5dcd4844bd4c78ef57ea50d650c032d9b792c2aa Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Sun, 13 Sep 2026 19:57:32 +0530 Subject: [PATCH] refactor(discord): one liveness-knob resolver; int knobs reject inf and fractions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_finite_positive_config_float` and `_config_int` were the same shape with the warn call pasted six times and asymmetric sign checks. Collapse both onto `_liveness_knob(key, default, cast)`: usable iff finite, >= 0 and exact for the cast; else warn and return 0; explicit 0 stays silent. Two int-path holes closed on the way: `websocket_liveness_failure_threshold: .inf` raised OverflowError inside `DiscordAdapter.__init__`, and `0.5` truncated to 0 and disabled the probe silently — the bug class this PR exists to remove. `2.0` / `"2"` still resolve to 2. --- plugins/platforms/discord/adapter.py | 48 ++++++++++---------------- tests/gateway/test_discord_liveness.py | 14 ++++++-- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/plugins/platforms/discord/adapter.py b/plugins/platforms/discord/adapter.py index 80eeed6008..6408c694a1 100644 --- a/plugins/platforms/discord/adapter.py +++ b/plugins/platforms/discord/adapter.py @@ -1115,40 +1115,30 @@ class DiscordAdapter(DiscordMediaMixin, BasePlatformAdapter): self.name, key, raw, ) + def _liveness_knob(self, key: str, default: Any, cast: type, *, env_key: Optional[str] = None): + """Resolve a liveness knob: usable iff finite, >= 0 and exact for ``cast``; else warn and return 0. + + ``0`` is the documented opt-out and stays silent. Bools, unparsable strings, nan/inf, + negatives and (for int knobs) fractional values all disable the probe WITH a warning. + """ + raw = self._config_value(key, default, env_key=env_key) + try: + value = None if isinstance(raw, bool) else float(raw) + except (TypeError, ValueError): + value = None + if value is not None and math.isfinite(value) and value >= 0 and cast(value) == value: + return cast(value) + if value != 0: + self._warn_liveness_config_disabled(key, raw) + return cast(0) + def _finite_positive_config_float( self, key: str, default: float, *, env_key: Optional[str] = None ) -> float: - """Resolve a finite positive liveness duration; invalid values disable it (with a warning).""" - raw = self._config_value(key, default, env_key=env_key) - if isinstance(raw, bool): - self._warn_liveness_config_disabled(key, raw) - return 0.0 - try: - value = float(raw) - except (TypeError, ValueError): - self._warn_liveness_config_disabled(key, raw) - return 0.0 - if math.isfinite(value) and value > 0: - return value - if value != 0: - self._warn_liveness_config_disabled(key, raw) - return 0.0 + return self._liveness_knob(key, default, float, env_key=env_key) def _config_int(self, key: str, default: int, *, env_key: Optional[str] = None) -> int: - """Resolve a positive liveness count; invalid values disable it (with a warning).""" - raw = self._config_value(key, default, env_key=env_key) - if isinstance(raw, bool): - self._warn_liveness_config_disabled(key, raw) - return 0 - try: - value = int(raw) - except (TypeError, ValueError): - self._warn_liveness_config_disabled(key, raw) - return 0 - if value < 0: - self._warn_liveness_config_disabled(key, raw) - return 0 - return value + return self._liveness_knob(key, default, int, env_key=env_key) def _handle_bot_task_done(self, task: asyncio.Task) -> None: """Surface post-startup discord.py task exits as a retryable fatal so GatewayRunner diff --git a/tests/gateway/test_discord_liveness.py b/tests/gateway/test_discord_liveness.py index 1e55aaa30b..a0e31bf2fb 100644 --- a/tests/gateway/test_discord_liveness.py +++ b/tests/gateway/test_discord_liveness.py @@ -124,6 +124,8 @@ class _BrokenWebSocket: ("websocket_max_latency_seconds", "_max_latency_seconds", "-inf"), ("websocket_max_latency_seconds", "_max_latency_seconds", True), ("websocket_max_latency_seconds", "_max_latency_seconds", "15s"), + ("websocket_liveness_failure_threshold", "_liveness_failure_threshold", 2.5), + ("websocket_liveness_failure_threshold", "_liveness_failure_threshold", float("inf")), ], ) def test_nonfinite_liveness_config_disables_that_probe_dimension(monkeypatch, key, attribute, raw): @@ -149,6 +151,7 @@ def test_unusable_liveness_config_warns_instead_of_disabling_silently(caplog): "websocket_liveness_interval_seconds": "15s", "websocket_max_latency_seconds": True, "websocket_liveness_failure_threshold": -1, + "websocket_heartbeat_ack_max_age_seconds": "nan", }, ) ) @@ -156,8 +159,10 @@ def test_unusable_liveness_config_warns_instead_of_disabling_silently(caplog): assert adapter._liveness_interval_seconds == 0.0 assert adapter._max_latency_seconds == 0.0 assert adapter._liveness_failure_threshold == 0 + assert adapter._heartbeat_ack_max_age_seconds == 0.0 warned = [r.getMessage() for r in caplog.records if "liveness knob" in r.getMessage()] - assert len(warned) == 3 + assert len(warned) == 4 + assert any("websocket_heartbeat_ack_max_age_seconds='nan'" in w for w in warned) assert any("websocket_liveness_interval_seconds='15s'" in w for w in warned) assert any("websocket_max_latency_seconds=True" in w for w in warned) assert any("websocket_liveness_failure_threshold=-1" in w for w in warned) @@ -170,11 +175,16 @@ def test_explicit_zero_liveness_knob_disables_without_warning(caplog): PlatformConfig( enabled=True, token="test-token", - extra={"websocket_liveness_interval_seconds": 0, "websocket_liveness_failure_threshold": 0}, + extra={ + "websocket_liveness_interval_seconds": 0, + "websocket_max_latency_seconds": "0", + "websocket_liveness_failure_threshold": 0, + }, ) ) assert adapter._liveness_interval_seconds == 0.0 + assert adapter._max_latency_seconds == 0.0 assert adapter._liveness_failure_threshold == 0 assert not [r for r in caplog.records if "liveness knob" in r.getMessage()]