yams/tests/unit/common/log_rate_limiter_catch2_test.cpp
trvon bbec4093cd
fix(daemon): rate-limit repeating warnings that flood the daemon log
With 5 x 10 MB rotation the daemon log covered about three hours on the
audited machine, mostly one of a few lines repeated:

- "Failed to submit extraction ...: entity_graph_jobs channel full"
  (11k lines) from GraphComponent, echoed per batch by PostIngestQueue's
  "KG batch failed", plus "Entity channel full, dropping job";
- the per-document "Title+NL extraction for <hash> in N ms" info line;
- session-watcher scan/indexing warnings retried every pass.

Add yams::common::LogRateLimiter, a small lock-free helper that admits
one message per interval and reports how many it held back ("(N similar
messages suppressed)"), and use it at those sites (60 s for the queue
paths, 10 min for the session watcher, including its first-failure
warning per path). The Title+NL timing line is per-document progress
and moves to debug. The drop counters are unchanged, so status still
shows the full numbers.

Signed-off-by: trvon <me@trevon.dev>
2026-09-27 14:00:25 -06:00

70 lines
2.3 KiB
C++

#include <catch2/catch_test_macros.hpp>
#include <yams/common/log_rate_limiter.h>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <thread>
#include <vector>
using yams::common::LogRateLimiter;
using namespace std::chrono_literals;
TEST_CASE("LogRateLimiter lets one message through per interval",
"[common][logging][rate-limit][catch2]") {
LogRateLimiter limiter(30s);
const auto t0 = LogRateLimiter::Clock::time_point{} + 1h;
auto first = limiter.admit(t0);
REQUIRE(first.has_value());
CHECK(*first == 0);
CHECK_FALSE(limiter.admit(t0 + 1s).has_value());
CHECK_FALSE(limiter.admit(t0 + 10s).has_value());
CHECK_FALSE(limiter.admit(t0 + 29s).has_value());
// The next admitted message reports how many were held back.
auto second = limiter.admit(t0 + 30s);
REQUIRE(second.has_value());
CHECK(*second == 3);
CHECK_FALSE(limiter.admit(t0 + 31s).has_value());
auto third = limiter.admit(t0 + 5min);
REQUIRE(third.has_value());
CHECK(*third == 1);
}
TEST_CASE("LogRateLimiter suffix names suppressed messages",
"[common][logging][rate-limit][catch2]") {
CHECK(LogRateLimiter::suppressedSuffix(0).empty());
CHECK(LogRateLimiter::suppressedSuffix(1) == " (1 similar message suppressed)");
CHECK(LogRateLimiter::suppressedSuffix(11000) == " (11000 similar messages suppressed)");
}
TEST_CASE("LogRateLimiter admits exactly one caller per interval under contention",
"[common][logging][rate-limit][catch2]") {
LogRateLimiter limiter(1h);
const auto now = LogRateLimiter::Clock::now();
std::atomic<int> admitted{0};
std::atomic<std::uint64_t> reported{0};
std::vector<std::thread> threads;
for (int i = 0; i < 8; ++i) {
threads.emplace_back([&] {
for (int j = 0; j < 1000; ++j) {
if (auto held = limiter.admit(now)) {
admitted.fetch_add(1);
reported.fetch_add(*held);
}
}
});
}
for (auto& thread : threads) {
thread.join();
}
CHECK(admitted.load() == 1);
// Every held-back occurrence is reported exactly once across the admitted messages.
auto next = limiter.admit(now + 1h);
REQUIRE(next.has_value());
CHECK(reported.load() + *next == 7999);
}