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:
@@ -0,0 +1,96 @@
|
||||
// vdm/util/error.cpp
|
||||
|
||||
#include "vdm/util/error.hpp"
|
||||
|
||||
namespace vdm {
|
||||
|
||||
std::string_view error_name(Error e) noexcept {
|
||||
switch (e) {
|
||||
case Error::ok: return "ok";
|
||||
case Error::canceled: return "canceled";
|
||||
case Error::resolve_failed: return "resolve_failed";
|
||||
case Error::connect_failed: return "connect_failed";
|
||||
case Error::tls_failed: return "tls_failed";
|
||||
case Error::connection_reset: return "connection_reset";
|
||||
case Error::timeout: return "timeout";
|
||||
case Error::too_many_redirects: return "too_many_redirects";
|
||||
case Error::http_client_error: return "http_client_error";
|
||||
case Error::http_server_error: return "http_server_error";
|
||||
case Error::auth_required: return "auth_required";
|
||||
case Error::forbidden: return "forbidden";
|
||||
case Error::not_found: return "not_found";
|
||||
case Error::range_not_satisfiable: return "range_not_satisfiable";
|
||||
case Error::gone: return "gone";
|
||||
case Error::server_file_changed: return "server_file_changed";
|
||||
case Error::content_length_mismatch: return "content_length_mismatch";
|
||||
case Error::checksum_mismatch: return "checksum_mismatch";
|
||||
case Error::disk_full: return "disk_full";
|
||||
case Error::io_error: return "io_error";
|
||||
case Error::path_rejected: return "path_rejected";
|
||||
case Error::permission_denied: return "permission_denied";
|
||||
case Error::meta_corrupt: return "meta_corrupt";
|
||||
case Error::meta_version_unsupported: return "meta_version_unsupported";
|
||||
case Error::probe_failed: return "probe_failed";
|
||||
case Error::unsupported_url_scheme: return "unsupported_url_scheme";
|
||||
case Error::max_retries_exhausted: return "max_retries_exhausted";
|
||||
case Error::internal: return "internal";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
bool is_retryable(Error e) noexcept {
|
||||
// Every value is listed on purpose: no `default:`, so adding an Error without a
|
||||
// -Werror -Wswitch diagnostic is impossible and the retry decision is forced.
|
||||
switch (e) {
|
||||
// retryable
|
||||
case Error::resolve_failed:
|
||||
case Error::connect_failed:
|
||||
case Error::connection_reset:
|
||||
case Error::timeout:
|
||||
case Error::http_server_error:
|
||||
case Error::range_not_satisfiable: // stale metadata → re-probe then retry
|
||||
case Error::content_length_mismatch:
|
||||
return true;
|
||||
|
||||
// not retryable
|
||||
case Error::ok:
|
||||
case Error::canceled:
|
||||
case Error::tls_failed:
|
||||
case Error::too_many_redirects:
|
||||
case Error::http_client_error:
|
||||
case Error::auth_required: // resolved by credentials, not a retry
|
||||
case Error::forbidden:
|
||||
case Error::not_found:
|
||||
case Error::gone:
|
||||
case Error::server_file_changed: // needs a user decision
|
||||
case Error::checksum_mismatch:
|
||||
case Error::disk_full:
|
||||
case Error::io_error:
|
||||
case Error::path_rejected:
|
||||
case Error::permission_denied:
|
||||
case Error::meta_corrupt:
|
||||
case Error::meta_version_unsupported:
|
||||
case Error::probe_failed:
|
||||
case Error::unsupported_url_scheme:
|
||||
case Error::max_retries_exhausted:
|
||||
case Error::internal:
|
||||
return false;
|
||||
}
|
||||
return false; // unreachable; silences -Wreturn-type for an out-of-range value
|
||||
}
|
||||
|
||||
std::string ErrorInfo::to_string() const {
|
||||
std::string out(name());
|
||||
if (!context.empty()) {
|
||||
out += ": ";
|
||||
out += context;
|
||||
}
|
||||
if (http_status != 0) {
|
||||
out += " (HTTP ";
|
||||
out += std::to_string(http_status);
|
||||
out += ')';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace vdm
|
||||
@@ -0,0 +1,53 @@
|
||||
// vdm/util/log.cpp
|
||||
|
||||
#include "vdm/util/log.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
|
||||
namespace vdm {
|
||||
namespace {
|
||||
|
||||
// Guards assignment; reads take a copy of the shared_ptr under the same lock so a
|
||||
// concurrent set_log_sink() can't free the sink mid-write.
|
||||
std::mutex g_mu;
|
||||
std::shared_ptr<LogSink> g_sink;
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string_view log_level_name(LogLevel l) noexcept {
|
||||
switch (l) {
|
||||
case LogLevel::trace: return "trace";
|
||||
case LogLevel::debug: return "debug";
|
||||
case LogLevel::info: return "info";
|
||||
case LogLevel::warn: return "warn";
|
||||
case LogLevel::error: return "error";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
void set_log_sink(std::shared_ptr<LogSink> sink) {
|
||||
std::lock_guard lk(g_mu);
|
||||
g_sink = std::move(sink);
|
||||
}
|
||||
|
||||
std::shared_ptr<LogSink> log_sink() {
|
||||
std::lock_guard lk(g_mu);
|
||||
return g_sink;
|
||||
}
|
||||
|
||||
namespace detail {
|
||||
bool log_wants(LogLevel level) {
|
||||
auto s = log_sink();
|
||||
return s && s->enabled(level);
|
||||
}
|
||||
} // namespace detail
|
||||
|
||||
void log_emit(LogLevel level, std::string_view category, std::string message) {
|
||||
auto s = log_sink();
|
||||
if (!s || !s->enabled(level))
|
||||
return;
|
||||
s->write(LogRecord{level, category, std::move(message)});
|
||||
}
|
||||
|
||||
} // namespace vdm
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user