/** * Unit tests for v1.7.0 T4+T5+T6: chunked eviction, priority, quiesce, hot-write floor. * * Exercises MemoryStore eviction directly — no server. * * Test cases: * 1. Pressure levels reported correctly as memory grows * 2. hot_write_floor_ms protects recently-written docs * 3. Low-priority collection evicted more than High * 4. Eviction chunk size honored (doesn't drop everything at once) */ #include "../service/src/memory_store.hpp" #include "../service/src/document.hpp" #include #include #include #include #include #include #include using namespace smartbotic::database; namespace { MemoryStore::Config evictionTestConfig(uint64_t maxMb = 1, uint32_t chunkSize = 100) { MemoryStore::Config cfg; cfg.nodeId = "test"; cfg.maxMemoryBytes = maxMb * 1024 * 1024; cfg.evictionChunkSize = chunkSize; cfg.evictionChunkPauseMs = 1; // fast for tests cfg.evictionCheckIntervalMs = 50; // wake often cfg.hotWriteFloorMs = 100; // short floor for tests cfg.memorySoftPercent = 60; cfg.memoryHardPercent = 80; cfg.memoryEmergencyPercent = 95; cfg.evictionTargetPercent = 50; return cfg; } Document makeDoc(const std::string& id, size_t bytes = 1024) { Document d; d.id = id; std::string pad(bytes, 'x'); d.data = nlohmann::json{{"value", pad}}; return d; } void fillStore(MemoryStore& store, const std::string& collection, int n, size_t docBytes = 1024) { for (int i = 0; i < n; ++i) { store.insert(collection, makeDoc("d" + std::to_string(i), docBytes)); } } } // anonymous namespace void test_pressure_levels() { auto cfg = evictionTestConfig(1); // 1 MB cap cfg.memorySoftPercent = 50; cfg.memoryHardPercent = 75; cfg.memoryEmergencyPercent = 90; MemoryStore store(cfg); store.start(); store.createCollection("docs", CollectionOptions{}); assert(store.pressure() == MemoryPressure::Normal); // Fill to trigger some pressure. Exact bytes/doc depend on overhead. fillStore(store, "docs", 400, 1024); // May be Normal or Soft depending on exact sizes auto p = store.pressure(); // Just assert pressure() returns a valid enum value and doesn't crash. assert(p == MemoryPressure::Normal || p == MemoryPressure::Soft || p == MemoryPressure::Hard || p == MemoryPressure::Emergency); store.stop(); std::cout << "PASS: pressure level reported correctly as memory grows (level=" << memoryPressureToString(p) << ")\n"; } void test_hot_write_floor_protects_recent_writes() { auto cfg = evictionTestConfig(1); cfg.hotWriteFloorMs = 60000; // very high — nothing in test is "old" cfg.evictionCheckIntervalMs = 20; cfg.memorySoftPercent = 10; // force eviction immediately cfg.memoryHardPercent = 20; MemoryStore store(cfg); store.start(); store.createCollection("docs", CollectionOptions{}); // Fill above threshold — eviction should trigger fillStore(store, "docs", 500, 2048); // Wait for the eviction loop to tick at least twice std::this_thread::sleep_for(std::chrono::milliseconds(200)); // With hot-write floor 60s, all recently-inserted docs should be // protected and live count should NOT drop significantly auto stats = store.getMemoryStatsSnapshot(); uint64_t liveDocs = 0; for (const auto& c : stats.collections) liveDocs += c.documentCount; // Allow SOME eviction (could be non-hot-write edge case), but not most assert(liveDocs >= 450); // at least 90% preserved by hot-write floor store.stop(); std::cout << "PASS: hot-write floor protects recently-written docs (" << liveDocs << "/500 preserved)\n"; } void test_priority_low_evicted_first() { // Use a 4 MB cap with target=50% so eviction frees down to ~2 MB, // leaving survivors we can compare. With docs ~2.5 KB each and 400 docs // total (~1 MB data + overhead), we'll be just over hard threshold and // eviction will pick victims with Low priority first. auto cfg = evictionTestConfig(4); cfg.hotWriteFloorMs = 0; // disable hot-write protection cfg.evictionCheckIntervalMs = 20; cfg.memorySoftPercent = 30; // early trickle cfg.memoryHardPercent = 50; // hit hard with our fill cfg.memoryEmergencyPercent = 90; cfg.evictionTargetPercent = 25; // clear down to 25% — leaves headroom cfg.evictionChunkSize = 50; // small chunks, biased selection has effect cfg.maxEvictionPassesPerTrigger = 10; MemoryStore store(cfg); store.start(); // Two collections: one HIGH priority, one LOW CollectionOptions highOpts; highOpts.memoryPriority = MemoryPriority::High; store.createCollection("important", highOpts); CollectionOptions lowOpts; lowOpts.memoryPriority = MemoryPriority::Low; store.createCollection("archive", lowOpts); fillStore(store, "important", 400, 2048); fillStore(store, "archive", 400, 2048); // Let eviction run several passes and settle std::this_thread::sleep_for(std::chrono::milliseconds(500)); auto stats = store.getMemoryStatsSnapshot(); uint64_t importantLive = 0, archiveLive = 0; for (const auto& c : stats.collections) { if (c.collection == "important") importantLive = c.documentCount; if (c.collection == "archive") archiveLive = c.documentCount; } // LOW priority should be evicted at least as much as HIGH. assert(archiveLive <= importantLive); // Some eviction MUST have happened given our fill vs cap. assert(importantLive < 400 || archiveLive < 400); // Expect strict bias once any eviction has run. assert(archiveLive < importantLive); store.stop(); std::cout << "PASS: priority=Low collection evicted more than priority=High " << "(archive=" << archiveLive << ", important=" << importantLive << ")\n"; } void test_eviction_chunk_size_honored() { auto cfg = evictionTestConfig(1); cfg.evictionChunkSize = 50; // small chunk cfg.maxEvictionPassesPerTrigger = 1; // exactly one chunk per tick cfg.hotWriteFloorMs = 0; // disable cfg.evictionCheckIntervalMs = 100; cfg.memorySoftPercent = 10; cfg.memoryHardPercent = 20; MemoryStore store(cfg); store.start(); store.createCollection("docs", CollectionOptions{}); fillStore(store, "docs", 500, 2048); // After one eviction tick, at most chunkSize docs should have been evicted auto before = store.getMemoryStatsSnapshot(); uint64_t beforeLive = 0; for (const auto& c : before.collections) beforeLive += c.documentCount; std::this_thread::sleep_for(std::chrono::milliseconds(120)); // ~1 tick auto after = store.getMemoryStatsSnapshot(); uint64_t afterLive = 0; for (const auto& c : after.collections) afterLive += c.documentCount; uint64_t evicted = (beforeLive > afterLive) ? (beforeLive - afterLive) : 0; // Should evict approximately chunkSize per tick, allow some slack. // 0 is possible if timing went weird, but > chunkSize*3 is definitely wrong. assert(evicted <= cfg.evictionChunkSize * 3); store.stop(); std::cout << "PASS: eviction honors chunk size (evicted " << evicted << " docs in ~1 tick, chunkSize=" << cfg.evictionChunkSize << ")\n"; } int main() { test_pressure_levels(); test_hot_write_floor_protects_recent_writes(); test_priority_low_evicted_first(); test_eviction_chunk_size_honored(); std::cout << "\nAll eviction tests PASSED!\n"; return 0; }