Skip to content

MQTTManager: stop a room's status thread when the room is destroyed - #406

Open
jamesmulcahy wants to merge 1 commit into
NSPManager:develfrom
jamesmulcahy:fix/room-thread-lifetime
Open

jamesmulcahy wants to merge 1 commit into
NSPManager:develfrom
jamesmulcahy:fix/room-thread-lifetime

Conversation

@jamesmulcahy

Copy link
Copy Markdown
Contributor

Problem

Each Room starts 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, EntityManager drops the Room, but its thread keeps running against the freed object. What happens next depends on what the thread was doing:

  • Waiting for the next change: it stays blocked on a freed condition variable. It can wake later, once that memory is reused for something else.
  • Waiting for a change to settle (about 100 ms by default): it goes on to build and publish a status from freed memory.

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

  • The status thread is now a 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.
  • Debounce: it used to detect "another change arrived" from wait_for() returning no_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.
  • Self-join guard: if a room were ever destroyed on its own thread (nothing does this today), ~Room() logs an error and detaches instead of throwing from join().

Tests

tests/room_test.cpp covers three cases:

  • A burst of changes produces exactly one status publish, which checks the reworked debounce.
  • Destroying a room while it's waiting for a change to settle stops its thread: nothing is published afterwards, and destruction doesn't hang.
  • A room removed from the database is unloaded and destroyed by load_rooms().

Notes

🤖 Generated with Claude Code

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
jamesmulcahy marked this pull request as ready for review October 17, 2026 21:09

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant