Compare commits
15 Commits
50e27cced8
...
8b828be0a9
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b828be0a9 | |||
| 58649103e5 | |||
| b6809d8700 | |||
| 889109f4ae | |||
| 4b2c5b8ce8 | |||
| 93ccd2eb71 | |||
| b52d6e5a13 | |||
| f560ac1736 | |||
| 4f72840e51 | |||
| ff7642195b | |||
| 0ff197d406 | |||
| affeeb674a | |||
| 21ddcb75fb | |||
| dcf8af6d43 | |||
| 935bab9454 |
@@ -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(
|
||||||
|
|||||||
@@ -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}) {
|
||||||
|
|||||||
20
design.md
20
design.md
@@ -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}`):
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
1064
src/metric.cpp
1064
src/metric.cpp
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
Reference in New Issue
Block a user