|
|
@@ -21,6 +21,62 @@
|
|
|
|
|
|
namespace smartbotic::database {
|
|
|
|
|
|
+namespace {
|
|
|
+
|
|
|
+// Decompress a v4-chunked LZ4 body produced by createSnapshot.
|
|
|
+// Body layout: sequence of [u32 uncomp_size, u32 comp_size, LZ4 bytes...] terminated
|
|
|
+// by a (0, 0) end marker. Returns the full concatenated uncompressed payload.
|
|
|
+std::vector<uint8_t> decompressChunked(const std::vector<uint8_t>& body,
|
|
|
+ uint64_t uncompressedSize) {
|
|
|
+#ifndef DISABLE_LZ4
|
|
|
+ std::vector<uint8_t> out;
|
|
|
+ out.reserve(uncompressedSize);
|
|
|
+
|
|
|
+ size_t offset = 0;
|
|
|
+ while (offset + 8 <= body.size()) {
|
|
|
+ uint32_t uncompU32 = 0;
|
|
|
+ uint32_t compU32 = 0;
|
|
|
+ std::memcpy(&uncompU32, body.data() + offset, 4);
|
|
|
+ std::memcpy(&compU32, body.data() + offset + 4, 4);
|
|
|
+ offset += 8;
|
|
|
+
|
|
|
+ if (uncompU32 == 0 && compU32 == 0) {
|
|
|
+ break; // end marker
|
|
|
+ }
|
|
|
+
|
|
|
+ if (offset + compU32 > body.size()) {
|
|
|
+ throw std::runtime_error("Snapshot chunk extends past body");
|
|
|
+ }
|
|
|
+
|
|
|
+ size_t outStart = out.size();
|
|
|
+ out.resize(outStart + uncompU32);
|
|
|
+ int decompressed = LZ4_decompress_safe(
|
|
|
+ reinterpret_cast<const char*>(body.data() + offset),
|
|
|
+ reinterpret_cast<char*>(out.data() + outStart),
|
|
|
+ static_cast<int>(compU32),
|
|
|
+ static_cast<int>(uncompU32)
|
|
|
+ );
|
|
|
+ if (decompressed < 0 || static_cast<uint32_t>(decompressed) != uncompU32) {
|
|
|
+ throw std::runtime_error("Snapshot chunk decompression failed");
|
|
|
+ }
|
|
|
+ offset += compU32;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (out.size() != uncompressedSize) {
|
|
|
+ throw std::runtime_error("Decompressed size mismatch: expected " +
|
|
|
+ std::to_string(uncompressedSize) + " got " +
|
|
|
+ std::to_string(out.size()));
|
|
|
+ }
|
|
|
+ return out;
|
|
|
+#else
|
|
|
+ (void)body;
|
|
|
+ (void)uncompressedSize;
|
|
|
+ throw std::runtime_error("LZ4 disabled — cannot decompress chunked snapshot body");
|
|
|
+#endif
|
|
|
+}
|
|
|
+
|
|
|
+} // namespace
|
|
|
+
|
|
|
SnapshotManager::SnapshotManager(Config config)
|
|
|
: config_(std::move(config)) {
|
|
|
// Create snapshot directory if it doesn't exist
|
|
|
@@ -33,18 +89,63 @@ std::filesystem::path SnapshotManager::createSnapshot(const MemoryStore& store,
|
|
|
uint64_t docCount = 0;
|
|
|
uint32_t collCount = 0;
|
|
|
std::vector<uint8_t> uncompressedData = serializeStore(store, docCount, collCount);
|
|
|
+ const uint64_t originalUncompressedSize = uncompressedData.size();
|
|
|
+
|
|
|
+ // Build chunked body with streaming compression.
|
|
|
+ // v4 body layout: sequence of [u32 uncompressed_size, u32 compressed_size, bytes...]
|
|
|
+ // terminated by a (0, 0) end marker. Peak memory per chunk is ~16 MB output buffer
|
|
|
+ // instead of LZ4_compressBound(full DB) which used to be ~3 GB for production sizes.
|
|
|
+ constexpr size_t CHUNK_SIZE = 16 * 1024 * 1024; // 16 MB uncompressed per chunk
|
|
|
|
|
|
- // Compress if enabled
|
|
|
std::vector<uint8_t> bodyData;
|
|
|
uint32_t compressionType = 0;
|
|
|
|
|
|
if (config_.compressionEnabled) {
|
|
|
- bodyData = compress(uncompressedData);
|
|
|
- if (!bodyData.empty()) {
|
|
|
- compressionType = 1; // LZ4
|
|
|
- } else {
|
|
|
- bodyData = std::move(uncompressedData);
|
|
|
+#ifndef DISABLE_LZ4
|
|
|
+ compressionType = 1;
|
|
|
+ // Rough estimate to reduce reallocation thrash; actual size depends on payload compressibility.
|
|
|
+ bodyData.reserve(uncompressedData.size() / 2 + CHUNK_SIZE);
|
|
|
+
|
|
|
+ size_t offset = 0;
|
|
|
+ while (offset < uncompressedData.size()) {
|
|
|
+ size_t thisChunk = std::min(CHUNK_SIZE, uncompressedData.size() - offset);
|
|
|
+ int compressBound = LZ4_compressBound(static_cast<int>(thisChunk));
|
|
|
+ std::vector<uint8_t> chunkOut(compressBound);
|
|
|
+
|
|
|
+ int compressedSize = LZ4_compress_default(
|
|
|
+ reinterpret_cast<const char*>(uncompressedData.data() + offset),
|
|
|
+ reinterpret_cast<char*>(chunkOut.data()),
|
|
|
+ static_cast<int>(thisChunk),
|
|
|
+ compressBound
|
|
|
+ );
|
|
|
+ if (compressedSize <= 0) {
|
|
|
+ throw std::runtime_error("LZ4 compression failed at offset " + std::to_string(offset));
|
|
|
+ }
|
|
|
+
|
|
|
+ // Append length prefixes and compressed data to bodyData
|
|
|
+ uint32_t uncompU32 = static_cast<uint32_t>(thisChunk);
|
|
|
+ uint32_t compU32 = static_cast<uint32_t>(compressedSize);
|
|
|
+ const uint8_t* uncompBytes = reinterpret_cast<const uint8_t*>(&uncompU32);
|
|
|
+ const uint8_t* compBytes = reinterpret_cast<const uint8_t*>(&compU32);
|
|
|
+ bodyData.insert(bodyData.end(), uncompBytes, uncompBytes + 4);
|
|
|
+ bodyData.insert(bodyData.end(), compBytes, compBytes + 4);
|
|
|
+ bodyData.insert(bodyData.end(), chunkOut.begin(), chunkOut.begin() + compressedSize);
|
|
|
+
|
|
|
+ offset += thisChunk;
|
|
|
}
|
|
|
+
|
|
|
+ // End marker: two zero u32s
|
|
|
+ uint32_t zero = 0;
|
|
|
+ const uint8_t* zeroBytes = reinterpret_cast<const uint8_t*>(&zero);
|
|
|
+ bodyData.insert(bodyData.end(), zeroBytes, zeroBytes + 4);
|
|
|
+ bodyData.insert(bodyData.end(), zeroBytes, zeroBytes + 4);
|
|
|
+
|
|
|
+ // Free the uncompressed buffer — we no longer need it
|
|
|
+ std::vector<uint8_t>().swap(uncompressedData);
|
|
|
+#else
|
|
|
+ bodyData = std::move(uncompressedData);
|
|
|
+ compressionType = 0;
|
|
|
+#endif
|
|
|
} else {
|
|
|
bodyData = std::move(uncompressedData);
|
|
|
}
|
|
|
@@ -59,7 +160,7 @@ std::filesystem::path SnapshotManager::createSnapshot(const MemoryStore& store,
|
|
|
header.walSequence = walSequence;
|
|
|
header.documentCount = docCount;
|
|
|
header.collectionCount = collCount;
|
|
|
- header.uncompressedSize = uncompressedData.size();
|
|
|
+ header.uncompressedSize = originalUncompressedSize;
|
|
|
header.compressedSize = bodyData.size();
|
|
|
header.compressionType = compressionType;
|
|
|
|
|
|
@@ -267,9 +368,15 @@ uint64_t SnapshotManager::loadSnapshot(const std::filesystem::path& path, Memory
|
|
|
// Decompress if needed
|
|
|
std::vector<uint8_t> uncompressedData;
|
|
|
if (header.compressionType == 1) {
|
|
|
- uncompressedData = decompress(bodyData, header.uncompressedSize);
|
|
|
- if (uncompressedData.empty()) {
|
|
|
- throw std::runtime_error("Failed to decompress snapshot");
|
|
|
+ if (header.version >= 4) {
|
|
|
+ // v4: chunked LZ4 body
|
|
|
+ uncompressedData = decompressChunked(bodyData, header.uncompressedSize);
|
|
|
+ } else {
|
|
|
+ // v3 and earlier: single LZ4 block
|
|
|
+ uncompressedData = decompress(bodyData, header.uncompressedSize);
|
|
|
+ if (uncompressedData.empty()) {
|
|
|
+ throw std::runtime_error("Failed to decompress snapshot");
|
|
|
+ }
|
|
|
}
|
|
|
} else {
|
|
|
uncompressedData = std::move(bodyData);
|