core: add util layer — Result, Error taxonomy, bytes, event bus, pool, log

util/ carries no wire surface, so it lands before the contract freeze.

- error: enum class Error, the engine-wide failure taxonomy; is_retryable
  enumerates every value (no default:) so -Wswitch forces the retry
  decision on each future addition. ErrorInfo carries context/http_status.
- result: Result<T> over std::expected<T, ErrorInfo>, Result<void>,
  VDM_TRY / VDM_TRY_ASSIGN. Errors returned, never thrown, on the
  transfer path.
- bytes: span aliases, LE load_le/store_le (debug-asserted precondition,
  not input validation), and a bounds-checked latching ByteReader for the
  .veloxpart.meta reader.
- event_bus: typed thread-safe pub/sub; header states plainly that
  unsubscribe is not a quiesce point and download_task will need its own
  drain.
- thread_pool: std::jthread pool; dtor joins in the body before members
  die (fixed a use-after-destruction on cv_/mu_). Header notes shutdown is
  drain-only and DAEMON will need a cancel mode.
- log: sink interface (core does no I/O); DAEMON installs one.

Tested: -Werror clean, 6 binaries green under plain / ASan+UBSan / TSan.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HPPSGhiArbvQgwC2DNiURS
This commit is contained in:
2026-09-09 19:03:11 +04:00
co-authored by Claude Sonnet 5
parent 5ac74ecbd9
commit ddf36e848a
17 changed files with 1401 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
// vdm/util/thread_pool.cpp
#include "vdm/util/thread_pool.hpp"
namespace vdm {
ThreadPool::ThreadPool(std::size_t threads) {
if (threads == 0) {
threads = std::thread::hardware_concurrency();
if (threads == 0)
threads = 1;
}
workers_.reserve(threads);
for (std::size_t i = 0; i < threads; ++i)
workers_.emplace_back([this] { worker_loop(); });
}
ThreadPool::~ThreadPool() {
{
std::lock_guard lk(mu_);
stopping_ = true;
}
cv_.notify_all();
// Join here, in the destructor body, while mu_/cv_/jobs_ are still alive. Relying on
// std::jthread's implicit join would run it during member destruction — after cv_ and
// mu_ are already gone, which the workers are still touching.
for (auto &w : workers_)
w.join();
workers_.clear();
}
void ThreadPool::worker_loop() {
for (;;) {
std::function<void()> job;
{
std::unique_lock lk(mu_);
cv_.wait(lk, [this] { return stopping_ || !jobs_.empty(); });
if (jobs_.empty()) {
// Only reached when stopping_ and nothing left to do.
return;
}
job = std::move(jobs_.front());
jobs_.pop();
}
job();
}
}
} // namespace vdm