fix(rpc): abort in-flight curl on disconnect/shutdown to avoid UI freezes

stop()-ing a worker that is mid curl_easy_perform joined on the UI thread, so a
slow/hung transfer froze the UI until the request timeout. Add RPCClient::
requestAbort() (a thread-safe atomic read by a curl progress callback that aborts
the transfer), and call it before stopping the workers on disconnect
(onDisconnected) and shutdown (beginShutdown + the synchronous fallback). The
flag is cleared on each connect() so a fresh connection never starts aborted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 14:43:34 -05:00
parent 070a516f4e
commit 142a6826af
4 changed files with 48 additions and 2 deletions

View File

@@ -88,6 +88,14 @@ static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::stri
return totalSize;
}
// curl progress callback: a non-zero return aborts the in-flight transfer. This lets a
// requestAbort() from another thread (disconnect/shutdown) unblock curl_easy_perform so the
// UI thread's worker join() returns promptly instead of waiting out the request timeout.
static int xferInfoCallback(void* clientp, curl_off_t, curl_off_t, curl_off_t, curl_off_t) {
const auto* self = static_cast<const RPCClient*>(clientp);
return (self != nullptr && self->abortRequested()) ? 1 : 0;
}
// Private implementation using libcurl
class RPCClient::Impl {
public:
@@ -170,6 +178,11 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
curl_easy_setopt(impl_->curl, CURLOPT_URL, impl_->url.c_str());
curl_easy_setopt(impl_->curl, CURLOPT_HTTPHEADER, impl_->headers);
curl_easy_setopt(impl_->curl, CURLOPT_WRITEFUNCTION, WriteCallback);
// Progress callback so requestAbort() can unblock an in-flight curl_easy_perform.
clearAbort(); // a fresh connection must not start in the aborted state
curl_easy_setopt(impl_->curl, CURLOPT_NOPROGRESS, 0L);
curl_easy_setopt(impl_->curl, CURLOPT_XFERINFOFUNCTION, xferInfoCallback);
curl_easy_setopt(impl_->curl, CURLOPT_XFERINFODATA, this);
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, 30L);
// Localhost fails fast if nothing is listening; a remote/TLS daemon needs a larger
// budget for the TCP + TLS handshake over real network latency (1s would spuriously fail).
@@ -230,6 +243,18 @@ json RPCClient::getLastConnectInfo() const
return last_connect_info_;
}
void RPCClient::requestAbort()
{
// Deliberately NOT taking curl_mutex_ — the whole point is to interrupt a call() that is
// currently holding it inside curl_easy_perform. The atomic is read by xferInfoCallback.
abort_.store(true, std::memory_order_relaxed);
}
void RPCClient::clearAbort()
{
abort_.store(false, std::memory_order_relaxed);
}
void RPCClient::disconnect()
{
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);