Push Tracker
ria-toolkit-oss/docs/spectrogram_dashboard_op_bug.md

124 lines
4.3 KiB
Markdown

# Bug: `SpectrogramDashboardOp` destructor calls `std::terminate`
## Summary
`SpectrogramDashboardOp` spawns an HTTP server thread during setup but its destructor
does not `join()` or `detach()` it. Per the C++ standard, destroying a joinable
`std::thread` calls `std::terminate()` — so **any** shutdown path kills the app:
init failure, Ctrl-C, or normal exit at end of `main`.
## Evidence
Built app (`new_dashboard`) crashes on shutdown with this backtrace:
```
#3 __GI_raise
#4 __GI_abort
#5 libstdc++ (std::terminate handler)
#6 libstdc++
#7 std::terminate()
#8 std::thread::~thread()
#9 ria::ops::SpectrogramDashboardOp::~SpectrogramDashboardOp()
#10 __gnu_cxx::new_allocator<SpectrogramDashboardOp>::destroy(...)
...
#23 ria::Pipeline::~Pipeline()
#24 main
```
The stack shows the failure is entirely inside the op's own destructor — not
downstream of any flow / port-wiring issue. The op's startup message
`HTTP server started on port 8080` prints just before the crash, confirming the
server thread is running and joinable when destruction begins.
## Reproduction
1. Build any RIA app that includes `SpectrogramDashboardOp`.
2. Run the container; it crashes with `terminate called without an active exception`
regardless of whether other operators succeed or fail.
## Root cause
Standard C++ invariant:
> If a `std::thread` object is destroyed while still `joinable()`, the destructor
> calls `std::terminate()`.
> — [cppreference.com/w/cpp/thread/thread/~thread](https://en.cppreference.com/w/cpp/thread/thread/~thread)
The destructor needs to (a) signal the server to stop, (b) wait for the thread
to exit, and (c) join it before the `std::thread` member is destroyed.
## Fix
In `SpectrogramDashboardOp`:
```cpp
SpectrogramDashboardOp::~SpectrogramDashboardOp() {
// 1. Tell the HTTP server / websocket server to stop accepting
// and to return from its serve loop. Exact call depends on the
// HTTP library in use:
// - cpp-httplib: server_.stop();
// - Boost.Beast: acceptor_.close(); io_context_.stop();
// - custom: shutdown_flag_.store(true); close(listen_fd_);
if (server_) {
server_->stop();
}
// 2. Join the thread if it was ever started.
if (http_thread_.joinable()) {
http_thread_.join();
}
}
```
If multiple threads are owned (e.g. separate WebSocket broadcaster, update-rate
timer), join **each** of them.
## Related checks
While fixing this op, audit any other operator in the same repo that owns a
thread:
```bash
grep -rn "std::thread " src/
```
For each match, confirm the owning class's destructor does:
```cpp
if (thread_.joinable()) thread_.join();
```
plus whatever shutdown signal is needed to make the thread actually return.
## Acceptance
- `SpectrogramDashboardOp` destructor joins all spawned threads.
- A RIA app containing this op exits cleanly on `Ctrl-C` with no
`terminate called without an active exception` message.
- Forcing an init failure (e.g. a bad `websocket_port`) produces a readable
exception message instead of `SIGABRT`.
---
## Prompt to paste into Claude Code (in the op's repo)
> `SpectrogramDashboardOp` has a latent bug: its destructor lets a joinable
> `std::thread` (the HTTP server thread that prints "HTTP server started on
> port 8080") go out of scope, which per the C++ standard calls
> `std::terminate()`. This makes any built RIA app containing this op crash on
> every shutdown path — init failure, normal exit, and Ctrl-C — with the
> unhelpful message `terminate called without an active exception`. Stack trace
> at the point of abort goes through `std::thread::~thread()` →
> `SpectrogramDashboardOp::~SpectrogramDashboardOp()`.
>
> Fix the destructor: (a) signal the HTTP server to stop (e.g. `server_->stop()`
> for cpp-httplib, or close the listening socket + set a shutdown flag), then
> (b) `if (http_thread_.joinable()) http_thread_.join();`. Apply the same pattern
> to any other `std::thread` members the op owns (WebSocket broadcaster, rate
> timer, etc.). Then grep for other `std::thread` members in this repo and audit
> their owners' destructors for the same bug.
>
> Acceptance: the op's destructor joins every thread it starts; a test that
> constructs and immediately destroys the op exits cleanly; Ctrl-C on a running
> app produces no `terminate` message.