// ResourceGovernor Integration Tests // Tests that resource governor throttling behavior actually kicks in during real // daemon operations, verifying the integration between: // TuneAdvisor (thresholds) → ResourceGovernor (pressure detection) → Daemon (status) #include #include "test_async_helpers.h" #include "test_daemon_harness.h" #include "../../common/env_compat.h" #include "../../common/test_helpers_catch2.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace yams::daemon; using namespace yams::test; using namespace std::chrono_literals; namespace { /// RAII guard for TuneAdvisor CPU threshold restoration class CpuThresholdGuard { double prev_; public: explicit CpuThresholdGuard(double newThreshold) { prev_ = TuneAdvisor::cpuHighThresholdPercent(); TuneAdvisor::setCpuHighThresholdPercent(newThreshold); } ~CpuThresholdGuard() { TuneAdvisor::setCpuHighThresholdPercent(prev_); } CpuThresholdGuard(const CpuThresholdGuard&) = delete; CpuThresholdGuard& operator=(const CpuThresholdGuard&) = delete; }; using yams::test::ScopedEnvVar; /// Wait for daemon to complete initial setup and governor to be active. /// The governor budget is populated by TuningManager tick, which runs on ~500ms intervals. /// We check the ResourceGovernor singleton directly for reliability. bool waitForGovernorActive(std::chrono::milliseconds timeout) { auto deadline = std::chrono::steady_clock::now() + timeout; while (std::chrono::steady_clock::now() < deadline) { auto& governor = ResourceGovernor::instance(); auto snapshot = governor.getSnapshot(); if (snapshot.memoryBudgetBytes > 0) { return true; } // TuningManager ticks every ~500ms, so don't poll too aggressively std::this_thread::sleep_for(300ms); } return false; } /// Wait for embedding queue to drain (processing complete) bool waitForEmbeddingDrain(DaemonClient& client, std::chrono::milliseconds timeout) { auto deadline = std::chrono::steady_clock::now() + timeout; int stableCount = 0; constexpr int stableRequired = 5; while (std::chrono::steady_clock::now() < deadline) { auto status = yams::cli::run_sync(client.status(), 5s); if (!status) { std::this_thread::sleep_for(500ms); continue; } const auto& st = status.value(); uint64_t embedQueued = 0; uint64_t embedInFlight = 0; uint64_t postQueued = 0; uint64_t postInFlight = 0; if (auto it = st.requestCounts.find("embed_svc_queued"); it != st.requestCounts.end()) { embedQueued = it->second; } if (auto it = st.requestCounts.find("embed_in_flight"); it != st.requestCounts.end()) { embedInFlight = it->second; } if (auto it = st.requestCounts.find("post_ingest_queued"); it != st.requestCounts.end()) { postQueued = it->second; } if (auto it = st.requestCounts.find("post_ingest_inflight"); it != st.requestCounts.end()) { postInFlight = it->second; } const bool embedDrained = (embedQueued == 0 && embedInFlight == 0); const bool postDrained = (postQueued == 0 && postInFlight == 0); if (embedDrained && postDrained) { ++stableCount; if (stableCount >= stableRequired) { return true; } } else { stableCount = 0; } std::this_thread::sleep_for(500ms); } return false; } /// Add a test document via daemon bool addTestDocument(const DaemonHarness& harness, int docId) { const std::string docName = "governor_test_doc_" + std::to_string(docId) + ".txt"; auto path = harness.dataDir() / docName; std::ofstream ofs(path); if (!ofs) { return false; } // Create content that will generate some processing work ofs << "Test document " << docId << " for resource governor integration testing.\n"; ofs << "This document contains content to exercise the ingest pipeline.\n"; for (int i = 0; i < 10; ++i) { ofs << "Line " << i << ": Sample content for processing.\n"; } ofs.close(); yams::app::services::DocumentIngestionService docSvc; yams::app::services::AddOptions opts; opts.socketPath = harness.socketPath().string(); opts.explicitDataDir = harness.dataDir().string(); opts.path = path.string(); opts.noEmbeddings = true; // Skip embeddings for faster tests auto result = docSvc.addViaDaemon(opts); return result.has_value() && !result.value().hash.empty(); } } // namespace // ============================================================================= // Test 1: Verify Governor Metrics Available via Status // ============================================================================= TEST_CASE("ResourceGovernor metrics exposed in daemon status", "[integration][governor][status]") { SKIP_DAEMON_TEST_ON_WINDOWS(); DaemonHarness::Options opts; opts.useMockModelProvider = true; DaemonHarness harness(opts); REQUIRE(harness.start(10s)); ClientConfig clientCfg; clientCfg.socketPath = harness.socketPath(); clientCfg.requestTimeout = 10s; clientCfg.autoStart = false; DaemonClient client(clientCfg); auto connected = yams::cli::run_sync(client.connect(), 5s); REQUIRE(connected.has_value()); // Wait for governor to be active (via singleton check) REQUIRE(waitForGovernorActive(30s)); // Wait a bit longer for FSM metrics to propagate to status response // The TuningManager updates FSM metrics on its tick cycle (~500ms) std::this_thread::sleep_for(1s); auto status = yams::cli::run_sync(client.status(), 5s); REQUIRE(status.has_value()); // Check governor budget via singleton (FSM propagation may be slow) auto& governor = ResourceGovernor::instance(); auto snapshot = governor.getSnapshot(); INFO("governorBudgetBytes (status)=" << status.value().governorBudgetBytes); INFO("governorBudgetBytes (singleton)=" << snapshot.memoryBudgetBytes); CHECK(snapshot.memoryBudgetBytes > 0); INFO("governorPressureLevel=" << static_cast(status.value().governorPressureLevel)); CHECK(status.value().governorPressureLevel <= 3); INFO("governorRssBytes=" << snapshot.rssBytes); CHECK(snapshot.rssBytes > 0); harness.stop(); } // ============================================================================= // Test 2: Verify Governor Singleton Accessibility During Daemon Operation // ============================================================================= TEST_CASE("ResourceGovernor singleton is accessible during daemon operation", "[integration][governor][singleton]") { SKIP_DAEMON_TEST_ON_WINDOWS(); DaemonHarness::Options opts; opts.useMockModelProvider = true; DaemonHarness harness(opts); REQUIRE(harness.start(10s)); auto& governor = ResourceGovernor::instance(); // Test: Governor provides pressure level auto level = governor.getPressureLevel(); INFO("pressureLevel=" << static_cast(level)); CHECK((level == ResourcePressureLevel::Normal || level == ResourcePressureLevel::Warning || level == ResourcePressureLevel::Critical || level == ResourcePressureLevel::Emergency)); // Test: Governor provides scaling caps auto caps = governor.getScalingCaps(); INFO("allowModelLoads=" << caps.allowModelLoads); INFO("allowNewIngest=" << caps.allowNewIngest); // At normal pressure, model loads and ingest should be allowed if (level == ResourcePressureLevel::Normal) { CHECK(caps.allowModelLoads); CHECK(caps.allowNewIngest); } // Test: Governor max concurrency values are positive CHECK(governor.maxIngestWorkers() > 0); CHECK(governor.maxSearchConcurrency() > 0); // Test: Governor admission control is callable bool canWork = governor.canAdmitWork(); bool canLoad = governor.canLoadModel(1024ULL * 1024ULL); // 1 MB bool canScale = governor.canScaleUp("test", 1); INFO("canAdmitWork=" << canWork); INFO("canLoadModel(1MB)=" << canLoad); INFO("canScaleUp(test,1)=" << canScale); // At normal pressure, these should all return true if (level == ResourcePressureLevel::Normal) { CHECK(canWork); CHECK(canLoad); CHECK(canScale); } harness.stop(); } TEST_CASE("ResourceGovernor phase-4 policy surfaces respond during daemon operation", "[integration][governor][policy]") { SKIP_DAEMON_TEST_ON_WINDOWS(); DaemonHarness::Options opts; opts.useMockModelProvider = true; DaemonHarness harness(opts); REQUIRE(harness.start(10s)); REQUIRE(waitForGovernorActive(30s)); auto& governor = ResourceGovernor::instance(); // Normalize singleton policy state before making policy assertions. governor.reportDbLockContention(0, 1); (void)governor.recommendConnectionSlotTarget(16u, 8u, 4u, 64u, 8u, 1); const auto retryBaseline = governor.recommendRetryAfterMs(0, 200, 0, 1024 * 1024, 1, 128, 0, 128, 500); const auto retryOverloaded = governor.recommendRetryAfterMs( 300, 200, 4 * 1024 * 1024, 1024 * 1024, 200, 128, 128, 128, 500); CHECK(retryOverloaded >= retryBaseline); CHECK(retryOverloaded <= 5000); const auto pauseIdle = governor.recommendBackpressureReadPauseMs(100); CHECK(pauseIdle >= 1); CHECK(pauseIdle <= 5000); const auto currentSlots = 16u; const auto shrunk = governor.recommendConnectionSlotTarget(currentSlots, 1, 4, 64, 8, 1); CHECK(shrunk <= currentSlots); CHECK(shrunk >= 4u); governor.reportDbLockContention(100, 10); CHECK(governor.capKgConcurrencyForDbContention(8) <= 2); CHECK(governor.capEmbedConcurrencyForDbContention(8) <= 1); governor.reportDbLockContention(0, 10); CHECK(governor.capKgConcurrencyForDbContention(8) == 8); CHECK(governor.capEmbedConcurrencyForDbContention(4) == 4); // Leave singleton state neutral to reduce inter-test coupling. governor.reportDbLockContention(0, 1); harness.stop(); } // ============================================================================= // Test 3: Verify Pressure Level Detection with Low Threshold // ============================================================================= TEST_CASE("Governor detects pressure with low CPU threshold", "[integration][governor][pressure]") { SKIP_DAEMON_TEST_ON_WINDOWS(); // Set a very low CPU threshold so that normal daemon operation triggers Warning CpuThresholdGuard thresholdGuard(5.0); // 5% - very low threshold DaemonHarness::Options opts; opts.useMockModelProvider = true; DaemonHarness harness(opts); REQUIRE(harness.start(10s)); ClientConfig clientCfg; clientCfg.socketPath = harness.socketPath(); clientCfg.requestTimeout = 10s; clientCfg.autoStart = false; DaemonClient client(clientCfg); auto connected = yams::cli::run_sync(client.connect(), 5s); REQUIRE(connected.has_value()); // Add some documents to create CPU load INFO("Adding test documents to create load..."); for (int i = 0; i < 10; ++i) { REQUIRE(addTestDocument(harness, i)); } // Poll for pressure level change or timeout auto deadline = std::chrono::steady_clock::now() + 30s; bool sawPressure = false; uint8_t maxPressureLevel = 0; while (std::chrono::steady_clock::now() < deadline) { auto status = yams::cli::run_sync(client.status(), 5s); if (status.has_value() && status.value().governorPressureLevel > 0) { sawPressure = true; maxPressureLevel = std::max(maxPressureLevel, status.value().governorPressureLevel); INFO("Detected pressure level: " << static_cast(status.value().governorPressureLevel)); break; } std::this_thread::sleep_for(200ms); } // Note: Pressure detection is non-deterministic - depends on actual CPU usage // On fast machines, we may not see pressure. This test documents whether // throttling CAN work under load, not that it ALWAYS triggers. INFO("sawPressure=" << sawPressure << ", maxPressureLevel=" << static_cast(maxPressureLevel)); // Wait for processing to complete waitForEmbeddingDrain(client, 30s); harness.stop(); } // ============================================================================= // Test 4: Verify Governor Scaling Caps Adjust Under Pressure // ============================================================================= TEST_CASE("Governor scaling caps adjust based on pressure level", "[integration][governor][scaling]") { SKIP_DAEMON_TEST_ON_WINDOWS(); DaemonHarness::Options opts; opts.useMockModelProvider = true; DaemonHarness harness(opts); REQUIRE(harness.start(10s)); auto& governor = ResourceGovernor::instance(); // Record caps at Normal level auto normalLevel = governor.getPressureLevel(); auto normalMaxIngest = governor.maxIngestWorkers(); auto normalMaxSearch = governor.maxSearchConcurrency(); auto normalMaxEmbed = governor.maxEmbedConcurrency(); INFO("At level=" << static_cast(normalLevel)); INFO("normalMaxIngest=" << normalMaxIngest); INFO("normalMaxSearch=" << normalMaxSearch); INFO("normalMaxEmbed=" << normalMaxEmbed); // These caps should be reasonable positive values CHECK(normalMaxIngest > 0); CHECK(normalMaxSearch > 0); // Note: maxEmbed can be 0 at Emergency level harness.stop(); } // ============================================================================= // Test 5: Verify Status Response Contains Governor Metrics After Ingest // ============================================================================= TEST_CASE("Status response contains governor metrics during ingest", "[integration][governor][ingest]") { SKIP_DAEMON_TEST_ON_WINDOWS(); DaemonHarness::Options opts; opts.useMockModelProvider = true; DaemonHarness harness(opts); REQUIRE(harness.start(10s)); ClientConfig clientCfg; clientCfg.socketPath = harness.socketPath(); clientCfg.requestTimeout = 10s; clientCfg.autoStart = false; DaemonClient client(clientCfg); auto connected = yams::cli::run_sync(client.connect(), 5s); REQUIRE(connected.has_value()); REQUIRE(waitForGovernorActive(30s)); // Add documents while checking status std::vector pressureLevels; std::vector budgetValues; for (int i = 0; i < 5; ++i) { REQUIRE(addTestDocument(harness, i)); // Check status after each add auto status = yams::cli::run_sync(client.status(), 5s); if (status) { pressureLevels.push_back(status.value().governorPressureLevel); budgetValues.push_back(status.value().governorBudgetBytes); } } // Verify we got consistent governor metrics throughout INFO("Captured " << pressureLevels.size() << " status readings"); CHECK(!pressureLevels.empty()); CHECK(!budgetValues.empty()); // Budget should remain consistent (it's a configured value) if (budgetValues.size() >= 2) { for (size_t i = 1; i < budgetValues.size(); ++i) { CHECK(budgetValues[i] == budgetValues[0]); } } // Wait for processing to complete waitForEmbeddingDrain(client, 30s); harness.stop(); } // ============================================================================= // Test 6: Verify Governor Snapshot Contains Valid Metrics // ============================================================================= TEST_CASE("ResourceGovernor snapshot contains valid metrics", "[integration][governor][snapshot]") { SKIP_DAEMON_TEST_ON_WINDOWS(); DaemonHarness::Options opts; opts.useMockModelProvider = true; DaemonHarness harness(opts); REQUIRE(harness.start(10s)); // Wait for governor to be ticked at least once REQUIRE(waitForGovernorActive(30s)); auto& governor = ResourceGovernor::instance(); auto snapshot = governor.getSnapshot(); // Test: Memory metrics are populated INFO("rssBytes=" << snapshot.rssBytes); INFO("memoryBudgetBytes=" << snapshot.memoryBudgetBytes); INFO("memoryPressure=" << snapshot.memoryPressure); CHECK(snapshot.rssBytes > 0); CHECK(snapshot.memoryBudgetBytes > 0); // Test: Timestamp is recent auto now = std::chrono::steady_clock::now(); auto age = std::chrono::duration_cast(now - snapshot.timestamp); INFO("snapshotAge=" << age.count() << "s"); CHECK(age.count() < 10); // Test: Pressure level matches snapshot auto level = governor.getPressureLevel(); CHECK(snapshot.level == level); harness.stop(); } // ============================================================================= // Test 7: Verify Concurrent Status Queries Return Consistent Governor Data // ============================================================================= TEST_CASE("Concurrent status queries return consistent governor data", "[integration][governor][concurrent]") { SKIP_DAEMON_TEST_ON_WINDOWS(); DaemonHarness::Options opts; opts.useMockModelProvider = true; DaemonHarness harness(opts); REQUIRE(harness.start(10s)); ClientConfig clientCfg; clientCfg.socketPath = harness.socketPath(); clientCfg.requestTimeout = 10s; clientCfg.autoStart = false; DaemonClient client(clientCfg); auto connected = yams::cli::run_sync(client.connect(), 5s); REQUIRE(connected.has_value()); REQUIRE(waitForGovernorActive(30s)); // Issue multiple concurrent status queries // This test verifies thread safety of concurrent DaemonClient::status() calls. // Prior to the fix in asio_connection.cpp, this would crash with SIGSEGV. std::atomic successCount{0}; std::atomic errorCount{0}; std::vector budgetValues; std::mutex valuesMutex; auto worker = [&]() { auto status = yams::cli::run_sync(client.status(), 5s); if (status.has_value()) { ++successCount; // governorBudgetBytes may be 0 if TuningManager hasn't updated // FsmMetricsRegistry yet - that's OK, we're testing thread safety auto budget = status.value().governorBudgetBytes; if (budget > 0) { std::lock_guard lock(valuesMutex); budgetValues.push_back(budget); } } else { ++errorCount; } }; std::vector threads; constexpr int numQueries = 8; threads.reserve(numQueries); for (int i = 0; i < numQueries; ++i) { threads.emplace_back(worker); } for (auto& t : threads) { t.join(); } INFO("successCount=" << successCount << ", errorCount=" << errorCount); // All concurrent status queries should succeed (no crashes, no errors) CHECK(successCount == numQueries); CHECK(errorCount == 0); // If any budget values were captured, they should all be consistent if (budgetValues.size() >= 2) { for (size_t i = 1; i < budgetValues.size(); ++i) { CHECK(budgetValues[i] == budgetValues[0]); } } harness.stop(); }