feat: auto-flush stats timer, .env.json defaults, expose round-trip+timer tests

This commit is contained in:
2026-10-24 00:36:36 +02:00
parent 0bd9dcd584
commit 7d968fb7a0
5 changed files with 48 additions and 1 deletions
+4
View File
@@ -0,0 +1,4 @@
{
"DATABASE_URL": "sqlite://system.db",
"STATS_FLUSH_SECS": "5"
}
+2 -1
View File
@@ -83,7 +83,8 @@ ExitCode RunExposeServe(const ParsedArgs& args, Output& out) {
auto& sc = realtime::expose::StatsCollector::Global();
sc.Configure(realtime::expose::MakeStatsStore(""));
std::string serr;
(void)sc.Flush(serr); // ensure schema exists; batch flushes follow on timer
(void)sc.Flush(serr); // ensure schema exists
sc.StartAutoFlush(); // time-based persisted flush (STATS_FLUSH_SECS, default 5s)
molo::app::views::View::SetObserver([](const std::string& m, std::uint64_t bi,
std::uint64_t bo, std::uint64_t us) {
realtime::expose::StatsCollector::Global().Note(m, bi, bo, us);
+26
View File
@@ -9,6 +9,7 @@
#include <cstdlib>
#include <cstring>
#include <sstream>
#include <thread>
// Minimal libpq declarations (no dev headers on build hosts); link -lpq.
extern "C" {
@@ -283,6 +284,31 @@ std::unique_ptr<StatsStore> MakeStatsStore(const std::string& database_url) {
return std::make_unique<SqliteStore>(path);
}
static int FlushIntervalSecs() {
std::string v = EnvJsonValue("STATS_FLUSH_SECS");
if (v.empty()) v = EnvOr("STATS_FLUSH_SECS", "5");
try { int n = std::stoi(v); return n > 0 ? n : 5; } catch (...) { return 5; }
}
void StatsCollector::StartAutoFlush(std::chrono::seconds interval) {
StopAutoFlush();
if (interval.count() <= 0) interval = std::chrono::seconds(FlushIntervalSecs());
flush_run_.store(true);
flush_thread_ = std::thread([this, interval] {
while (flush_run_.load()) {
std::this_thread::sleep_for(interval);
if (!flush_run_.load()) break;
std::string err;
Flush(err);
}
});
}
void StatsCollector::StopAutoFlush() {
flush_run_.store(false);
if (flush_thread_.joinable()) flush_thread_.join();
}
StatsCollector& StatsCollector::Global() {
static StatsCollector g;
return g;
+10
View File
@@ -5,7 +5,9 @@
// atomic in-memory accumulator + a pluggable Store backend; flush is
// explicit (timer-driven by caller) inside one transaction.
#include <atomic>
#include <chrono>
#include <cstdint>
#include <thread>
#include <map>
#include <memory>
#include <mutex>
@@ -49,6 +51,12 @@ class StatsCollector {
}
bool enabled() const { return enabled_.load(std::memory_order_relaxed); }
void set_enabled(bool v) { enabled_.store(v, std::memory_order_relaxed); }
~StatsCollector() { StopAutoFlush(); }
// Time-based persisted flush: background thread calls Flush() every
// interval (default 5s; STATS_FLUSH_SECS in .env.json/env overrides).
// Idempotent; safe to call twice.
void StartAutoFlush(std::chrono::seconds interval = std::chrono::seconds(5));
void StopAutoFlush();
void Note(const std::string& method, std::uint64_t bytes_in,
std::uint64_t bytes_out, std::uint64_t latency_us);
@@ -61,6 +69,8 @@ class StatsCollector {
std::mutex mu_;
std::unique_ptr<StatsStore> store_;
std::map<std::string, MethodStat> pending_;
std::thread flush_thread_;
std::atomic<bool> flush_run_{false};
};
} // namespace molo::realtime::expose
@@ -1,4 +1,5 @@
#include <cassert>
#include <chrono>
#include <cstdio>
#include <thread>
#include <vector>
@@ -91,6 +92,11 @@ int main() {
assert(sc.Query(out, false, err));
assert(out.find("GET") != std::string::npos && out.find("count=1") != std::string::npos);
assert(sc.Query(out, true, err) && out.front() == '[');
sc.StartAutoFlush(std::chrono::seconds(1));
sc.Note("POST", 1024, 1024, 1000);
std::this_thread::sleep_for(std::chrono::milliseconds(2500));
sc.StopAutoFlush();
assert(sc.Query(out, false, err) && out.find("POST") != std::string::npos);
std::remove(db.c_str());
}
printf("expose_test PASS\n");