Compare commits

..

15 Commits

8 changed files with 994 additions and 367 deletions

View File

@@ -113,6 +113,7 @@ set(SOURCES
src/http_handler.cpp src/http_handler.cpp
src/arena_allocator.cpp src/arena_allocator.cpp
src/format.cpp src/format.cpp
src/metric.cpp
${CMAKE_BINARY_DIR}/json_tokens.cpp) ${CMAKE_BINARY_DIR}/json_tokens.cpp)
add_executable(weaseldb ${SOURCES}) add_executable(weaseldb ${SOURCES})
@@ -152,10 +153,15 @@ target_compile_options(test_commit_request PRIVATE -UNDEBUG)
add_executable( add_executable(
test_http_handler test_http_handler
tests/test_http_handler.cpp src/http_handler.cpp src/arena_allocator.cpp tests/test_http_handler.cpp
src/connection.cpp src/connection_registry.cpp) src/http_handler.cpp
src/arena_allocator.cpp
src/format.cpp
src/connection.cpp
src/connection_registry.cpp
src/metric.cpp)
target_link_libraries(test_http_handler doctest::doctest llhttp_static target_link_libraries(test_http_handler doctest::doctest llhttp_static
Threads::Threads perfetto) Threads::Threads perfetto simdutf::simdutf)
target_include_directories(test_http_handler PRIVATE src) target_include_directories(test_http_handler PRIVATE src)
target_compile_definitions(test_http_handler target_compile_definitions(test_http_handler
PRIVATE DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN) PRIVATE DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN)
@@ -170,6 +176,8 @@ add_executable(
src/arena_allocator.cpp src/arena_allocator.cpp
src/config.cpp src/config.cpp
src/http_handler.cpp src/http_handler.cpp
src/format.cpp
src/metric.cpp
${CMAKE_BINARY_DIR}/json_tokens.cpp) ${CMAKE_BINARY_DIR}/json_tokens.cpp)
add_dependencies(test_server_connection_return generate_json_tokens) add_dependencies(test_server_connection_return generate_json_tokens)
target_link_libraries( target_link_libraries(

View File

@@ -10,6 +10,12 @@
#include <thread> #include <thread>
#include <vector> #include <vector>
metric::Family<metric::Gauge> gauge_family = metric::create_gauge("gauge", "");
metric::Family<metric::Counter> counter_family =
metric::create_counter("counter", "");
metric::Family<metric::Histogram> histogram_family = metric::create_histogram(
"histogram", "", metric::exponential_buckets(0.001, 5, 8));
// High-contention benchmark setup // High-contention benchmark setup
struct ContentionEnvironment { struct ContentionEnvironment {
// Background threads for contention // Background threads for contention
@@ -20,13 +26,6 @@ struct ContentionEnvironment {
std::unique_ptr<std::latch> contention_latch; std::unique_ptr<std::latch> contention_latch;
std::unique_ptr<std::latch> render_latch; std::unique_ptr<std::latch> render_latch;
metric::Family<metric::Gauge> gauge_family =
metric::create_gauge("gauge", "");
metric::Family<metric::Counter> counter_family =
metric::create_counter("counter", "");
metric::Family<metric::Histogram> histogram_family = metric::create_histogram(
"histogram", "", metric::exponential_buckets(0.001, 5, 7));
ContentionEnvironment() = default; ContentionEnvironment() = default;
void start_background_contention(int num_threads = 4) { void start_background_contention(int num_threads = 4) {
@@ -94,13 +93,6 @@ int main() {
ankerl::nanobench::Bench bench; ankerl::nanobench::Bench bench;
bench.title("WeaselDB Metrics Performance").unit("operation").warmup(1000); bench.title("WeaselDB Metrics Performance").unit("operation").warmup(1000);
metric::Family<metric::Gauge> gauge_family =
metric::create_gauge("gauge", "");
metric::Family<metric::Counter> counter_family =
metric::create_counter("counter", "");
metric::Family<metric::Histogram> histogram_family = metric::create_histogram(
"histogram", "", metric::exponential_buckets(0.001, 5, 7));
auto counter = counter_family.create({}); auto counter = counter_family.create({});
auto gauge = gauge_family.create({}); auto gauge = gauge_family.create({});
auto histogram = histogram_family.create({}); auto histogram = histogram_family.create({});
@@ -176,7 +168,8 @@ int main() {
auto gauge_family = metric::create_gauge("scale_gauge", "Scale gauge"); auto gauge_family = metric::create_gauge("scale_gauge", "Scale gauge");
auto histogram_family = metric::create_histogram( auto histogram_family = metric::create_histogram(
"scale_histogram", "Scale histogram", "scale_histogram", "Scale histogram",
std::initializer_list<double>{0.1, 0.5, 1.0, 2.5, 5.0, 10.0, 25.0}); std::initializer_list<double>{0.1, 0.5, 1.0, 2.5, 5.0, 10.0, 25.0,
50.0});
// Create varying numbers of metrics // Create varying numbers of metrics
for (int scale : {10, 100, 1000}) { for (int scale : {10, 100, 1000}) {

View File

@@ -154,6 +154,26 @@ A high-performance, multi-stage, lock-free pipeline for inter-thread communicati
- **Builder pattern** for constructing commit requests - **Builder pattern** for constructing commit requests
- **String views** pointing to arena-allocated memory to avoid unnecessary copying - **String views** pointing to arena-allocated memory to avoid unnecessary copying
#### **Metrics System** (`src/metric.{hpp,cpp}`)
**High-Performance Metrics Implementation:**
- **Thread-local counters/histograms** with single writer for performance
- **Global gauges** with lock-free atomic CAS operations for multi-writer scenarios
- **SIMD-optimized histogram bucket updates** using AVX instructions for high throughput
- **Arena allocator integration** for efficient memory management during rendering
**Threading Model:**
- **Counters**: Per-thread storage, single writer, atomic write in `Counter::inc()`, atomic read in render thread
- **Histograms**: Per-thread storage, single writer, per-histogram mutex serializes all access (observe and render)
- **Gauges**: Lock-free atomic operations using `std::bit_cast` for double precision
- **Thread cleanup**: Automatic accumulation of thread-local state into global state on destruction
**Prometheus Compatibility:**
- **Standard metric types** with proper label handling and validation
- **Bucket generation helpers** for linear/exponential histogram distributions
- **Callback-based metrics** for dynamic values
- **UTF-8 validation** using simdutf for label values
#### **Configuration & Optimization** #### **Configuration & Optimization**
**Configuration System** (`src/config.{hpp,cpp}`): **Configuration System** (`src/config.{hpp,cpp}`):

View File

@@ -637,6 +637,14 @@ template <typename T> struct ArenaVector {
T &operator[](size_t index) { return data_[index]; } T &operator[](size_t index) { return data_[index]; }
const T &operator[](size_t index) const { return data_[index]; } const T &operator[](size_t index) const { return data_[index]; }
void clear() { size_ = 0; }
// Iterator support for range-based for loops
T *begin() { return data_; }
const T *begin() const { return data_; }
T *end() { return data_ + size_; }
const T *end() const { return data_ + size_; }
// No destructor - arena cleanup handles memory // No destructor - arena cleanup handles memory
private: private:

View File

@@ -1,10 +1,19 @@
#include "http_handler.hpp" #include "http_handler.hpp"
#include "arena_allocator.hpp"
#include "perfetto_categories.hpp"
#include <cstring> #include <cstring>
#include <string> #include <string>
#include <strings.h> #include <strings.h>
#include "arena_allocator.hpp"
#include "format.hpp"
#include "metric.hpp"
#include "perfetto_categories.hpp"
auto requests_counter_family = metric::create_counter(
"weaseldb_http_requests_total", "Total http requests");
thread_local auto metrics_counter =
requests_counter_family.create({{"path", "/metrics"}});
// HttpConnectionState implementation // HttpConnectionState implementation
HttpConnectionState::HttpConnectionState(ArenaAllocator &arena) HttpConnectionState::HttpConnectionState(ArenaAllocator &arena)
: current_header_field_buf(ArenaStlAllocator<char>(&arena)), : current_header_field_buf(ArenaStlAllocator<char>(&arena)),
@@ -227,10 +236,44 @@ void HttpHandler::handleDeleteRetention(Connection &conn,
void HttpHandler::handleGetMetrics(Connection &conn, void HttpHandler::handleGetMetrics(Connection &conn,
const HttpConnectionState &state) { const HttpConnectionState &state) {
// TODO: Implement metrics collection and formatting metrics_counter.inc();
sendResponse(conn, 200, "text/plain", ArenaAllocator &arena = conn.get_arena();
"# WeaselDB metrics\nweaseldb_requests_total 0\n", auto metrics_span = metric::render(arena);
state.connection_close);
// Calculate total size for the response body
size_t total_size = 0;
for (const auto &sv : metrics_span) {
total_size += sv.size();
}
auto *http_state = static_cast<HttpConnectionState *>(conn.user_data);
// Build HTTP response headers using arena
std::string_view headers;
if (state.connection_close) {
headers = static_format(
arena, "HTTP/1.1 200 OK\r\n",
"Content-Type: text/plain; version=0.0.4\r\n",
"Content-Length: ", static_cast<uint64_t>(total_size), "\r\n",
"X-Response-ID: ", static_cast<int64_t>(http_state->request_id), "\r\n",
"Connection: close\r\n", "\r\n");
conn.close_after_send();
} else {
headers = static_format(
arena, "HTTP/1.1 200 OK\r\n",
"Content-Type: text/plain; version=0.0.4\r\n",
"Content-Length: ", static_cast<uint64_t>(total_size), "\r\n",
"X-Response-ID: ", static_cast<int64_t>(http_state->request_id), "\r\n",
"Connection: keep-alive\r\n", "\r\n");
}
// Send headers
conn.append_message(headers, false);
// Send body in chunks
for (const auto &sv : metrics_span) {
conn.append_message(sv, false);
}
} }
void HttpHandler::handleGetOk(Connection &conn, void HttpHandler::handleGetOk(Connection &conn,

File diff suppressed because it is too large Load Diff

View File

@@ -38,7 +38,6 @@
#include <functional> #include <functional>
#include <initializer_list> #include <initializer_list>
#include <span> #include <span>
#include <string>
#include <type_traits> #include <type_traits>
#include <vector> #include <vector>
@@ -124,24 +123,44 @@ template <class T> struct Family {
static_assert(std::is_same_v<T, Counter> || std::is_same_v<T, Gauge> || static_assert(std::is_same_v<T, Counter> || std::is_same_v<T, Gauge> ||
std::is_same_v<T, Histogram>); std::is_same_v<T, Histogram>);
// Create metric instance with specific labels // Create metric instance with specific labels.
// Labels are sorted by key for Prometheus compatibility // For performance, it is recommended to create instances once and cache them
// ERROR: Will abort if labels already registered via register_callback() // for reuse, rather than calling .create() repeatedly in
// OK: Multiple calls with same labels return same instance (idempotent) // performance-critical paths.
T create(std::vector<std::pair<std::string, std::string>> labels); //
// Labels are sorted by key for Prometheus compatibility.
// ERROR: Will abort if labels already registered via register_callback().
// OK: Multiple calls with same labels return same instance (idempotent).
T create(std::initializer_list<std::pair<std::string_view, std::string_view>>
labels) {
return create(
std::span<const std::pair<std::string_view, std::string_view>>(
labels.begin(), labels.end()));
}
T create(
std::span<const std::pair<std::string_view, std::string_view>> labels);
// Register callback-based metric (Counter and Gauge only) // Register callback-based metric (Counter and Gauge only)
// Validates that label set isn't already taken // Validates that label set isn't already taken
void void register_callback(
register_callback(std::vector<std::pair<std::string, std::string>> labels, std::initializer_list<std::pair<std::string_view, std::string_view>>
MetricCallback<T> callback); labels,
MetricCallback<T> callback) {
register_callback(
std::span<const std::pair<std::string_view, std::string_view>>(
labels.begin(), labels.end()),
callback);
}
void register_callback(
std::span<const std::pair<std::string_view, std::string_view>> labels,
MetricCallback<T> callback);
private: private:
Family(); Family();
friend struct Metric; friend struct Metric;
friend Family<Counter> create_counter(std::string, std::string); friend Family<Counter> create_counter(std::string_view, std::string_view);
friend Family<Gauge> create_gauge(std::string, std::string); friend Family<Gauge> create_gauge(std::string_view, std::string_view);
friend Family<Histogram> create_histogram(std::string, std::string, friend Family<Histogram> create_histogram(std::string_view, std::string_view,
std::span<const double>); std::span<const double>);
struct State; struct State;
@@ -149,24 +168,29 @@ private:
}; };
// Factory functions for creating metric families // Factory functions for creating metric families
// IMPORTANT: name and help must point to static memory (string literals)
// Create counter family (monotonically increasing values) // Create counter family (monotonically increasing values)
// ERROR: Aborts if family with same name is registered with different help // ERROR: Aborts if family with same name is registered with different help
// text. // text.
Family<Counter> create_counter(std::string name, std::string help); Family<Counter> create_counter(std::string_view name, std::string_view help);
// Create gauge family (can increase/decrease) // Create gauge family (can increase/decrease)
// ERROR: Aborts if family with same name is registered with different help // ERROR: Aborts if family with same name is registered with different help
// text. // text.
Family<Gauge> create_gauge(std::string name, std::string help); Family<Gauge> create_gauge(std::string_view name, std::string_view help);
// Create histogram family with custom buckets // Create histogram family with custom buckets
// Buckets will be sorted, deduplicated, and +Inf will be added automatically // Buckets will be sorted, deduplicated, and +Inf will be added automatically
// ERROR: Aborts if family with same name is registered with different help text // ERROR: Aborts if family with same name is registered with different help text
// or buckets. // or buckets.
Family<Histogram> create_histogram(std::string name, std::string help, Family<Histogram> create_histogram(std::string_view name, std::string_view help,
std::span<const double> buckets); std::span<const double> buckets);
inline Family<Histogram>
create_histogram(std::string_view name, std::string_view help,
std::initializer_list<double> buckets) {
return create_histogram(
name, help, std::span<const double>(buckets.begin(), buckets.end()));
}
// Helper functions for generating standard histogram buckets // Helper functions for generating standard histogram buckets
// Following Prometheus client library conventions // Following Prometheus client library conventions
@@ -184,17 +208,17 @@ std::vector<double> exponential_buckets(double start, double factor, int count);
// Returns chunks of Prometheus exposition format (includes # HELP and # TYPE // Returns chunks of Prometheus exposition format (includes # HELP and # TYPE
// lines) Each string_view may contain multiple lines separated by '\n' String // lines) Each string_view may contain multiple lines separated by '\n' String
// views are NOT null-terminated - use .size() for length All string data // views are NOT null-terminated - use .size() for length All string data
// allocated in provided arena for zero-copy efficiency // allocated in provided arena for zero-copy efficiency. The caller is
// TODO: Implement Prometheus text exposition format // responsible for the arena's lifecycle. THREAD SAFETY: Serialized by global
// THREAD SAFETY: Serialized by global mutex - callbacks need not be thread-safe // mutex - callbacks need not be thread-safe
std::span<std::string_view> render(ArenaAllocator &arena); std::span<std::string_view> render(ArenaAllocator &arena);
// Validation functions for Prometheus compatibility // Validation functions for Prometheus compatibility
bool is_valid_metric_name(const std::string &name); bool is_valid_metric_name(std::string_view name);
bool is_valid_label_key(const std::string &key); bool is_valid_label_key(std::string_view key);
bool is_valid_label_value(const std::string &value); bool is_valid_label_value(std::string_view value);
// Note: Histograms do not support callbacks due to their multi-value nature // Note: Histograms do not support callbacks due to their multi-value nature
// (buckets + sum + count). Use static histogram metrics only. // (buckets + sum + count). Use static histogram metrics only.
} // namespace metric } // namespace metric

View File

@@ -483,6 +483,113 @@ TEST_CASE("thread safety") {
} }
} }
TEST_CASE("thread counter cleanup bug") {
SUBCASE(
"counter and histogram values should persist after thread destruction") {
auto counter_family = metric::create_counter(
"thread_cleanup_counter", "Counter for thread cleanup test");
auto histogram_family = metric::create_histogram(
"thread_cleanup_histogram", "Histogram for thread cleanup test",
metric::linear_buckets(0.0, 1.0, 5)); // buckets: 0, 1, 2, 3, 4
// Variables to collect actual values from worker thread
double counter_value_in_thread = 0;
double histogram_sum_in_thread = 0;
// Create thread that increments metrics and then exits
std::thread worker([&]() {
auto counter = counter_family.create({{"worker", "cleanup_test"}});
auto histogram = histogram_family.create({{"worker", "cleanup_test"}});
counter.inc(1.0);
histogram.observe(1.5); // Should contribute to sum
// Measure actual values from within the thread (before ThreadInit
// destructor runs)
ArenaAllocator thread_arena;
auto thread_output = metric::render(thread_arena);
for (const auto &line : thread_output) {
if (line.find("thread_cleanup_counter{worker=\"cleanup_test\"}") !=
std::string_view::npos) {
auto space_pos = line.rfind(' ');
if (space_pos != std::string_view::npos) {
auto value_str = line.substr(space_pos + 1);
if (value_str.back() == '\n') {
value_str.remove_suffix(1);
}
counter_value_in_thread = std::stod(std::string(value_str));
}
}
if (line.find(
"thread_cleanup_histogram_sum{worker=\"cleanup_test\"}") !=
std::string_view::npos) {
auto space_pos = line.rfind(' ');
if (space_pos != std::string_view::npos) {
auto value_str = line.substr(space_pos + 1);
if (value_str.back() == '\n') {
value_str.remove_suffix(1);
}
histogram_sum_in_thread = std::stod(std::string(value_str));
}
}
}
});
// Wait for thread to complete and destroy (triggering ThreadInit
// destructor)
worker.join();
// Measure values after thread cleanup
ArenaAllocator arena;
auto output = metric::render(arena);
double counter_value_after = 0;
double histogram_sum_after = 0;
for (const auto &line : output) {
if (line.find("thread_cleanup_counter{worker=\"cleanup_test\"}") !=
std::string_view::npos) {
auto space_pos = line.rfind(' ');
if (space_pos != std::string_view::npos) {
auto value_str = line.substr(space_pos + 1);
if (value_str.back() == '\n') {
value_str.remove_suffix(1);
}
counter_value_after = std::stod(std::string(value_str));
}
}
if (line.find("thread_cleanup_histogram_sum{worker=\"cleanup_test\"}") !=
std::string_view::npos) {
auto space_pos = line.rfind(' ');
if (space_pos != std::string_view::npos) {
auto value_str = line.substr(space_pos + 1);
if (value_str.back() == '\n') {
value_str.remove_suffix(1);
}
histogram_sum_after = std::stod(std::string(value_str));
}
}
}
// Values should have been captured correctly within the thread
CHECK(counter_value_in_thread == 1.0);
CHECK(histogram_sum_in_thread == 1.5);
// The bug: These values should persist after thread cleanup but will be
// lost because ThreadInit destructor erases per-thread state without
// accumulating values
CHECK(counter_value_after == 1.0);
CHECK(histogram_sum_after == 1.5);
// The bug: After thread destruction, the counter and histogram values are
// lost because ThreadInit::~ThreadInit() calls
// family->perThreadState.erase(thread_id) without accumulating the values
// into global storage first. This causes counter values to "go backwards"
// when threads are destroyed, violating the monotonic property of counters.
}
}
TEST_CASE("error conditions") { TEST_CASE("error conditions") {
SUBCASE("counter negative increment") { SUBCASE("counter negative increment") {
auto counter_family = metric::create_counter("error_counter", "Error test"); auto counter_family = metric::create_counter("error_counter", "Error test");