snapshot.cpp 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886
  1. #include "snapshot.hpp"
  2. #include "wal.hpp"
  3. #include <algorithm>
  4. #include <cerrno>
  5. #include <chrono>
  6. #include <cstring>
  7. #include <fstream>
  8. #include <iomanip>
  9. #include <sstream>
  10. #include <system_error>
  11. #include <fcntl.h>
  12. #include <unistd.h>
  13. #include <spdlog/spdlog.h>
  14. #ifndef DISABLE_LZ4
  15. #include <lz4.h>
  16. #endif
  17. namespace smartbotic::database {
  18. namespace {
  19. // Decompress a v4-chunked LZ4 body produced by createSnapshot.
  20. // Body layout: sequence of [u32 uncomp_size, u32 comp_size, LZ4 bytes...] terminated
  21. // by a (0, 0) end marker. Returns the full concatenated uncompressed payload.
  22. std::vector<uint8_t> decompressChunked(const std::vector<uint8_t>& body,
  23. uint64_t uncompressedSize) {
  24. #ifndef DISABLE_LZ4
  25. std::vector<uint8_t> out;
  26. out.reserve(uncompressedSize);
  27. size_t offset = 0;
  28. while (offset + 8 <= body.size()) {
  29. uint32_t uncompU32 = 0;
  30. uint32_t compU32 = 0;
  31. std::memcpy(&uncompU32, body.data() + offset, 4);
  32. std::memcpy(&compU32, body.data() + offset + 4, 4);
  33. offset += 8;
  34. if (uncompU32 == 0 && compU32 == 0) {
  35. break; // end marker
  36. }
  37. if (offset + compU32 > body.size()) {
  38. throw std::runtime_error("Snapshot chunk extends past body");
  39. }
  40. size_t outStart = out.size();
  41. out.resize(outStart + uncompU32);
  42. int decompressed = LZ4_decompress_safe(
  43. reinterpret_cast<const char*>(body.data() + offset),
  44. reinterpret_cast<char*>(out.data() + outStart),
  45. static_cast<int>(compU32),
  46. static_cast<int>(uncompU32)
  47. );
  48. if (decompressed < 0 || static_cast<uint32_t>(decompressed) != uncompU32) {
  49. throw std::runtime_error("Snapshot chunk decompression failed");
  50. }
  51. offset += compU32;
  52. }
  53. if (out.size() != uncompressedSize) {
  54. throw std::runtime_error("Decompressed size mismatch: expected " +
  55. std::to_string(uncompressedSize) + " got " +
  56. std::to_string(out.size()));
  57. }
  58. return out;
  59. #else
  60. (void)body;
  61. (void)uncompressedSize;
  62. throw std::runtime_error("LZ4 disabled — cannot decompress chunked snapshot body");
  63. #endif
  64. }
  65. } // namespace
  66. SnapshotManager::SnapshotManager(Config config)
  67. : config_(std::move(config)) {
  68. // Create snapshot directory if it doesn't exist
  69. std::error_code ec;
  70. std::filesystem::create_directories(config_.snapshotDir, ec);
  71. }
  72. std::filesystem::path SnapshotManager::createSnapshot(const MemoryStore& store, uint64_t walSequence) {
  73. // Serialize store data
  74. uint64_t docCount = 0;
  75. uint32_t collCount = 0;
  76. std::vector<uint8_t> uncompressedData = serializeStore(store, docCount, collCount);
  77. const uint64_t originalUncompressedSize = uncompressedData.size();
  78. // Build chunked body with streaming compression.
  79. // v4 body layout: sequence of [u32 uncompressed_size, u32 compressed_size, bytes...]
  80. // terminated by a (0, 0) end marker. Peak memory per chunk is ~16 MB output buffer
  81. // instead of LZ4_compressBound(full DB) which used to be ~3 GB for production sizes.
  82. constexpr size_t CHUNK_SIZE = 16 * 1024 * 1024; // 16 MB uncompressed per chunk
  83. std::vector<uint8_t> bodyData;
  84. uint32_t compressionType = 0;
  85. if (config_.compressionEnabled) {
  86. #ifndef DISABLE_LZ4
  87. compressionType = 1;
  88. // Rough estimate to reduce reallocation thrash; actual size depends on payload compressibility.
  89. bodyData.reserve(uncompressedData.size() / 2 + CHUNK_SIZE);
  90. size_t offset = 0;
  91. while (offset < uncompressedData.size()) {
  92. size_t thisChunk = std::min(CHUNK_SIZE, uncompressedData.size() - offset);
  93. int compressBound = LZ4_compressBound(static_cast<int>(thisChunk));
  94. std::vector<uint8_t> chunkOut(compressBound);
  95. int compressedSize = LZ4_compress_default(
  96. reinterpret_cast<const char*>(uncompressedData.data() + offset),
  97. reinterpret_cast<char*>(chunkOut.data()),
  98. static_cast<int>(thisChunk),
  99. compressBound
  100. );
  101. if (compressedSize <= 0) {
  102. throw std::runtime_error("LZ4 compression failed at offset " + std::to_string(offset));
  103. }
  104. // Append length prefixes and compressed data to bodyData
  105. uint32_t uncompU32 = static_cast<uint32_t>(thisChunk);
  106. uint32_t compU32 = static_cast<uint32_t>(compressedSize);
  107. const uint8_t* uncompBytes = reinterpret_cast<const uint8_t*>(&uncompU32);
  108. const uint8_t* compBytes = reinterpret_cast<const uint8_t*>(&compU32);
  109. bodyData.insert(bodyData.end(), uncompBytes, uncompBytes + 4);
  110. bodyData.insert(bodyData.end(), compBytes, compBytes + 4);
  111. bodyData.insert(bodyData.end(), chunkOut.begin(), chunkOut.begin() + compressedSize);
  112. offset += thisChunk;
  113. }
  114. // End marker: two zero u32s
  115. uint32_t zero = 0;
  116. const uint8_t* zeroBytes = reinterpret_cast<const uint8_t*>(&zero);
  117. bodyData.insert(bodyData.end(), zeroBytes, zeroBytes + 4);
  118. bodyData.insert(bodyData.end(), zeroBytes, zeroBytes + 4);
  119. // Free the uncompressed buffer — we no longer need it
  120. std::vector<uint8_t>().swap(uncompressedData);
  121. #else
  122. bodyData = std::move(uncompressedData);
  123. compressionType = 0;
  124. #endif
  125. } else {
  126. bodyData = std::move(uncompressedData);
  127. }
  128. // Build header
  129. SnapshotHeader header;
  130. header.timestamp = static_cast<uint64_t>(
  131. std::chrono::duration_cast<std::chrono::milliseconds>(
  132. std::chrono::system_clock::now().time_since_epoch()
  133. ).count()
  134. );
  135. header.walSequence = walSequence;
  136. header.documentCount = docCount;
  137. header.collectionCount = collCount;
  138. header.uncompressedSize = originalUncompressedSize;
  139. header.compressedSize = bodyData.size();
  140. header.compressionType = compressionType;
  141. // Calculate header checksum (excluding the checksum field itself)
  142. header.headerChecksum = crc32::calculate(&header, sizeof(header) - sizeof(header.headerChecksum));
  143. // Generate filename — write to .tmp first, atomic rename at the end
  144. std::filesystem::path snapshotPath = config_.snapshotDir / generateFilename();
  145. std::filesystem::path tmpPath = snapshotPath;
  146. tmpPath += ".tmp";
  147. // Helper to remove tmp on error paths
  148. auto cleanupTmp = [&tmpPath]() {
  149. std::error_code ec;
  150. std::filesystem::remove(tmpPath, ec);
  151. };
  152. // Write to temp file
  153. {
  154. std::ofstream file(tmpPath, std::ios::binary | std::ios::trunc);
  155. if (!file) {
  156. throw std::runtime_error("Failed to create snapshot tmp file: " + tmpPath.string());
  157. }
  158. // Header + short-write check
  159. file.write(reinterpret_cast<const char*>(&header), sizeof(header));
  160. if (!file) {
  161. cleanupTmp();
  162. throw std::runtime_error("Failed to write snapshot header: " + tmpPath.string());
  163. }
  164. // Body + short-write check
  165. file.write(reinterpret_cast<const char*>(bodyData.data()),
  166. static_cast<std::streamsize>(bodyData.size()));
  167. if (!file) {
  168. cleanupTmp();
  169. throw std::runtime_error("Failed to write snapshot body (short write): " + tmpPath.string());
  170. }
  171. // Trailer (body CRC) + short-write check
  172. uint32_t bodyChecksum = crc32::calculate(bodyData.data(), bodyData.size());
  173. file.write(reinterpret_cast<const char*>(&bodyChecksum), sizeof(bodyChecksum));
  174. if (!file) {
  175. cleanupTmp();
  176. throw std::runtime_error("Failed to write snapshot trailer: " + tmpPath.string());
  177. }
  178. // Flush C++ stream buffer
  179. file.flush();
  180. if (!file) {
  181. cleanupTmp();
  182. throw std::runtime_error("Failed to flush snapshot: " + tmpPath.string());
  183. }
  184. } // ofstream destructor runs close()
  185. // fsync the file descriptor — C++ ofstream does not do this
  186. {
  187. int fd = ::open(tmpPath.c_str(), O_RDONLY);
  188. if (fd < 0) {
  189. cleanupTmp();
  190. throw std::runtime_error("Failed to reopen snapshot tmp for fsync: " + tmpPath.string());
  191. }
  192. if (::fsync(fd) != 0) {
  193. int saved_errno = errno;
  194. ::close(fd);
  195. cleanupTmp();
  196. throw std::runtime_error("fsync failed on snapshot tmp (errno=" +
  197. std::to_string(saved_errno) + "): " + tmpPath.string());
  198. }
  199. ::close(fd);
  200. }
  201. // Atomic rename — from this point on, either the final file exists and is complete,
  202. // or the .tmp is gone and no snapshot was produced.
  203. {
  204. std::error_code ec;
  205. std::filesystem::rename(tmpPath, snapshotPath, ec);
  206. if (ec) {
  207. cleanupTmp();
  208. throw std::runtime_error("Snapshot rename failed: " + ec.message());
  209. }
  210. }
  211. // fsync the containing directory so the rename is durable after crash
  212. {
  213. int dfd = ::open(config_.snapshotDir.c_str(), O_RDONLY | O_DIRECTORY);
  214. if (dfd >= 0) {
  215. ::fsync(dfd);
  216. ::close(dfd);
  217. }
  218. // Non-fatal on failure — some filesystems reject O_DIRECTORY
  219. }
  220. // Post-write verification (opt-out via config)
  221. bool verified = true;
  222. if (config_.validateAfterWrite) {
  223. if (!verifySnapshot(snapshotPath)) {
  224. // The file exists on disk but is corrupt — delete it so it can't be used
  225. std::error_code ec;
  226. std::filesystem::remove(snapshotPath, ec);
  227. throw std::runtime_error("Snapshot verification failed after write: " + snapshotPath.string());
  228. }
  229. spdlog::debug("Snapshot verified after write: {}", snapshotPath.string());
  230. }
  231. // Cleanup — only evict old snapshots if the new one passed verification
  232. // (or if cleanup_only_if_verified is disabled)
  233. if (verified || !config_.cleanupOnlyIfVerified) {
  234. cleanupOldSnapshots();
  235. }
  236. return snapshotPath;
  237. }
  238. uint64_t SnapshotManager::loadLatestSnapshot(MemoryStore& store) {
  239. auto snapshots = listSnapshots();
  240. if (snapshots.empty()) {
  241. return 0;
  242. }
  243. return loadSnapshot(snapshots.front(), store);
  244. }
  245. uint64_t SnapshotManager::loadWithFallback(MemoryStore& store,
  246. std::filesystem::path* outUsed,
  247. size_t* outAttempted) {
  248. auto snapshots = listSnapshots(); // already sorted newest-first
  249. if (outAttempted) *outAttempted = 0;
  250. if (snapshots.empty()) {
  251. throw std::runtime_error("loadWithFallback: no snapshots available");
  252. }
  253. std::string lastError;
  254. for (size_t i = 0; i < snapshots.size(); ++i) {
  255. const auto& path = snapshots[i];
  256. if (outAttempted) (*outAttempted)++;
  257. try {
  258. uint64_t seq = loadSnapshot(path, store);
  259. if (outUsed) *outUsed = path;
  260. if (i > 0) {
  261. spdlog::error(
  262. "Loaded fallback snapshot (index {} of {}): {} "
  263. "— newer snapshots were corrupt",
  264. i, snapshots.size(), path.string());
  265. }
  266. return seq;
  267. } catch (const std::exception& e) {
  268. lastError = e.what();
  269. spdlog::warn("Snapshot load failed for {}: {} — trying next older",
  270. path.string(), e.what());
  271. // Note: store.clear() is called by loadSnapshot() before writing,
  272. // so a partial load followed by retry on the next snapshot is safe.
  273. }
  274. }
  275. throw std::runtime_error("loadWithFallback: all " +
  276. std::to_string(snapshots.size()) +
  277. " snapshots failed to load (last error: " +
  278. lastError + ")");
  279. }
  280. uint64_t SnapshotManager::loadSnapshot(const std::filesystem::path& path, MemoryStore& store) {
  281. std::ifstream file(path, std::ios::binary);
  282. if (!file) {
  283. throw std::runtime_error("Failed to open snapshot file: " + path.string());
  284. }
  285. // Read header
  286. SnapshotHeader header;
  287. file.read(reinterpret_cast<char*>(&header), sizeof(header));
  288. if (!file) {
  289. throw std::runtime_error("Failed to read snapshot header");
  290. }
  291. // Verify magic
  292. if (std::memcmp(header.magic, "CALSNAP1", 8) != 0) {
  293. throw std::runtime_error("Invalid snapshot file magic");
  294. }
  295. // Verify header checksum
  296. uint32_t expectedHeaderCrc = crc32::calculate(&header, sizeof(header) - sizeof(header.headerChecksum));
  297. if (header.headerChecksum != expectedHeaderCrc) {
  298. throw std::runtime_error("Snapshot header checksum mismatch");
  299. }
  300. // Read body
  301. std::vector<uint8_t> bodyData(header.compressedSize);
  302. file.read(reinterpret_cast<char*>(bodyData.data()),
  303. static_cast<std::streamsize>(bodyData.size()));
  304. if (!file) {
  305. throw std::runtime_error("Failed to read snapshot body");
  306. }
  307. // Read and verify body checksum
  308. uint32_t storedBodyCrc;
  309. file.read(reinterpret_cast<char*>(&storedBodyCrc), sizeof(storedBodyCrc));
  310. uint32_t calculatedBodyCrc = crc32::calculate(bodyData.data(), bodyData.size());
  311. if (storedBodyCrc != calculatedBodyCrc) {
  312. throw std::runtime_error("Snapshot body checksum mismatch");
  313. }
  314. // Decompress if needed
  315. std::vector<uint8_t> uncompressedData;
  316. if (header.compressionType == 1) {
  317. if (header.version >= 4) {
  318. // v4: chunked LZ4 body
  319. uncompressedData = decompressChunked(bodyData, header.uncompressedSize);
  320. } else {
  321. // v3 and earlier: single LZ4 block
  322. uncompressedData = decompress(bodyData, header.uncompressedSize);
  323. if (uncompressedData.empty()) {
  324. throw std::runtime_error("Failed to decompress snapshot");
  325. }
  326. }
  327. } else {
  328. uncompressedData = std::move(bodyData);
  329. }
  330. // Clear store and deserialize
  331. store.clear();
  332. deserializeStore(uncompressedData, store, header.version);
  333. return header.walSequence;
  334. }
  335. std::vector<std::filesystem::path> SnapshotManager::listSnapshots() const {
  336. std::vector<std::pair<uint64_t, std::filesystem::path>> snapshots;
  337. // Clean up .tmp files left from crashed writes (they start with "snapshot-" and end with ".tmp")
  338. {
  339. std::error_code scan_ec;
  340. for (const auto& entry : std::filesystem::directory_iterator(config_.snapshotDir, scan_ec)) {
  341. if (!entry.is_regular_file()) continue;
  342. const auto& p = entry.path();
  343. if (p.extension() == ".tmp" &&
  344. p.filename().string().starts_with("snapshot-")) {
  345. std::error_code rm_ec;
  346. std::filesystem::remove(p, rm_ec);
  347. spdlog::info("Removed orphaned snapshot tmp file: {}", p.string());
  348. }
  349. }
  350. }
  351. std::error_code ec;
  352. for (const auto& entry : std::filesystem::directory_iterator(config_.snapshotDir, ec)) {
  353. if (entry.is_regular_file()) {
  354. const auto& path = entry.path();
  355. if (path.filename().string().starts_with("snapshot-") &&
  356. path.extension() == ".dat") {
  357. // Read header to get timestamp
  358. std::ifstream file(path, std::ios::binary);
  359. if (file) {
  360. SnapshotHeader header;
  361. file.read(reinterpret_cast<char*>(&header), sizeof(header));
  362. if (file && std::memcmp(header.magic, "CALSNAP1", 8) == 0) {
  363. snapshots.emplace_back(header.timestamp, path);
  364. }
  365. }
  366. }
  367. }
  368. }
  369. // Sort by timestamp descending (newest first)
  370. std::sort(snapshots.begin(), snapshots.end(),
  371. [](const auto& a, const auto& b) { return a.first > b.first; });
  372. std::vector<std::filesystem::path> result;
  373. result.reserve(snapshots.size());
  374. for (const auto& [_, path] : snapshots) {
  375. result.push_back(path);
  376. }
  377. return result;
  378. }
  379. std::optional<SnapshotHeader> SnapshotManager::getSnapshotInfo(const std::filesystem::path& path) const {
  380. std::ifstream file(path, std::ios::binary);
  381. if (!file) {
  382. return std::nullopt;
  383. }
  384. SnapshotHeader header;
  385. file.read(reinterpret_cast<char*>(&header), sizeof(header));
  386. if (!file) {
  387. return std::nullopt;
  388. }
  389. if (std::memcmp(header.magic, "CALSNAP1", 8) != 0) {
  390. return std::nullopt;
  391. }
  392. return header;
  393. }
  394. void SnapshotManager::cleanupOldSnapshots() {
  395. auto snapshots = listSnapshots();
  396. // Delete snapshots beyond the max count
  397. while (snapshots.size() > config_.maxSnapshots) {
  398. const auto& oldestPath = snapshots.back();
  399. std::error_code ec;
  400. std::filesystem::remove(oldestPath, ec);
  401. snapshots.pop_back();
  402. }
  403. }
  404. bool SnapshotManager::verifySnapshot(const std::filesystem::path& path) const {
  405. try {
  406. std::ifstream file(path, std::ios::binary);
  407. if (!file) {
  408. return false;
  409. }
  410. // Read and verify header
  411. SnapshotHeader header;
  412. file.read(reinterpret_cast<char*>(&header), sizeof(header));
  413. if (!file) {
  414. return false;
  415. }
  416. if (std::memcmp(header.magic, "CALSNAP1", 8) != 0) {
  417. return false;
  418. }
  419. uint32_t expectedHeaderCrc = crc32::calculate(&header, sizeof(header) - sizeof(header.headerChecksum));
  420. if (header.headerChecksum != expectedHeaderCrc) {
  421. return false;
  422. }
  423. // Read and verify body
  424. std::vector<uint8_t> bodyData(header.compressedSize);
  425. file.read(reinterpret_cast<char*>(bodyData.data()),
  426. static_cast<std::streamsize>(bodyData.size()));
  427. if (!file) {
  428. return false;
  429. }
  430. uint32_t storedBodyCrc;
  431. file.read(reinterpret_cast<char*>(&storedBodyCrc), sizeof(storedBodyCrc));
  432. uint32_t calculatedBodyCrc = crc32::calculate(bodyData.data(), bodyData.size());
  433. return storedBodyCrc == calculatedBodyCrc;
  434. } catch (...) {
  435. return false;
  436. }
  437. }
  438. std::string SnapshotManager::generateFilename() const {
  439. auto now = std::chrono::system_clock::now();
  440. auto time = std::chrono::system_clock::to_time_t(now);
  441. auto tm = *std::localtime(&time);
  442. std::ostringstream oss;
  443. oss << "snapshot-"
  444. << std::put_time(&tm, "%Y%m%d-%H%M%S")
  445. << ".dat";
  446. return oss.str();
  447. }
  448. std::vector<uint8_t> SnapshotManager::compress(const std::vector<uint8_t>& data) const {
  449. #ifndef DISABLE_LZ4
  450. if (data.empty()) {
  451. return {};
  452. }
  453. int maxCompressedSize = LZ4_compressBound(static_cast<int>(data.size()));
  454. std::vector<uint8_t> compressed(maxCompressedSize);
  455. int compressedSize = LZ4_compress_default(
  456. reinterpret_cast<const char*>(data.data()),
  457. reinterpret_cast<char*>(compressed.data()),
  458. static_cast<int>(data.size()),
  459. maxCompressedSize
  460. );
  461. if (compressedSize <= 0) {
  462. return {}; // Compression failed
  463. }
  464. compressed.resize(compressedSize);
  465. return compressed;
  466. #else
  467. return {}; // Compression disabled
  468. #endif
  469. }
  470. std::vector<uint8_t> SnapshotManager::decompress(const std::vector<uint8_t>& data,
  471. uint64_t uncompressedSize) const {
  472. #ifndef DISABLE_LZ4
  473. if (data.empty() || uncompressedSize == 0) {
  474. return {};
  475. }
  476. std::vector<uint8_t> decompressed(uncompressedSize);
  477. int decompressedSize = LZ4_decompress_safe(
  478. reinterpret_cast<const char*>(data.data()),
  479. reinterpret_cast<char*>(decompressed.data()),
  480. static_cast<int>(data.size()),
  481. static_cast<int>(uncompressedSize)
  482. );
  483. if (decompressedSize < 0 || static_cast<uint64_t>(decompressedSize) != uncompressedSize) {
  484. return {}; // Decompression failed
  485. }
  486. return decompressed;
  487. #else
  488. return {}; // Compression disabled
  489. #endif
  490. }
  491. std::vector<uint8_t> SnapshotManager::serializeStore(const MemoryStore& store,
  492. uint64_t& docCount,
  493. uint32_t& collCount) const {
  494. std::vector<uint8_t> result;
  495. docCount = 0;
  496. collCount = 0;
  497. auto collections = store.getAllCollectionsWithOptions();
  498. collCount = static_cast<uint32_t>(collections.size());
  499. for (const auto& [name, options] : collections) {
  500. // Write collection name
  501. uint16_t nameLen = static_cast<uint16_t>(name.size());
  502. result.push_back(static_cast<uint8_t>(nameLen & 0xFF));
  503. result.push_back(static_cast<uint8_t>((nameLen >> 8) & 0xFF));
  504. result.insert(result.end(), name.begin(), name.end());
  505. // Write collection options
  506. std::string optionsJson = options.toJson().dump();
  507. uint32_t optionsLen = static_cast<uint32_t>(optionsJson.size());
  508. for (int i = 0; i < 4; ++i) {
  509. result.push_back(static_cast<uint8_t>((optionsLen >> (i * 8)) & 0xFF));
  510. }
  511. result.insert(result.end(), optionsJson.begin(), optionsJson.end());
  512. // Get all documents in collection. Snapshot must include both
  513. // in-memory docs AND docs that have been evicted to stubs — otherwise
  514. // truncateBefore(walSeq) after this snapshot deletes the WAL entries
  515. // that hold the only remaining copy of the evicted docs, and they're
  516. // gone forever. Pre-v1.7.3 only wrote in-memory docs; v1.7.0
  517. // introduced eviction without a corresponding snapshot path, so any
  518. // snapshot taken after eviction silently lost the evicted set.
  519. auto docs = store.getAllDocuments(name);
  520. auto evictedIds = store.getEvictedDocumentIds(name);
  521. // Reserve 8 bytes for the doc count and back-patch after writing,
  522. // so we can skip evicted docs that fail to load from WAL without
  523. // desynchronising the deserializer's counter.
  524. size_t countOffset = result.size();
  525. for (int i = 0; i < 8; ++i) result.push_back(0);
  526. uint64_t collDocCount = 0;
  527. // Write each in-memory document
  528. for (const auto& doc : docs) {
  529. std::string docJson = doc.toJson().dump();
  530. uint32_t docLen = static_cast<uint32_t>(docJson.size());
  531. for (int i = 0; i < 4; ++i) {
  532. result.push_back(static_cast<uint8_t>((docLen >> (i * 8)) & 0xFF));
  533. }
  534. result.insert(result.end(), docJson.begin(), docJson.end());
  535. collDocCount++;
  536. }
  537. // Write each evicted document, loading from WAL via the document
  538. // load callback. peekEvictedDocument is const and uses the same
  539. // path that read queries use to fault evicted docs back in.
  540. uint64_t evictedFailed = 0;
  541. for (const auto& id : evictedIds) {
  542. auto docOpt = store.peekEvictedDocument(name, id);
  543. if (!docOpt) {
  544. evictedFailed++;
  545. continue;
  546. }
  547. std::string docJson = docOpt->toJson().dump();
  548. uint32_t docLen = static_cast<uint32_t>(docJson.size());
  549. for (int i = 0; i < 4; ++i) {
  550. result.push_back(static_cast<uint8_t>((docLen >> (i * 8)) & 0xFF));
  551. }
  552. result.insert(result.end(), docJson.begin(), docJson.end());
  553. collDocCount++;
  554. }
  555. if (evictedFailed > 0) {
  556. spdlog::warn(
  557. "Snapshot: {} evicted documents in collection '{}' could not "
  558. "be loaded from WAL — they will be missing from the snapshot. "
  559. "Likely a pre-v1.7.3 snapshot already lost them.",
  560. evictedFailed, name);
  561. }
  562. // Back-patch the doc count
  563. for (int i = 0; i < 8; ++i) {
  564. result[countOffset + i] =
  565. static_cast<uint8_t>((collDocCount >> (i * 8)) & 0xFF);
  566. }
  567. docCount += collDocCount;
  568. // Write version history (v2)
  569. auto allHistory = store.getAllVersionHistory(name);
  570. uint64_t historyEntryCount = allHistory.size();
  571. for (int i = 0; i < 8; ++i) {
  572. result.push_back(static_cast<uint8_t>((historyEntryCount >> (i * 8)) & 0xFF));
  573. }
  574. for (const auto& [docId, versions] : allHistory) {
  575. // Write doc ID
  576. uint16_t docIdLen = static_cast<uint16_t>(docId.size());
  577. result.push_back(static_cast<uint8_t>(docIdLen & 0xFF));
  578. result.push_back(static_cast<uint8_t>((docIdLen >> 8) & 0xFF));
  579. result.insert(result.end(), docId.begin(), docId.end());
  580. // Write version count
  581. uint32_t versionCount = static_cast<uint32_t>(versions.size());
  582. for (int i = 0; i < 4; ++i) {
  583. result.push_back(static_cast<uint8_t>((versionCount >> (i * 8)) & 0xFF));
  584. }
  585. // Write each version
  586. for (const auto& ver : versions) {
  587. std::string verJson = ver.toJson().dump();
  588. uint32_t verLen = static_cast<uint32_t>(verJson.size());
  589. for (int i = 0; i < 4; ++i) {
  590. result.push_back(static_cast<uint8_t>((verLen >> (i * 8)) & 0xFF));
  591. }
  592. result.insert(result.end(), verJson.begin(), verJson.end());
  593. }
  594. }
  595. // Write vector data (v3)
  596. const auto* vectors = store.getCollectionVectors(name);
  597. uint64_t vecCount = (vectors != nullptr) ? static_cast<uint64_t>(vectors->size()) : 0;
  598. for (int i = 0; i < 8; ++i) {
  599. result.push_back(static_cast<uint8_t>((vecCount >> (i * 8)) & 0xFF));
  600. }
  601. if (vectors != nullptr) {
  602. for (const auto& [docId, vec] : *vectors) {
  603. // Write doc ID
  604. uint16_t docIdLen = static_cast<uint16_t>(docId.size());
  605. result.push_back(static_cast<uint8_t>(docIdLen & 0xFF));
  606. result.push_back(static_cast<uint8_t>((docIdLen >> 8) & 0xFF));
  607. result.insert(result.end(), docId.begin(), docId.end());
  608. // Write vector dimension
  609. uint32_t dim = static_cast<uint32_t>(vec.size());
  610. for (int i = 0; i < 4; ++i) {
  611. result.push_back(static_cast<uint8_t>((dim >> (i * 8)) & 0xFF));
  612. }
  613. // Write raw float data
  614. const uint8_t* floatBytes = reinterpret_cast<const uint8_t*>(vec.data());
  615. result.insert(result.end(), floatBytes, floatBytes + dim * sizeof(float));
  616. }
  617. }
  618. }
  619. return result;
  620. }
  621. void SnapshotManager::deserializeStore(const std::vector<uint8_t>& data, MemoryStore& store,
  622. uint32_t snapshotVersion) const {
  623. size_t offset = 0;
  624. while (offset < data.size()) {
  625. // Read collection name
  626. if (offset + 2 > data.size()) break;
  627. uint16_t nameLen = static_cast<uint16_t>(data[offset]) |
  628. (static_cast<uint16_t>(data[offset + 1]) << 8);
  629. offset += 2;
  630. if (offset + nameLen > data.size()) break;
  631. std::string collName(reinterpret_cast<const char*>(&data[offset]), nameLen);
  632. offset += nameLen;
  633. // Read collection options
  634. if (offset + 4 > data.size()) break;
  635. uint32_t optionsLen = 0;
  636. for (int i = 0; i < 4; ++i) {
  637. optionsLen |= static_cast<uint32_t>(data[offset++]) << (i * 8);
  638. }
  639. if (offset + optionsLen > data.size()) break;
  640. std::string optionsJson(reinterpret_cast<const char*>(&data[offset]), optionsLen);
  641. offset += optionsLen;
  642. CollectionOptions options;
  643. try {
  644. options = CollectionOptions::fromJson(nlohmann::json::parse(optionsJson));
  645. } catch (const nlohmann::json::exception&) {
  646. // Use default options
  647. }
  648. // Create collection
  649. store.createCollection(collName, options);
  650. // Read document count
  651. if (offset + 8 > data.size()) break;
  652. uint64_t docCount = 0;
  653. for (int i = 0; i < 8; ++i) {
  654. docCount |= static_cast<uint64_t>(data[offset++]) << (i * 8);
  655. }
  656. // Read each document
  657. for (uint64_t i = 0; i < docCount; ++i) {
  658. if (offset + 4 > data.size()) break;
  659. uint32_t docLen = 0;
  660. for (int j = 0; j < 4; ++j) {
  661. docLen |= static_cast<uint32_t>(data[offset++]) << (j * 8);
  662. }
  663. if (offset + docLen > data.size()) break;
  664. std::string docJson(reinterpret_cast<const char*>(&data[offset]), docLen);
  665. offset += docLen;
  666. try {
  667. Document doc = Document::fromJson(nlohmann::json::parse(docJson));
  668. store.loadDocument(collName, doc);
  669. } catch (const nlohmann::json::exception&) {
  670. // Skip invalid document
  671. }
  672. }
  673. // Read version history (v2+)
  674. if (snapshotVersion >= 2) {
  675. if (offset + 8 > data.size()) break;
  676. uint64_t historyEntryCount = 0;
  677. for (int i = 0; i < 8; ++i) {
  678. historyEntryCount |= static_cast<uint64_t>(data[offset++]) << (i * 8);
  679. }
  680. for (uint64_t h = 0; h < historyEntryCount; ++h) {
  681. // Read doc ID
  682. if (offset + 2 > data.size()) break;
  683. uint16_t docIdLen = static_cast<uint16_t>(data[offset]) |
  684. (static_cast<uint16_t>(data[offset + 1]) << 8);
  685. offset += 2;
  686. if (offset + docIdLen > data.size()) break;
  687. std::string docId(reinterpret_cast<const char*>(&data[offset]), docIdLen);
  688. offset += docIdLen;
  689. // Read version count
  690. if (offset + 4 > data.size()) break;
  691. uint32_t versionCount = 0;
  692. for (int i = 0; i < 4; ++i) {
  693. versionCount |= static_cast<uint32_t>(data[offset++]) << (i * 8);
  694. }
  695. // Read each version into a deque
  696. std::deque<DocumentVersion> history;
  697. for (uint32_t v = 0; v < versionCount; ++v) {
  698. if (offset + 4 > data.size()) break;
  699. uint32_t verLen = 0;
  700. for (int i = 0; i < 4; ++i) {
  701. verLen |= static_cast<uint32_t>(data[offset++]) << (i * 8);
  702. }
  703. if (offset + verLen > data.size()) break;
  704. std::string verJson(reinterpret_cast<const char*>(&data[offset]), verLen);
  705. offset += verLen;
  706. try {
  707. auto ver = DocumentVersion::fromJson(nlohmann::json::parse(verJson));
  708. history.push_back(std::move(ver));
  709. } catch (const nlohmann::json::exception&) {
  710. // Skip invalid version entry
  711. }
  712. }
  713. if (!history.empty()) {
  714. store.loadVersionHistory(collName, docId, std::move(history));
  715. }
  716. }
  717. }
  718. // Read vector data (v3+)
  719. if (snapshotVersion >= 3) {
  720. if (offset + 8 > data.size()) break;
  721. uint64_t vecCount = 0;
  722. for (int i = 0; i < 8; ++i) {
  723. vecCount |= static_cast<uint64_t>(data[offset++]) << (i * 8);
  724. }
  725. for (uint64_t v = 0; v < vecCount; ++v) {
  726. // Read doc ID
  727. if (offset + 2 > data.size()) break;
  728. uint16_t docIdLen = static_cast<uint16_t>(data[offset]) |
  729. (static_cast<uint16_t>(data[offset + 1]) << 8);
  730. offset += 2;
  731. if (offset + docIdLen > data.size()) break;
  732. std::string docId(reinterpret_cast<const char*>(&data[offset]), docIdLen);
  733. offset += docIdLen;
  734. // Read vector dimension
  735. if (offset + 4 > data.size()) break;
  736. uint32_t dim = 0;
  737. for (int i = 0; i < 4; ++i) {
  738. dim |= static_cast<uint32_t>(data[offset++]) << (i * 8);
  739. }
  740. // Read raw float data
  741. size_t byteCount = static_cast<size_t>(dim) * sizeof(float);
  742. if (offset + byteCount > data.size()) break;
  743. std::vector<float> vec(dim);
  744. std::memcpy(vec.data(), &data[offset], byteCount);
  745. offset += byteCount;
  746. store.loadVector(collName, docId, std::move(vec));
  747. }
  748. }
  749. }
  750. }
  751. } // namespace smartbotic::database