Skip to content

Stop socket subscription listener thread on destruction - #77

Merged
garethpotter merged 1 commit into
mainfrom
socket-subscription-shutdown
Nov 6, 2026
Merged

garethpotter merged 1 commit into
mainfrom
socket-subscription-shutdown

Conversation

@garethpotter

@garethpotter garethpotter commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

ThingSetSocketSubscriptionTransport (POSIX) starts a listener thread that loops forever in a blocking recvfrom() and is never joined or detached. Destroying the transport destroys a joinable std::thread, which calls std::terminate. This is also why there were no socket publish/subscribe tests.

Changes

  • Listener shutdown: the listener loop now waits with poll() (10 ms timeout) and checks a _runListener flag. The POSIX transport has a new destructor that clears the flag and joins the thread before the base class closes the socket. This follows the same pattern as ThingSetSocketServerTransport and RawCanSocketListener. No change to the base subscription interface.
  • Failed reads: a recvfrom() error is now skipped. Previously -1 was stored in Frame::length (a size_t) and passed to the decoder as a huge length.
  • ThingSetListener::subscribe: the lambda passed to the transport captured [&], including the callback parameter, which no longer exists once subscribe() returns. Every report then called through a dangling reference (the Asio publish/subscribe test only passed by luck). It now captures callback by copy. This header is shared by all transports.
  • Zephyr: behaviour is unchanged apart from the flag. The thread still runs for the life of the program.

Tests

New TestSocketIpPublishSubscribe.cpp (non-Apple only, like the other socket tests):

  • published reports arrive at the subscriber
  • destroying a subscribed transport stops the thread, and later reports don't reach the old callback
  • destroying a transport that never subscribed is safe
  • a new transport can subscribe and receive after an earlier one has gone

All four passed 30 out of 30 repeated runs in the gcc container. Against the original transport code, the first one aborts the test binary with terminate called without an active exception.

Two existing failures on main are not touched here: ServesClientInReusedSlotWhileOthersIdle fails intermittently (fixed in the socket client/server tests PR), and TextDecoder.DecodeString fails in the full suite.

Note: ThingSetListener.hpp doesn't include the headers it uses (ThingSetRegistry, ThingSetNodeType), so the new test includes ThingSetServer.hpp first, as the Asio test does.

🤖 Generated with Claude Code

The POSIX socket subscription transport's listener thread looped forever
and was never joined, so destroying the transport called std::terminate.
The listener now polls with a timeout and checks a run flag, and the
destructor clears the flag and joins the thread before the socket is
closed. Failed reads are skipped instead of being passed to the decoder
as a huge length.

ThingSetListener::subscribe also captured its callback parameter by
reference in the lambda it hands to the transport, leaving a dangling
reference once subscribe() returned. It now captures by copy.

Adds end-to-end publish/subscribe tests for the socket transports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@garethpotter
garethpotter merged commit 9491130 into main Nov 6, 2026
6 checks passed
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.

2 participants