Loading tests/test_cluster_performance.cpp +129 −0 Original line number Diff line number Diff line Loading @@ -630,3 +630,132 @@ TEST_F(ClusterPerformanceTest, ClusterVsFileComparison) { test_unlink(cmpDbPath.c_str()); test_unlink((cmpDbPath + ".lock").c_str()); } // ═════════════════════════════════════════════════════════════ // 6. Prefetch never blocks (non-blocking async retrieve) // // Bug: The old implementation did a synchronous 5s wait on // client->retrieve() inside fetchFromCluster(). Every other // request would hit the 5s timeout, blocking the HTTP thread. // Fix: retrieve is fire-and-forget; the result is picked up // on the next prefetch() call. Each prefetch() must complete // in well under 1 second. // ═════════════════════════════════════════════════════════════ TEST_F(ClusterPerformanceTest, PrefetchNeverBlocks) { constexpr size_t ROUNDS = 20; constexpr double MAX_MS = 1000.0; // must finish in <1s (old bug: 5s) std::vector<double> times_ms; for (size_t i = 0; i < ROUNDS; ++i) { // Each round: fresh ClusterBackend (simulates a new HTTP request). // Force a refresh so it actually attempts a cluster fetch, not just cache. AuthBackend cb(authdb::ClusterStore, "", "prefetch_test_domain"); auto t0 = Clock::now(); cb.lock(); cb.unlock(); auto t1 = Clock::now(); double ms = std::chrono::duration<double, std::milli>(t1 - t0).count(); times_ms.push_back(ms); } double maxTime = *std::max_element(times_ms.begin(), times_ms.end()); double avgTime = std::accumulate(times_ms.begin(), times_ms.end(), 0.0) / times_ms.size(); std::cout << "\n=== Prefetch Non-Blocking Test (" << ROUNDS << " rounds) ===\n" << " avg = " << avgTime << " ms\n" << " max = " << maxTime << " ms (limit: " << MAX_MS << " ms)\n"; // The critical assertion: no single prefetch may block ≥1s. // Before the fix, ~50% would hit the 5s timeout. EXPECT_LT(maxTime, MAX_MS) << "prefetch() blocked for " << maxTime << " ms — async retrieve may be synchronous"; } // ═════════════════════════════════════════════════════════════ // 7. Revision guard prevents stale data overwrite // // Bug: If a recovering cluster node returned a manifest with // an older revision, fetchFromCluster() would blindly replace // the local cache → data loss. // Fix: fetchFromCluster() compares the manifest revision to // the cached revision and discards stale data. // ═════════════════════════════════════════════════════════════ TEST_F(ClusterPerformanceTest, RevisionGuardNoDataLoss) { const std::string domain = "revguard_" + std::to_string(test_getpid()); // Phase 1: Create 10 users via cluster constexpr size_t N = 10; std::vector<uuid::uuid> uids(N); { AuthBackend cb(authdb::ClusterStore, "", domain); User user; for (size_t i = 0; i < N; ++i) { uids[i].generate(); class authdb::UserData udat(uids[i]); udat.setUserName("revuser_" + std::to_string(i)); udat.setPwHash("pw" + std::to_string(i)); user.create(cb, &udat); } } // Small delay to let async push propagate std::this_thread::sleep_for(std::chrono::milliseconds(500)); // Phase 2: Read back revision and user count size_t rev_after_create = 0; size_t user_count = 0; { AuthBackend cb(authdb::ClusterStore, "", domain); cb.lock(); rev_after_create = cb.getRevesion(); User user; std::vector<uuid::uuid> listed; user.list(cb, listed); user_count = listed.size(); cb.unlock(); } EXPECT_EQ(user_count, N) << "Expected " << N << " users but found " << user_count; EXPECT_GT(rev_after_create, 0u) << "Revision should be > 0 after creating users"; // Phase 3: Force multiple rapid prefetch cycles — revision must never decrease size_t min_rev = rev_after_create; size_t max_rev = rev_after_create; size_t min_users = user_count; for (int round = 0; round < 10; ++round) { AuthBackend cb(authdb::ClusterStore, "", domain); cb.lock(); size_t rev = cb.getRevesion(); User user; std::vector<uuid::uuid> listed; user.list(cb, listed); cb.unlock(); if (rev < min_rev) min_rev = rev; if (rev > max_rev) max_rev = rev; if (listed.size() < min_users) min_users = listed.size(); } std::cout << "\n=== Revision Guard Test ===\n" << " rev after create = " << rev_after_create << "\n" << " rev range = [" << min_rev << ", " << max_rev << "]\n" << " min users seen = " << min_users << " (expected " << N << ")\n"; // Revision must never go backward EXPECT_GE(min_rev, rev_after_create) << "Revision went backward: saw " << min_rev << " but expected >= " << rev_after_create << " — stale manifest was accepted"; // Users must never disappear EXPECT_EQ(min_users, N) << "Lost users: saw " << min_users << " but expected " << N << " — stale manifest overwrote cache"; } Loading
tests/test_cluster_performance.cpp +129 −0 Original line number Diff line number Diff line Loading @@ -630,3 +630,132 @@ TEST_F(ClusterPerformanceTest, ClusterVsFileComparison) { test_unlink(cmpDbPath.c_str()); test_unlink((cmpDbPath + ".lock").c_str()); } // ═════════════════════════════════════════════════════════════ // 6. Prefetch never blocks (non-blocking async retrieve) // // Bug: The old implementation did a synchronous 5s wait on // client->retrieve() inside fetchFromCluster(). Every other // request would hit the 5s timeout, blocking the HTTP thread. // Fix: retrieve is fire-and-forget; the result is picked up // on the next prefetch() call. Each prefetch() must complete // in well under 1 second. // ═════════════════════════════════════════════════════════════ TEST_F(ClusterPerformanceTest, PrefetchNeverBlocks) { constexpr size_t ROUNDS = 20; constexpr double MAX_MS = 1000.0; // must finish in <1s (old bug: 5s) std::vector<double> times_ms; for (size_t i = 0; i < ROUNDS; ++i) { // Each round: fresh ClusterBackend (simulates a new HTTP request). // Force a refresh so it actually attempts a cluster fetch, not just cache. AuthBackend cb(authdb::ClusterStore, "", "prefetch_test_domain"); auto t0 = Clock::now(); cb.lock(); cb.unlock(); auto t1 = Clock::now(); double ms = std::chrono::duration<double, std::milli>(t1 - t0).count(); times_ms.push_back(ms); } double maxTime = *std::max_element(times_ms.begin(), times_ms.end()); double avgTime = std::accumulate(times_ms.begin(), times_ms.end(), 0.0) / times_ms.size(); std::cout << "\n=== Prefetch Non-Blocking Test (" << ROUNDS << " rounds) ===\n" << " avg = " << avgTime << " ms\n" << " max = " << maxTime << " ms (limit: " << MAX_MS << " ms)\n"; // The critical assertion: no single prefetch may block ≥1s. // Before the fix, ~50% would hit the 5s timeout. EXPECT_LT(maxTime, MAX_MS) << "prefetch() blocked for " << maxTime << " ms — async retrieve may be synchronous"; } // ═════════════════════════════════════════════════════════════ // 7. Revision guard prevents stale data overwrite // // Bug: If a recovering cluster node returned a manifest with // an older revision, fetchFromCluster() would blindly replace // the local cache → data loss. // Fix: fetchFromCluster() compares the manifest revision to // the cached revision and discards stale data. // ═════════════════════════════════════════════════════════════ TEST_F(ClusterPerformanceTest, RevisionGuardNoDataLoss) { const std::string domain = "revguard_" + std::to_string(test_getpid()); // Phase 1: Create 10 users via cluster constexpr size_t N = 10; std::vector<uuid::uuid> uids(N); { AuthBackend cb(authdb::ClusterStore, "", domain); User user; for (size_t i = 0; i < N; ++i) { uids[i].generate(); class authdb::UserData udat(uids[i]); udat.setUserName("revuser_" + std::to_string(i)); udat.setPwHash("pw" + std::to_string(i)); user.create(cb, &udat); } } // Small delay to let async push propagate std::this_thread::sleep_for(std::chrono::milliseconds(500)); // Phase 2: Read back revision and user count size_t rev_after_create = 0; size_t user_count = 0; { AuthBackend cb(authdb::ClusterStore, "", domain); cb.lock(); rev_after_create = cb.getRevesion(); User user; std::vector<uuid::uuid> listed; user.list(cb, listed); user_count = listed.size(); cb.unlock(); } EXPECT_EQ(user_count, N) << "Expected " << N << " users but found " << user_count; EXPECT_GT(rev_after_create, 0u) << "Revision should be > 0 after creating users"; // Phase 3: Force multiple rapid prefetch cycles — revision must never decrease size_t min_rev = rev_after_create; size_t max_rev = rev_after_create; size_t min_users = user_count; for (int round = 0; round < 10; ++round) { AuthBackend cb(authdb::ClusterStore, "", domain); cb.lock(); size_t rev = cb.getRevesion(); User user; std::vector<uuid::uuid> listed; user.list(cb, listed); cb.unlock(); if (rev < min_rev) min_rev = rev; if (rev > max_rev) max_rev = rev; if (listed.size() < min_users) min_users = listed.size(); } std::cout << "\n=== Revision Guard Test ===\n" << " rev after create = " << rev_after_create << "\n" << " rev range = [" << min_rev << ", " << max_rev << "]\n" << " min users seen = " << min_users << " (expected " << N << ")\n"; // Revision must never go backward EXPECT_GE(min_rev, rev_after_create) << "Revision went backward: saw " << min_rev << " but expected >= " << rev_after_create << " — stale manifest was accepted"; // Users must never disappear EXPECT_EQ(min_users, N) << "Lost users: saw " << min_users << " but expected " << N << " — stale manifest overwrote cache"; }