Files
ObsidianDragon/src/rpc/rpc_worker.cpp
T
DanSandClaude Opus 4.8 d9143f0e18 fix(rpc): keep the worker alive on non-std throws; atomic flags
The RPC worker loop only caught std::exception. A task that threw
anything else - a non-std exception propagating out of a posted job -
escaped the loop and tore down the worker thread, so every later RPC
silently went unanswered.

- The worker loop now also has a catch-all that swallows non-std
  throws and keeps the thread running. Such a job still loses its
  main-thread callback, exactly as the std::exception path already
  did; the only change is that the worker survives.
- RPCClient's connected_ and warming_up_ flags are now atomic. They
  are read from the UI thread while the worker updates them, so the
  plain bools were a data race.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-14 07:21:07 -05:00

155 lines
3.7 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// rpc_worker.cpp — Background work queue. Executes WorkFn on its own thread,
// returns MainCb callbacks drained each frame on the main thread.
#include "rpc_worker.h"
#include <cstdio>
#include "../util/logger.h"
namespace dragonx {
namespace rpc {
RPCWorker::RPCWorker() = default;
RPCWorker::~RPCWorker()
{
stop();
}
void RPCWorker::start()
{
if (running_.load(std::memory_order_relaxed)) return;
running_.store(true, std::memory_order_release);
thread_ = std::thread(&RPCWorker::run, this);
}
void RPCWorker::stop()
{
if (!running_.load(std::memory_order_relaxed) && !thread_.joinable()) return;
// Signal stop if not already signaled
requestStop();
if (thread_.joinable()) {
thread_.join();
}
// Discard pending tasks
{
std::lock_guard<std::mutex> lk(taskMtx_);
tasks_.clear();
}
}
void RPCWorker::requestStop()
{
if (!running_.load(std::memory_order_relaxed)) return;
{
std::lock_guard<std::mutex> lk(taskMtx_);
running_.store(false, std::memory_order_release);
}
taskCv_.notify_one();
}
void RPCWorker::post(WorkFn work)
{
{
std::lock_guard<std::mutex> lk(taskMtx_);
tasks_.push_back(std::move(work));
}
taskCv_.notify_one();
}
int RPCWorker::drainResults()
{
// Swap the result queue under the lock, then execute outside the lock
// to minimise contention with the worker thread.
std::deque<MainCb> batch;
{
std::lock_guard<std::mutex> lk(resultMtx_);
batch.swap(results_);
}
int count = 0;
for (auto& cb : batch) {
if (cb) {
try {
cb();
} catch (const std::exception& e) {
DEBUG_LOGF("[RPCWorker] Main-thread callback threw: %s\n", e.what());
} catch (...) {
DEBUG_LOGF("[RPCWorker] Main-thread callback threw unknown exception\n");
}
++count;
}
}
return count;
}
bool RPCWorker::hasPendingResults() const
{
std::lock_guard<std::mutex> lk(resultMtx_);
return !results_.empty();
}
std::size_t RPCWorker::pendingTaskCount() const
{
std::lock_guard<std::mutex> lk(taskMtx_);
return tasks_.size();
}
std::size_t RPCWorker::pendingResultCount() const
{
std::lock_guard<std::mutex> lk(resultMtx_);
return results_.size();
}
void RPCWorker::run()
{
while (true) {
WorkFn task;
// Wait for a task or stop signal
{
std::unique_lock<std::mutex> lk(taskMtx_);
taskCv_.wait(lk, [this] {
return !tasks_.empty() || !running_.load(std::memory_order_acquire);
});
if (!running_.load(std::memory_order_acquire) && tasks_.empty()) {
break;
}
if (!tasks_.empty()) {
task = std::move(tasks_.front());
tasks_.pop_front();
}
}
if (!task) continue;
// Execute the work function (blocking I/O happens here)
try {
MainCb result = task();
if (result) {
std::lock_guard<std::mutex> lk(resultMtx_);
results_.push_back(std::move(result));
}
} catch (const std::exception& e) {
DEBUG_LOGF("[RPCWorker] Task threw: %s\n", e.what());
} catch (...) {
// A raw post() job that throws still loses its MainCb (no worse than the
// std::exception path); this only keeps the worker thread alive.
DEBUG_LOGF("[RPCWorker] Task threw unknown exception\n");
}
}
}
} // namespace rpc
} // namespace dragonx