feat(util): add SerialJobLane

A FIFO of blocking jobs, run one at a time on a background thread that a
post starts when none is running and that exits once the queue is empty.
Each job returns a completion, and completions run only in pump(), on
the owner's thread. Unlike RPCWorker and AsyncTaskManager nothing ever
joins: abandon() (also run by the destructor) drops queued jobs and
undelivered completions and refuses later posts, and a job stuck in a
blocking call finishes on its own thread. It is meant for backend calls
that can wait out a long scan and may have to be given up. post()
reports a dropped job (abandoned lane, or no thread could be started),
and waitIdle() is bounded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 21:09:48 -05:00
co-authored by Claude Opus 5.5
parent 296c04386c
commit b2a57a61a0
3 changed files with 222 additions and 0 deletions
+144
View File
@@ -0,0 +1,144 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// util::SerialJobLane - a FIFO of blocking jobs run one at a time on a lazily started background thread that can be
// ABANDONED. Each job returns a completion; completions run only inside pump(), on the owner's thread. Unlike
// RPCWorker / AsyncTaskManager nothing here ever joins: a job stuck in a blocking call (a lite-backend save waiting
// out a scan) is abandoned when the lane is destroyed, and its thread finishes on its own holding only the shared State.
// post() is meant to be called from ONE producer thread (the owner); pump() from the owner thread.
#pragma once
#include <chrono>
#include <condition_variable>
#include <deque>
#include <functional>
#include <memory>
#include <mutex>
#include <thread>
#include <utility>
namespace dragonx {
namespace util {
class SerialJobLane {
public:
using Completion = std::function<void()>; // runs on the thread that calls pump()
using Job = std::function<Completion()>; // runs on the lane thread; may return an empty Completion
SerialJobLane() : st_(std::make_shared<State>()) {}
~SerialJobLane() { abandon(); }
SerialJobLane(const SerialJobLane&) = delete;
SerialJobLane& operator=(const SerialJobLane&) = delete;
// Owner thread. False if the job was dropped: the lane is abandoned, or no thread could be started.
bool post(Job job)
{
if (!job) return false;
bool spawn = false;
{
std::lock_guard<std::mutex> lk(st_->m);
if (st_->closed) return false;
st_->queue.push_back(std::move(job));
if (!st_->threadRunning) { st_->threadRunning = true; spawn = true; }
}
if (!spawn) return true;
try {
std::thread([st = st_] { run(st); }).detach();
} catch (...) {
std::deque<Job> dropped; // destroyed outside the lock (may wipe captured secrets)
{
std::lock_guard<std::mutex> lk(st_->m);
st_->threadRunning = false;
dropped.swap(st_->queue); // single producer: holds only the job just posted
}
return false;
}
return true;
}
// Owner thread: run every completion produced so far, in FIFO order. Returns how many ran.
int pump()
{
std::deque<Completion> ready;
{
std::lock_guard<std::mutex> lk(st_->m);
ready.swap(st_->done);
}
int n = 0;
for (auto& c : ready) { if (c) { c(); ++n; } }
return n;
}
bool hasCompletions() const { std::lock_guard<std::mutex> lk(st_->m); return !st_->done.empty(); }
bool busy() const
{
std::lock_guard<std::mutex> lk(st_->m);
return !st_->queue.empty() || st_->executing || !st_->done.empty();
}
// Bounded: true once nothing is queued or executing (completions may still await pump()).
bool waitIdle(std::chrono::milliseconds timeout)
{
std::unique_lock<std::mutex> lk(st_->m);
return st_->cv.wait_for(lk, timeout, [this] { return st_->queue.empty() && !st_->executing; });
}
// Drop queued jobs and undelivered completions; refuse later posts. A running job finishes by itself and its
// completion is discarded. Idempotent.
void abandon()
{
std::deque<Job> q;
std::deque<Completion> d;
{
std::lock_guard<std::mutex> lk(st_->m);
st_->closed = true;
q.swap(st_->queue);
d.swap(st_->done);
} // q/d destroyed here, outside the lock
}
private:
struct State {
mutable std::mutex m;
std::condition_variable cv;
std::deque<Job> queue;
std::deque<Completion> done;
bool threadRunning = false;
bool executing = false;
bool closed = false;
};
static void run(std::shared_ptr<State> st)
{
for (;;) {
Job job;
{
std::lock_guard<std::mutex> lk(st->m);
if (st->closed || st->queue.empty()) {
st->threadRunning = false;
st->cv.notify_all();
return;
}
job = std::move(st->queue.front());
st->queue.pop_front();
st->executing = true;
}
Completion done;
try { done = job(); } catch (...) {}
job = nullptr; // release captured state (secret holders) BEFORE reporting idle
{
std::lock_guard<std::mutex> lk(st->m);
st->executing = false;
if (done && !st->closed) st->done.push_back(std::move(done));
st->cv.notify_all();
}
// A completion that was not queued (closed lane) is destroyed here, on this thread, never invoked.
}
}
std::shared_ptr<State> st_;
};
} // namespace util
} // namespace dragonx