| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288 |
- /**
- * Integration tests for v1.6.1 snapshot durability + fallback.
- *
- * Exercises SnapshotManager directly (no gRPC server).
- *
- * Test cases:
- * 1. atomic write leaves no .tmp file behind
- * 2. snapshot round-trip (write then load back)
- * 3. post-write verification catches externally-truncated file
- * 4. loadWithFallback uses older snapshot when newest is corrupt
- * 5. orphaned .tmp files cleaned by listSnapshots
- * 6. loadWithFallback throws when all snapshots corrupt
- */
- #include "../service/src/persistence/snapshot.hpp"
- #include "../service/src/memory_store.hpp"
- #include "../service/src/document.hpp"
- #include <cassert>
- #include <chrono>
- #include <cstdint>
- #include <filesystem>
- #include <fstream>
- #include <iostream>
- #include <nlohmann/json.hpp>
- #include <thread>
- #include <unistd.h> // getpid
- namespace fs = std::filesystem;
- using smartbotic::database::MemoryStore;
- using smartbotic::database::SnapshotManager;
- using smartbotic::database::Document;
- using smartbotic::database::CollectionOptions;
- namespace {
- MemoryStore::Config defaultStoreConfig() {
- MemoryStore::Config cfg;
- cfg.nodeId = "test";
- cfg.maxMemoryBytes = 256ULL * 1024 * 1024;
- return cfg;
- }
- fs::path makeSnapshotDir(const std::string& name) {
- auto dir = fs::temp_directory_path() /
- ("snap-durability-" + name + "-" + std::to_string(::getpid()));
- fs::remove_all(dir);
- fs::create_directories(dir);
- return dir;
- }
- Document makeDoc(const std::string& id) {
- Document d;
- d.id = id;
- d.data = nlohmann::json{{"value", id}};
- return d;
- }
- } // anonymous namespace
- // ──────────────────────────────────────────────────────────
- // Test 1: atomic write leaves no .tmp file behind
- // ──────────────────────────────────────────────────────────
- void test_atomic_write_leaves_no_tmp() {
- auto dir = makeSnapshotDir("atomic");
- SnapshotManager::Config scfg;
- scfg.snapshotDir = dir;
- scfg.validateAfterWrite = true;
- SnapshotManager mgr(scfg);
- MemoryStore store(defaultStoreConfig());
- store.start();
- store.createCollection("docs", CollectionOptions{});
- for (int i = 0; i < 50; i++) {
- store.insert("docs", makeDoc("d" + std::to_string(i)));
- }
- auto path = mgr.createSnapshot(store, 42);
- // No .tmp files should remain
- int tmpCount = 0;
- for (auto& e : fs::directory_iterator(dir)) {
- if (e.path().extension() == ".tmp") tmpCount++;
- }
- assert(tmpCount == 0);
- // The final file should exist and pass verification
- assert(fs::exists(path));
- assert(mgr.verifySnapshot(path));
- store.stop();
- fs::remove_all(dir);
- std::cout << "PASS: atomic write leaves no .tmp file behind\n";
- }
- // ──────────────────────────────────────────────────────────
- // Test 2: snapshot round-trips (write then load back)
- // ──────────────────────────────────────────────────────────
- void test_snapshot_round_trip() {
- auto dir = makeSnapshotDir("roundtrip");
- SnapshotManager::Config scfg;
- scfg.snapshotDir = dir;
- SnapshotManager mgr(scfg);
- MemoryStore store(defaultStoreConfig());
- store.start();
- store.createCollection("docs", CollectionOptions{});
- for (int i = 0; i < 20; i++) {
- store.insert("docs", makeDoc("d" + std::to_string(i)));
- }
- auto path = mgr.createSnapshot(store, 99);
- store.stop();
- MemoryStore store2(defaultStoreConfig());
- store2.start();
- uint64_t seq = mgr.loadSnapshot(path, store2);
- assert(seq == 99);
- auto doc = store2.get("docs", "d5");
- assert(doc.has_value());
- store2.stop();
- fs::remove_all(dir);
- std::cout << "PASS: snapshot round-trip\n";
- }
- // ──────────────────────────────────────────────────────────
- // Test 3: post-write verification catches externally-truncated file
- // ──────────────────────────────────────────────────────────
- void test_verification_catches_truncation() {
- auto dir = makeSnapshotDir("truncate");
- SnapshotManager::Config scfg;
- scfg.snapshotDir = dir;
- SnapshotManager mgr(scfg);
- MemoryStore store(defaultStoreConfig());
- store.start();
- store.createCollection("docs", CollectionOptions{});
- for (int i = 0; i < 50; i++) {
- store.insert("docs", makeDoc("d" + std::to_string(i)));
- }
- auto path = mgr.createSnapshot(store, 1);
- store.stop();
- // Truncate the file AFTER a successful write to simulate the Zoe scenario
- uintmax_t original = fs::file_size(path);
- fs::resize_file(path, original / 2);
- // verifySnapshot should return false
- assert(!mgr.verifySnapshot(path));
- fs::remove_all(dir);
- std::cout << "PASS: post-write verification catches truncation\n";
- }
- // ──────────────────────────────────────────────────────────
- // Test 4: loadWithFallback uses older snapshot when newest is corrupt
- // ──────────────────────────────────────────────────────────
- void test_loadWithFallback_uses_older_when_newest_corrupt() {
- auto dir = makeSnapshotDir("fallback");
- SnapshotManager::Config scfg;
- scfg.snapshotDir = dir;
- scfg.maxSnapshots = 10;
- SnapshotManager mgr(scfg);
- MemoryStore store(defaultStoreConfig());
- store.start();
- store.createCollection("docs", CollectionOptions{});
- // Older snapshot — 10 docs
- for (int i = 0; i < 10; i++) {
- store.insert("docs", makeDoc("d" + std::to_string(i)));
- }
- auto older = mgr.createSnapshot(store, 100);
- // Filenames include timestamps down to the second, so sleep to ensure ordering
- std::this_thread::sleep_for(std::chrono::seconds(1));
- // Newer snapshot — 20 docs
- for (int i = 10; i < 20; i++) {
- store.insert("docs", makeDoc("d" + std::to_string(i)));
- }
- auto newer = mgr.createSnapshot(store, 200);
- store.stop();
- assert(older != newer);
- // Corrupt the newer
- fs::resize_file(newer, fs::file_size(newer) / 2);
- assert(!mgr.verifySnapshot(newer));
- // loadWithFallback should transparently fall back to the older
- MemoryStore store2(defaultStoreConfig());
- store2.start();
- fs::path used;
- size_t attempted = 0;
- uint64_t seq = mgr.loadWithFallback(store2, &used, &attempted);
- assert(seq == 100);
- assert(used == older);
- assert(attempted == 2); // tried newer first, then older
- store2.stop();
- fs::remove_all(dir);
- std::cout << "PASS: loadWithFallback uses older snapshot when newest corrupt\n";
- }
- // ──────────────────────────────────────────────────────────
- // Test 5: orphaned .tmp files cleaned by listSnapshots()
- // ──────────────────────────────────────────────────────────
- void test_orphaned_tmp_cleaned_by_listSnapshots() {
- auto dir = makeSnapshotDir("orphan");
- SnapshotManager::Config scfg;
- scfg.snapshotDir = dir;
- SnapshotManager mgr(scfg);
- // Manually plant a fake .tmp like a crashed-mid-write would leave
- auto tmp = dir / "snapshot-20260101-120000.dat.tmp";
- {
- std::ofstream f(tmp);
- f << "partial";
- }
- assert(fs::exists(tmp));
- // listSnapshots should clean up the .tmp on its pre-scan pass
- (void)mgr.listSnapshots();
- assert(!fs::exists(tmp));
- fs::remove_all(dir);
- std::cout << "PASS: orphaned .tmp cleaned by listSnapshots\n";
- }
- // ──────────────────────────────────────────────────────────
- // Test 6: loadWithFallback throws when all snapshots corrupt
- // ──────────────────────────────────────────────────────────
- void test_loadWithFallback_throws_when_all_corrupt() {
- auto dir = makeSnapshotDir("allcorrupt");
- SnapshotManager::Config scfg;
- scfg.snapshotDir = dir;
- scfg.maxSnapshots = 10;
- SnapshotManager mgr(scfg);
- MemoryStore store(defaultStoreConfig());
- store.start();
- store.createCollection("docs", CollectionOptions{});
- store.insert("docs", makeDoc("d1"));
- auto s1 = mgr.createSnapshot(store, 1);
- std::this_thread::sleep_for(std::chrono::seconds(1));
- store.insert("docs", makeDoc("d2"));
- auto s2 = mgr.createSnapshot(store, 2);
- store.stop();
- // Corrupt both
- fs::resize_file(s1, fs::file_size(s1) / 2);
- fs::resize_file(s2, fs::file_size(s2) / 2);
- MemoryStore store2(defaultStoreConfig());
- store2.start();
- bool threw = false;
- try {
- mgr.loadWithFallback(store2);
- } catch (const std::exception& e) {
- threw = true;
- std::string msg = e.what();
- assert(msg.find("all") != std::string::npos);
- }
- assert(threw);
- store2.stop();
- fs::remove_all(dir);
- std::cout << "PASS: loadWithFallback throws when all snapshots corrupt\n";
- }
- int main() {
- test_atomic_write_leaves_no_tmp();
- test_snapshot_round_trip();
- test_verification_catches_truncation();
- test_loadWithFallback_uses_older_when_newest_corrupt();
- test_orphaned_tmp_cleaned_by_listSnapshots();
- test_loadWithFallback_throws_when_all_corrupt();
- std::cout << "\nAll snapshot durability tests PASSED!\n";
- return 0;
- }
|