test_snapshot_durability.cpp 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. /**
  2. * Integration tests for v1.6.1 snapshot durability + fallback.
  3. *
  4. * Exercises SnapshotManager directly (no gRPC server).
  5. *
  6. * Test cases:
  7. * 1. atomic write leaves no .tmp file behind
  8. * 2. snapshot round-trip (write then load back)
  9. * 3. post-write verification catches externally-truncated file
  10. * 4. loadWithFallback uses older snapshot when newest is corrupt
  11. * 5. orphaned .tmp files cleaned by listSnapshots
  12. * 6. loadWithFallback throws when all snapshots corrupt
  13. */
  14. #include "../service/src/persistence/snapshot.hpp"
  15. #include "../service/src/memory_store.hpp"
  16. #include "../service/src/document.hpp"
  17. #include <cassert>
  18. #include <chrono>
  19. #include <cstdint>
  20. #include <filesystem>
  21. #include <fstream>
  22. #include <iostream>
  23. #include <nlohmann/json.hpp>
  24. #include <thread>
  25. #include <unistd.h> // getpid
  26. namespace fs = std::filesystem;
  27. using smartbotic::database::MemoryStore;
  28. using smartbotic::database::SnapshotManager;
  29. using smartbotic::database::Document;
  30. using smartbotic::database::CollectionOptions;
  31. namespace {
  32. MemoryStore::Config defaultStoreConfig() {
  33. MemoryStore::Config cfg;
  34. cfg.nodeId = "test";
  35. cfg.maxMemoryBytes = 256ULL * 1024 * 1024;
  36. return cfg;
  37. }
  38. fs::path makeSnapshotDir(const std::string& name) {
  39. auto dir = fs::temp_directory_path() /
  40. ("snap-durability-" + name + "-" + std::to_string(::getpid()));
  41. fs::remove_all(dir);
  42. fs::create_directories(dir);
  43. return dir;
  44. }
  45. Document makeDoc(const std::string& id) {
  46. Document d;
  47. d.id = id;
  48. d.data = nlohmann::json{{"value", id}};
  49. return d;
  50. }
  51. } // anonymous namespace
  52. // ──────────────────────────────────────────────────────────
  53. // Test 1: atomic write leaves no .tmp file behind
  54. // ──────────────────────────────────────────────────────────
  55. void test_atomic_write_leaves_no_tmp() {
  56. auto dir = makeSnapshotDir("atomic");
  57. SnapshotManager::Config scfg;
  58. scfg.snapshotDir = dir;
  59. scfg.validateAfterWrite = true;
  60. SnapshotManager mgr(scfg);
  61. MemoryStore store(defaultStoreConfig());
  62. store.start();
  63. store.createCollection("docs", CollectionOptions{});
  64. for (int i = 0; i < 50; i++) {
  65. store.insert("docs", makeDoc("d" + std::to_string(i)));
  66. }
  67. auto path = mgr.createSnapshot(store, 42);
  68. // No .tmp files should remain
  69. int tmpCount = 0;
  70. for (auto& e : fs::directory_iterator(dir)) {
  71. if (e.path().extension() == ".tmp") tmpCount++;
  72. }
  73. assert(tmpCount == 0);
  74. // The final file should exist and pass verification
  75. assert(fs::exists(path));
  76. assert(mgr.verifySnapshot(path));
  77. store.stop();
  78. fs::remove_all(dir);
  79. std::cout << "PASS: atomic write leaves no .tmp file behind\n";
  80. }
  81. // ──────────────────────────────────────────────────────────
  82. // Test 2: snapshot round-trips (write then load back)
  83. // ──────────────────────────────────────────────────────────
  84. void test_snapshot_round_trip() {
  85. auto dir = makeSnapshotDir("roundtrip");
  86. SnapshotManager::Config scfg;
  87. scfg.snapshotDir = dir;
  88. SnapshotManager mgr(scfg);
  89. MemoryStore store(defaultStoreConfig());
  90. store.start();
  91. store.createCollection("docs", CollectionOptions{});
  92. for (int i = 0; i < 20; i++) {
  93. store.insert("docs", makeDoc("d" + std::to_string(i)));
  94. }
  95. auto path = mgr.createSnapshot(store, 99);
  96. store.stop();
  97. MemoryStore store2(defaultStoreConfig());
  98. store2.start();
  99. uint64_t seq = mgr.loadSnapshot(path, store2);
  100. assert(seq == 99);
  101. auto doc = store2.get("docs", "d5");
  102. assert(doc.has_value());
  103. store2.stop();
  104. fs::remove_all(dir);
  105. std::cout << "PASS: snapshot round-trip\n";
  106. }
  107. // ──────────────────────────────────────────────────────────
  108. // Test 3: post-write verification catches externally-truncated file
  109. // ──────────────────────────────────────────────────────────
  110. void test_verification_catches_truncation() {
  111. auto dir = makeSnapshotDir("truncate");
  112. SnapshotManager::Config scfg;
  113. scfg.snapshotDir = dir;
  114. SnapshotManager mgr(scfg);
  115. MemoryStore store(defaultStoreConfig());
  116. store.start();
  117. store.createCollection("docs", CollectionOptions{});
  118. for (int i = 0; i < 50; i++) {
  119. store.insert("docs", makeDoc("d" + std::to_string(i)));
  120. }
  121. auto path = mgr.createSnapshot(store, 1);
  122. store.stop();
  123. // Truncate the file AFTER a successful write to simulate the Zoe scenario
  124. uintmax_t original = fs::file_size(path);
  125. fs::resize_file(path, original / 2);
  126. // verifySnapshot should return false
  127. assert(!mgr.verifySnapshot(path));
  128. fs::remove_all(dir);
  129. std::cout << "PASS: post-write verification catches truncation\n";
  130. }
  131. // ──────────────────────────────────────────────────────────
  132. // Test 4: loadWithFallback uses older snapshot when newest is corrupt
  133. // ──────────────────────────────────────────────────────────
  134. void test_loadWithFallback_uses_older_when_newest_corrupt() {
  135. auto dir = makeSnapshotDir("fallback");
  136. SnapshotManager::Config scfg;
  137. scfg.snapshotDir = dir;
  138. scfg.maxSnapshots = 10;
  139. SnapshotManager mgr(scfg);
  140. MemoryStore store(defaultStoreConfig());
  141. store.start();
  142. store.createCollection("docs", CollectionOptions{});
  143. // Older snapshot — 10 docs
  144. for (int i = 0; i < 10; i++) {
  145. store.insert("docs", makeDoc("d" + std::to_string(i)));
  146. }
  147. auto older = mgr.createSnapshot(store, 100);
  148. // Filenames include timestamps down to the second, so sleep to ensure ordering
  149. std::this_thread::sleep_for(std::chrono::seconds(1));
  150. // Newer snapshot — 20 docs
  151. for (int i = 10; i < 20; i++) {
  152. store.insert("docs", makeDoc("d" + std::to_string(i)));
  153. }
  154. auto newer = mgr.createSnapshot(store, 200);
  155. store.stop();
  156. assert(older != newer);
  157. // Corrupt the newer
  158. fs::resize_file(newer, fs::file_size(newer) / 2);
  159. assert(!mgr.verifySnapshot(newer));
  160. // loadWithFallback should transparently fall back to the older
  161. MemoryStore store2(defaultStoreConfig());
  162. store2.start();
  163. fs::path used;
  164. size_t attempted = 0;
  165. uint64_t seq = mgr.loadWithFallback(store2, &used, &attempted);
  166. assert(seq == 100);
  167. assert(used == older);
  168. assert(attempted == 2); // tried newer first, then older
  169. store2.stop();
  170. fs::remove_all(dir);
  171. std::cout << "PASS: loadWithFallback uses older snapshot when newest corrupt\n";
  172. }
  173. // ──────────────────────────────────────────────────────────
  174. // Test 5: orphaned .tmp files cleaned by listSnapshots()
  175. // ──────────────────────────────────────────────────────────
  176. void test_orphaned_tmp_cleaned_by_listSnapshots() {
  177. auto dir = makeSnapshotDir("orphan");
  178. SnapshotManager::Config scfg;
  179. scfg.snapshotDir = dir;
  180. SnapshotManager mgr(scfg);
  181. // Manually plant a fake .tmp like a crashed-mid-write would leave
  182. auto tmp = dir / "snapshot-20260101-120000.dat.tmp";
  183. {
  184. std::ofstream f(tmp);
  185. f << "partial";
  186. }
  187. assert(fs::exists(tmp));
  188. // listSnapshots should clean up the .tmp on its pre-scan pass
  189. (void)mgr.listSnapshots();
  190. assert(!fs::exists(tmp));
  191. fs::remove_all(dir);
  192. std::cout << "PASS: orphaned .tmp cleaned by listSnapshots\n";
  193. }
  194. // ──────────────────────────────────────────────────────────
  195. // Test 6: loadWithFallback throws when all snapshots corrupt
  196. // ──────────────────────────────────────────────────────────
  197. void test_loadWithFallback_throws_when_all_corrupt() {
  198. auto dir = makeSnapshotDir("allcorrupt");
  199. SnapshotManager::Config scfg;
  200. scfg.snapshotDir = dir;
  201. scfg.maxSnapshots = 10;
  202. SnapshotManager mgr(scfg);
  203. MemoryStore store(defaultStoreConfig());
  204. store.start();
  205. store.createCollection("docs", CollectionOptions{});
  206. store.insert("docs", makeDoc("d1"));
  207. auto s1 = mgr.createSnapshot(store, 1);
  208. std::this_thread::sleep_for(std::chrono::seconds(1));
  209. store.insert("docs", makeDoc("d2"));
  210. auto s2 = mgr.createSnapshot(store, 2);
  211. store.stop();
  212. // Corrupt both
  213. fs::resize_file(s1, fs::file_size(s1) / 2);
  214. fs::resize_file(s2, fs::file_size(s2) / 2);
  215. MemoryStore store2(defaultStoreConfig());
  216. store2.start();
  217. bool threw = false;
  218. try {
  219. mgr.loadWithFallback(store2);
  220. } catch (const std::exception& e) {
  221. threw = true;
  222. std::string msg = e.what();
  223. assert(msg.find("all") != std::string::npos);
  224. }
  225. assert(threw);
  226. store2.stop();
  227. fs::remove_all(dir);
  228. std::cout << "PASS: loadWithFallback throws when all snapshots corrupt\n";
  229. }
  230. int main() {
  231. test_atomic_write_leaves_no_tmp();
  232. test_snapshot_round_trip();
  233. test_verification_catches_truncation();
  234. test_loadWithFallback_uses_older_when_newest_corrupt();
  235. test_orphaned_tmp_cleaned_by_listSnapshots();
  236. test_loadWithFallback_throws_when_all_corrupt();
  237. std::cout << "\nAll snapshot durability tests PASSED!\n";
  238. return 0;
  239. }