From 7d968fb7a0562f9530bb56b9e81b376afb7ce4cf Mon Sep 17 00:00:00 2001 From: retoor Date: Sat, 24 Oct 2026 00:36:36 +0200 Subject: [PATCH] feat: auto-flush stats timer, .env.json defaults, expose round-trip+timer tests --- .env.json | 4 ++++ src/cli/cli/expose_cli.cpp | 3 ++- src/realtime/expose/stats.cpp | 26 +++++++++++++++++++++++ src/realtime/expose/stats.h | 10 +++++++++ tests/src/realtime/expose/expose_test.cpp | 6 ++++++ 5 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 .env.json diff --git a/.env.json b/.env.json new file mode 100644 index 0000000..81915de --- /dev/null +++ b/.env.json @@ -0,0 +1,4 @@ +{ + "DATABASE_URL": "sqlite://system.db", + "STATS_FLUSH_SECS": "5" +} diff --git a/src/cli/cli/expose_cli.cpp b/src/cli/cli/expose_cli.cpp index 8338258..69eb3a3 100644 --- a/src/cli/cli/expose_cli.cpp +++ b/src/cli/cli/expose_cli.cpp @@ -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); diff --git a/src/realtime/expose/stats.cpp b/src/realtime/expose/stats.cpp index 3d640bf..c4f764b 100644 --- a/src/realtime/expose/stats.cpp +++ b/src/realtime/expose/stats.cpp @@ -9,6 +9,7 @@ #include #include #include +#include // Minimal libpq declarations (no dev headers on build hosts); link -lpq. extern "C" { @@ -283,6 +284,31 @@ std::unique_ptr MakeStatsStore(const std::string& database_url) { return std::make_unique(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; diff --git a/src/realtime/expose/stats.h b/src/realtime/expose/stats.h index 9a77433..eae6708 100644 --- a/src/realtime/expose/stats.h +++ b/src/realtime/expose/stats.h @@ -5,7 +5,9 @@ // atomic in-memory accumulator + a pluggable Store backend; flush is // explicit (timer-driven by caller) inside one transaction. #include +#include #include +#include #include #include #include @@ -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 store_; std::map pending_; + std::thread flush_thread_; + std::atomic flush_run_{false}; }; } // namespace molo::realtime::expose diff --git a/tests/src/realtime/expose/expose_test.cpp b/tests/src/realtime/expose/expose_test.cpp index ebe4239..46dffc0 100644 --- a/tests/src/realtime/expose/expose_test.cpp +++ b/tests/src/realtime/expose/expose_test.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -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");