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

4.3 KiB

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

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:

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.

While fixing this op, audit any other operator in the same repo that owns a thread:

grep -rn "std::thread " src/

For each match, confirm the owning class's destructor does:

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.