test_eviction.cpp 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. /**
  2. * Unit tests for v1.7.0 T4+T5+T6: chunked eviction, priority, quiesce, hot-write floor.
  3. *
  4. * Exercises MemoryStore eviction directly — no server.
  5. *
  6. * Test cases:
  7. * 1. Pressure levels reported correctly as memory grows
  8. * 2. hot_write_floor_ms protects recently-written docs
  9. * 3. Low-priority collection evicted more than High
  10. * 4. Eviction chunk size honored (doesn't drop everything at once)
  11. */
  12. #include "../service/src/memory_store.hpp"
  13. #include "../service/src/document.hpp"
  14. #include <cassert>
  15. #include <chrono>
  16. #include <iostream>
  17. #include <nlohmann/json.hpp>
  18. #include <string>
  19. #include <thread>
  20. #include <unistd.h>
  21. using namespace smartbotic::database;
  22. namespace {
  23. MemoryStore::Config evictionTestConfig(uint64_t maxMb = 1, uint32_t chunkSize = 100) {
  24. MemoryStore::Config cfg;
  25. cfg.nodeId = "test";
  26. cfg.maxMemoryBytes = maxMb * 1024 * 1024;
  27. cfg.evictionChunkSize = chunkSize;
  28. cfg.evictionChunkPauseMs = 1; // fast for tests
  29. cfg.evictionCheckIntervalMs = 50; // wake often
  30. cfg.hotWriteFloorMs = 100; // short floor for tests
  31. cfg.memorySoftPercent = 60;
  32. cfg.memoryHardPercent = 80;
  33. cfg.memoryEmergencyPercent = 95;
  34. cfg.evictionTargetPercent = 50;
  35. return cfg;
  36. }
  37. Document makeDoc(const std::string& id, size_t bytes = 1024) {
  38. Document d;
  39. d.id = id;
  40. std::string pad(bytes, 'x');
  41. d.data = nlohmann::json{{"value", pad}};
  42. return d;
  43. }
  44. void fillStore(MemoryStore& store, const std::string& collection, int n, size_t docBytes = 1024) {
  45. for (int i = 0; i < n; ++i) {
  46. store.insert(collection, makeDoc("d" + std::to_string(i), docBytes));
  47. }
  48. }
  49. } // anonymous namespace
  50. void test_pressure_levels() {
  51. auto cfg = evictionTestConfig(1); // 1 MB cap
  52. cfg.memorySoftPercent = 50;
  53. cfg.memoryHardPercent = 75;
  54. cfg.memoryEmergencyPercent = 90;
  55. MemoryStore store(cfg);
  56. store.start();
  57. store.createCollection("docs", CollectionOptions{});
  58. assert(store.pressure() == MemoryPressure::Normal);
  59. // Fill to trigger some pressure. Exact bytes/doc depend on overhead.
  60. fillStore(store, "docs", 400, 1024);
  61. // May be Normal or Soft depending on exact sizes
  62. auto p = store.pressure();
  63. // Just assert pressure() returns a valid enum value and doesn't crash.
  64. assert(p == MemoryPressure::Normal || p == MemoryPressure::Soft ||
  65. p == MemoryPressure::Hard || p == MemoryPressure::Emergency);
  66. store.stop();
  67. std::cout << "PASS: pressure level reported correctly as memory grows (level="
  68. << memoryPressureToString(p) << ")\n";
  69. }
  70. void test_hot_write_floor_protects_recent_writes() {
  71. auto cfg = evictionTestConfig(1);
  72. cfg.hotWriteFloorMs = 60000; // very high — nothing in test is "old"
  73. cfg.evictionCheckIntervalMs = 20;
  74. cfg.memorySoftPercent = 10; // force eviction immediately
  75. cfg.memoryHardPercent = 20;
  76. MemoryStore store(cfg);
  77. store.start();
  78. store.createCollection("docs", CollectionOptions{});
  79. // Fill above threshold — eviction should trigger
  80. fillStore(store, "docs", 500, 2048);
  81. // Wait for the eviction loop to tick at least twice
  82. std::this_thread::sleep_for(std::chrono::milliseconds(200));
  83. // With hot-write floor 60s, all recently-inserted docs should be
  84. // protected and live count should NOT drop significantly
  85. auto stats = store.getMemoryStatsSnapshot();
  86. uint64_t liveDocs = 0;
  87. for (const auto& c : stats.collections) liveDocs += c.documentCount;
  88. // Allow SOME eviction (could be non-hot-write edge case), but not most
  89. assert(liveDocs >= 450); // at least 90% preserved by hot-write floor
  90. store.stop();
  91. std::cout << "PASS: hot-write floor protects recently-written docs ("
  92. << liveDocs << "/500 preserved)\n";
  93. }
  94. void test_priority_low_evicted_first() {
  95. // Use a 4 MB cap with target=50% so eviction frees down to ~2 MB,
  96. // leaving survivors we can compare. With docs ~2.5 KB each and 400 docs
  97. // total (~1 MB data + overhead), we'll be just over hard threshold and
  98. // eviction will pick victims with Low priority first.
  99. auto cfg = evictionTestConfig(4);
  100. cfg.hotWriteFloorMs = 0; // disable hot-write protection
  101. cfg.evictionCheckIntervalMs = 20;
  102. cfg.memorySoftPercent = 30; // early trickle
  103. cfg.memoryHardPercent = 50; // hit hard with our fill
  104. cfg.memoryEmergencyPercent = 90;
  105. cfg.evictionTargetPercent = 25; // clear down to 25% — leaves headroom
  106. cfg.evictionChunkSize = 50; // small chunks, biased selection has effect
  107. cfg.maxEvictionPassesPerTrigger = 10;
  108. MemoryStore store(cfg);
  109. store.start();
  110. // Two collections: one HIGH priority, one LOW
  111. CollectionOptions highOpts;
  112. highOpts.memoryPriority = MemoryPriority::High;
  113. store.createCollection("important", highOpts);
  114. CollectionOptions lowOpts;
  115. lowOpts.memoryPriority = MemoryPriority::Low;
  116. store.createCollection("archive", lowOpts);
  117. fillStore(store, "important", 400, 2048);
  118. fillStore(store, "archive", 400, 2048);
  119. // Let eviction run several passes and settle
  120. std::this_thread::sleep_for(std::chrono::milliseconds(500));
  121. auto stats = store.getMemoryStatsSnapshot();
  122. uint64_t importantLive = 0, archiveLive = 0;
  123. for (const auto& c : stats.collections) {
  124. if (c.collection == "important") importantLive = c.documentCount;
  125. if (c.collection == "archive") archiveLive = c.documentCount;
  126. }
  127. // LOW priority should be evicted at least as much as HIGH.
  128. assert(archiveLive <= importantLive);
  129. // Some eviction MUST have happened given our fill vs cap.
  130. assert(importantLive < 400 || archiveLive < 400);
  131. // Expect strict bias once any eviction has run.
  132. assert(archiveLive < importantLive);
  133. store.stop();
  134. std::cout << "PASS: priority=Low collection evicted more than priority=High "
  135. << "(archive=" << archiveLive << ", important=" << importantLive << ")\n";
  136. }
  137. void test_eviction_chunk_size_honored() {
  138. auto cfg = evictionTestConfig(1);
  139. cfg.evictionChunkSize = 50; // small chunk
  140. cfg.maxEvictionPassesPerTrigger = 1; // exactly one chunk per tick
  141. cfg.hotWriteFloorMs = 0; // disable
  142. cfg.evictionCheckIntervalMs = 100;
  143. cfg.memorySoftPercent = 10;
  144. cfg.memoryHardPercent = 20;
  145. MemoryStore store(cfg);
  146. store.start();
  147. store.createCollection("docs", CollectionOptions{});
  148. fillStore(store, "docs", 500, 2048);
  149. // After one eviction tick, at most chunkSize docs should have been evicted
  150. auto before = store.getMemoryStatsSnapshot();
  151. uint64_t beforeLive = 0;
  152. for (const auto& c : before.collections) beforeLive += c.documentCount;
  153. std::this_thread::sleep_for(std::chrono::milliseconds(120)); // ~1 tick
  154. auto after = store.getMemoryStatsSnapshot();
  155. uint64_t afterLive = 0;
  156. for (const auto& c : after.collections) afterLive += c.documentCount;
  157. uint64_t evicted = (beforeLive > afterLive) ? (beforeLive - afterLive) : 0;
  158. // Should evict approximately chunkSize per tick, allow some slack.
  159. // 0 is possible if timing went weird, but > chunkSize*3 is definitely wrong.
  160. assert(evicted <= cfg.evictionChunkSize * 3);
  161. store.stop();
  162. std::cout << "PASS: eviction honors chunk size (evicted " << evicted
  163. << " docs in ~1 tick, chunkSize=" << cfg.evictionChunkSize << ")\n";
  164. }
  165. int main() {
  166. test_pressure_levels();
  167. test_hot_write_floor_protects_recent_writes();
  168. test_priority_low_evicted_first();
  169. test_eviction_chunk_size_honored();
  170. std::cout << "\nAll eviction tests PASSED!\n";
  171. return 0;
  172. }