Repository navigation
MQTTManager: stop a room's status thread when the room is destroyed - #406
Open
jamesmulcahy wants to merge 1 commit into
Open
jamesmulcahy wants to merge 1 commit into
jamesmulcahy wants to merge 1 commit into
Conversation
Each Room started a detached thread that waited on the room's own mutex and condition variable, and sent the room's status, forever. Nothing stopped it, so once a room was deleted and EntityManager dropped it, the thread kept using the freed Room: it either stayed blocked on a freed condition variable or, if a change was waiting to settle, went on to build and publish a status from freed memory. That could crash the manager at an unrelated point, or publish a stale state to the deleted room's topic just after ~Room() had cleared it. The thread is now a std::jthread whose waits take its stop token, and ~Room() stops and joins it before anything else is torn down. Stopping wakes the thread from either wait, so the join only waits for an update already being sent. The backoff used wait_for() returning no_timeout to tell that another change had arrived. The stop token overloads take a predicate instead, so changes are now counted and the backoff waits for the count to change. This also stops a spurious wakeup from restarting the backoff. EntityManager::load_rooms() now destroys removed rooms after releasing _rooms_mutex, so that the join never waits while holding it. If a room is ever destroyed on its own thread, which nothing does today, it logs and detaches instead of throwing from join(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jamesmulcahy
marked this pull request as ready for review
October 17, 2026 21:09
This branch has not been deployed
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.
Problem
Each
Roomstarts a detached thread that waits on the room's own mutex and condition variable, and sends the room's status, forever. Nothing stops it.When a room is deleted,
EntityManagerdrops theRoom, but its thread keeps running against the freed object. What happens next depends on what the thread was doing:That can crash the manager at an unrelated point. It can also publish a stale state to the deleted room's topic, just after
~Room()cleared it.The test suite already worked around this.
create_room()warned that rooms can't be safely destroyed.Fix
std::jthread. Both of its waits take its stop token.~Room()stops and joins the thread before anything else is torn down. Stopping wakes the thread from either wait, so the join only waits for an update that's already being sent.wait_for()returningno_timeout. The stop-token overloads take a predicate instead, so changes are now counted, and the wait continues while the count keeps changing. The debounce behaves the same, except that a spurious wakeup no longer restarts it.EntityManager::load_rooms()destroys removed rooms after releasing_rooms_mutex. That way the join can never wait while holding that lock.~Room()logs an error and detaches instead of throwing fromjoin().Tests
tests/room_test.cppcovers three cases:load_rooms().Notes
std::_Exit(). Room threads were one reason for it, but static destruction order across translation units and other unstopped threads (weather, OpenHAB light group events) remain. I've updated the comment to say so.ScopedErrorLogchanges the logger's sinks) can still hit this branch's CI until MQTTManager: don't let new messages overtake ones buffered while disconnected #403 merges.SIGUSR1signal handler, which can interrupt any thread, including a room's own.🤖 Generated with Claude Code