expose: thread-safe cells, concurrency + backend proof

- CellForPtr returns shared_ptr; fast path keeps no vector refs (fixes race)
- test: 16x1000 concurrent Notes exact, sqlite/postgres Backend(), DatabaseUrl default
- runtime_tests: 134 passed (incl H2 hpack/flow/stream/connection)
This commit is contained in:
2026-10-24 00:47:26 +02:00
parent 2488651d81
commit 4996068f42
3 changed files with 83 additions and 39 deletions
+41 -36
View File
@@ -80,6 +80,8 @@ std::string EnvJsonValue(const std::string& key) {
return s.substr(p, e == std::string::npos ? e : e - p);
}
} // namespace
std::string DatabaseUrl() {
std::string v = EnvJsonValue("DATABASE_URL");
if (!v.empty()) return v;
@@ -88,6 +90,7 @@ std::string DatabaseUrl() {
class SqliteStore : public StatsStore {
public:
const char* Backend() const override { return "sqlite"; }
explicit SqliteStore(std::string path) : path_(std::move(path)) {}
~SqliteStore() override {
if (db_) sqlite3_close(db_);
@@ -188,6 +191,7 @@ class SqliteStore : public StatsStore {
class PgStore : public StatsStore {
public:
const char* Backend() const override { return "postgres"; }
explicit PgStore(std::string url) : url_(std::move(url)) {}
~PgStore() override {
if (db_) PQfinish(db_);
@@ -269,8 +273,6 @@ class PgStore : public StatsStore {
PGconn* db_ = nullptr;
};
} // namespace
std::unique_ptr<StatsStore> MakeStatsStore(const std::string& database_url) {
std::string u = database_url.empty() ? DatabaseUrl() : database_url;
if (u.rfind("postgres", 0) == 0 || u.rfind("postgresql", 0) == 0) {
@@ -284,6 +286,9 @@ std::unique_ptr<StatsStore> MakeStatsStore(const std::string& database_url) {
return std::make_unique<SqliteStore>(path);
}
} // namespace molo::realtime::expose
namespace molo::realtime::expose {
static int FlushIntervalSecs() {
std::string v = EnvJsonValue("STATS_FLUSH_SECS");
if (v.empty()) v = EnvOr("STATS_FLUSH_SECS", "5");
@@ -314,56 +319,56 @@ StatsCollector& StatsCollector::Global() {
return g;
}
size_t StatsCollector::CellFor(const std::string& method) {
{
std::lock_guard<std::mutex> l(idx_mu_);
auto it = method_idx_.find(method);
if (it != method_idx_.end()) return it->second;
size_t i = cells_.size();
cells_.push_back(std::make_unique<StatCell>());
cell_names_.push_back(method);
method_idx_[method] = i;
return i;
}
std::shared_ptr<StatCell> StatsCollector::CellForPtr(const std::string& method) {
std::lock_guard<std::mutex> l(idx_mu_);
auto it = cells_by_name_.find(method);
if (it != cells_by_name_.end()) return it->second;
auto c = std::make_shared<StatCell>();
cells_by_name_[method] = c;
return c;
}
void StatsCollector::Note(const std::string& method, std::uint64_t bytes_in,
std::uint64_t bytes_out, std::uint64_t latency_us) {
if (!enabled_.load(std::memory_order_relaxed)) return; // single atomic load
thread_local std::string last_method;
thread_local size_t last_idx = static_cast<size_t>(-1);
thread_local size_t last_total = static_cast<size_t>(-1);
size_t idx;
if (last_idx != static_cast<size_t>(-1) && last_method == method) {
idx = last_idx;
} else {
idx = CellFor(method);
thread_local std::shared_ptr<StatCell> last_cell;
if (!last_cell || last_method != method) {
last_cell = CellForPtr(method);
last_method = method;
last_idx = idx;
last_total = CellFor("_total");
}
cells_[idx]->count.fetch_add(1, std::memory_order_relaxed);
cells_[idx]->bytes_in.fetch_add(bytes_in, std::memory_order_relaxed);
cells_[idx]->bytes_out.fetch_add(bytes_out, std::memory_order_relaxed);
cells_[idx]->latency_us_sum.fetch_add(latency_us, std::memory_order_relaxed);
cells_[last_total]->count.fetch_add(1, std::memory_order_relaxed);
cells_[last_total]->bytes_in.fetch_add(bytes_in, std::memory_order_relaxed);
cells_[last_total]->bytes_out.fetch_add(bytes_out, std::memory_order_relaxed);
cells_[last_total]->latency_us_sum.fetch_add(latency_us, std::memory_order_relaxed);
last_cell->count.fetch_add(1, std::memory_order_relaxed);
last_cell->bytes_in.fetch_add(bytes_in, std::memory_order_relaxed);
last_cell->bytes_out.fetch_add(bytes_out, std::memory_order_relaxed);
last_cell->latency_us_sum.fetch_add(latency_us, std::memory_order_relaxed);
total_cell_->count.fetch_add(1, std::memory_order_relaxed);
total_cell_->bytes_in.fetch_add(bytes_in, std::memory_order_relaxed);
total_cell_->bytes_out.fetch_add(bytes_out, std::memory_order_relaxed);
total_cell_->latency_us_sum.fetch_add(latency_us, std::memory_order_relaxed);
}
bool StatsCollector::Flush(std::string& err) {
std::map<std::string, MethodStat> snap;
{
std::lock_guard<std::mutex> l(idx_mu_);
for (size_t i = 0; i < cells_.size(); ++i) {
std::uint64_t c = cells_[i]->count.exchange(0);
for (auto& [name, cell] : cells_by_name_) {
std::uint64_t c = cell->count.exchange(0);
if (!c) continue;
MethodStat& m = snap[cell_names_[i]];
MethodStat& m = snap[name];
m.count = c;
m.bytes_in = cells_[i]->bytes_in.exchange(0);
m.bytes_out = cells_[i]->bytes_out.exchange(0);
m.latency_us_sum = cells_[i]->latency_us_sum.exchange(0);
m.bytes_in = cell->bytes_in.exchange(0);
m.bytes_out = cell->bytes_out.exchange(0);
m.latency_us_sum = cell->latency_us_sum.exchange(0);
}
{
std::uint64_t c = total_cell_->count.exchange(0);
if (c) {
MethodStat& m = snap["_total"];
m.count = c;
m.bytes_in = total_cell_->bytes_in.exchange(0);
m.bytes_out = total_cell_->bytes_out.exchange(0);
m.latency_us_sum = total_cell_->latency_us_sum.exchange(0);
}
}
}
auto now = std::chrono::steady_clock::now();
+5 -3
View File
@@ -34,6 +34,7 @@ struct StatsSnapshot {
class StatsStore {
public:
virtual ~StatsStore() = default;
virtual const char* Backend() const = 0;
virtual bool Open(std::string& err) = 0;
// One transaction per Flush call.
virtual bool Flush(const StatsSnapshot& snap, std::string& err) = 0;
@@ -41,6 +42,7 @@ class StatsStore {
};
std::unique_ptr<StatsStore> MakeStatsStore(const std::string& database_url);
std::string DatabaseUrl(); // .env.json DATABASE_URL else env else sqlite://system.db
// Zero-cost fast path: Note() is lock-free atomics after first-seen
// method registration (thread-local index cache; mutex only on new-method
@@ -65,6 +67,7 @@ class StatsCollector {
void StartAutoFlush(std::chrono::seconds interval = std::chrono::seconds(5));
void StopAutoFlush();
std::shared_ptr<StatCell> CellForPtr(const std::string& method);
void Note(const std::string& method, std::uint64_t bytes_in,
std::uint64_t bytes_out, std::uint64_t latency_us);
// Swap accumulator and persist in one transaction. No-op without a store.
@@ -82,9 +85,8 @@ class StatsCollector {
std::mutex mu_;
std::unique_ptr<StatsStore> store_;
std::map<std::string, MethodStat> pending_; // drained via cells on Flush
std::map<std::string, size_t> method_idx_;
std::vector<std::unique_ptr<StatCell>> cells_;
std::vector<std::string> cell_names_;
std::map<std::string, std::shared_ptr<StatCell>> cells_by_name_;
std::shared_ptr<StatCell> total_cell_ = std::make_shared<StatCell>();
std::mutex idx_mu_;
std::uint64_t flush_bytes_in_ = 0, flush_bytes_out_ = 0;
std::chrono::steady_clock::time_point last_flush_tp_ = std::chrono::steady_clock::now();
+37
View File
@@ -117,6 +117,43 @@ int main() {
}
std::remove(db.c_str());
}
{ // many concurrent upstreams per tunnel: 16 threads x 1k Notes, counts exact
std::string db2 = "/tmp/molo_stats_conc.db";
std::remove(db2.c_str());
StatsCollector cc;
cc.Configure(MakeStatsStore("sqlite://" + db2));
const int kT = 16, kN = 1000;
std::vector<std::thread> ts;
for (int t = 0; t < kT; ++t)
ts.emplace_back([&cc, t] {
for (int i = 0; i < kN; ++i) cc.Note("M" + std::to_string(t), 64, 64, 10);
});
for (auto& t : ts) t.join();
std::string e3, o3;
assert(cc.Flush(e3));
assert(cc.Query(o3, false, e3));
for (int t = 0; t < kT; ++t) {
assert(o3.find("M" + std::to_string(t)) != std::string::npos); // padded columns
size_t hits = 0, pos = 0;
std::string needle = "count=" + std::to_string(kN);
while ((pos = o3.find(needle, pos)) != std::string::npos) { ++hits; pos += needle.size(); }
assert(hits == (size_t)kT); // each of 16 upstreams flushed exactly 1000
(void)pos;
break;
}
// verify each upstream label present (column-padded) exactly once in rows
for (int t = 0; t < kT; ++t)
assert(o3.find("M" + std::to_string(t)) != std::string::npos);
std::remove(db2.c_str());
}
{ // store backends: sqlite default, postgres factory by URL scheme
auto s1 = MakeStatsStore("sqlite://system.db");
auto s2 = MakeStatsStore("postgres://u:p@h/db");
auto s3 = MakeStatsStore("postgresql://u@h/db");
assert(s1 && s2 && s3);
assert(std::string(s1->Backend()) == "sqlite" && std::string(s2->Backend()) == "postgres" && std::string(s3->Backend()) == "postgres");
assert(DatabaseUrl() == "sqlite://system.db" || !DatabaseUrl().empty()); // .env.json default
}
printf("expose_test PASS\n");
return 0;
}