Prevent MQTT callback errors from silently killing the network loop thread#103
Merged
Conversation
…hread paho dispatches user callbacks (on_message, on_connect, on_disconnect, ...) on the network loop thread and, with `suppress_exceptions=False` (the default), re-raises any exception that escapes them. That re-raise unwinds `loop_forever` and the loop thread exits permanently -- it is never restarted, yet `is_connected()` can keep reporting True, so the session silently wedges (QoS-0 publishes are dropped) until the process restarts. - `_on_message` no longer re-raises: log with traceback and swallow, so a message-handler error can't take down the loop thread. - Set `client.suppress_exceptions = True` as a catch-all covering every callback (on_connect's subscribe/_resend_modules, on_disconnect, etc.). - Raise the default keepalive from 10s to 60s; 10s trips spurious "keep alive timeout" disconnects whenever the loop thread is briefly busy. Still overridable via the `keepalive_secs` kwarg. Tests: 2 new cases in test_robot_session_callbacks.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Builds on suppress_exceptions to also keep the error visible. Without enable_logger, paho's own "Caught exception in ..." message (emitted when it swallows a callback exception) went nowhere, so suppress_exceptions alone kept the loop alive but discarded the error. - Add RobotSession._guard_callback(name) and wrap on_connect / on_message / on_disconnect at registration: an exception is logged with a traceback via the session logger (tagged with the callback name) and swallowed, so it never reaches paho's re-raise path on the network loop thread. - Call client.enable_logger(self.logger) so paho's internal logs are surfaced. - Keep suppress_exceptions = True as the catch-all net for unwrapped/future callbacks. Tests: 2 new cases (guard logs-and-swallows; enable_logger is wired). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Note in the comment that the flag is the only thing covering callback paths the per-callback guards do not: QoS>0 on_publish, consumer-set callbacks on the public client, and future callbacks added without a guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
b-Tomas
approved these changes
Jun 9, 2026
The comment claimed an exception escaping _on_message kills paho's loop thread. That was true before _guard_callback wrapped the callbacks; now the wrapper (and suppress_exceptions) already prevent that. The clause remains for message-scoped logging (with topic) and to keep _on_message safe if called unwrapped -- not for loop-thread safety. Comment updated to match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
paho dispatches user callbacks (
on_message,on_connect,on_disconnect, …) on the network loop thread, and withsuppress_exceptions=False(paho's default) it re-raises any exception that escapes a callback (client.py:if not self.suppress_exceptions: raise). That re-raise unwindsloop_forever, so the loop thread exits permanently — it is never restarted, yetis_connected()can keep reportingTrue. The result is a session that looks connected but silently drops every QoS-0 publish (pose, key-values) until the process is restarted.This hardens
RobotSessionso a callback error can't wedge the session — and stays visible rather than merely suppressed — and reduces spurious keepalive-timeout disconnects.Changes
_guard_callback(name)wraps each registered callback (on_connect,on_message,on_disconnect): an exception is logged with a traceback through the session logger (tagged with the callback name) and swallowed, so it never reaches paho's re-raise path on the loop thread. This is the primary mechanism and keeps the error observable.client.enable_logger(self.logger)— without it, paho's own"Caught exception in …"message (emitted whensuppress_exceptionsswallows) went nowhere; now paho's internal logs are surfaced.client.suppress_exceptions = True— catch-all net for any callback path not wrapped above (and future ones)._on_messageno longer re-raises — logs with traceback (topic context) and swallows.keepalive_secskwarg.Note on the keepalive change
is_connected()is a cached state flag updated by the loop thread; a silent/black-hole link is only detected via keepalive, i.e. up to ~2× keepalive. Raising the default to 60s trades slower silent-drop detection (now up to ~120s, was ~20s) for far fewer false-positive disconnects. Latency-sensitive callers can pass a smallerkeepalive_secs.Test plan
test_robot_session_callbacks.py: handler error doesn't propagate out of_on_message; keepalive default is 60s;_guard_callbacklogs-and-swallows;enable_loggeris wired.test_metrics(requires theopentelemetryextra) andtest_video::test_robot_session_register_camera(OpenCV camera) — both fail identically with these edits reverted.flake8clean,black --checkclean.🤖 Generated with Claude Code