feat: extend backup
This commit is contained in:
1 parent
f9da2b513b
commit
a5ba9ed3db
1 file changed
+214
+214
@@ -311,3 +311,217 @@ test "backup is point-in-time: writes after snapshot are excluded" {
|
||||
defer std.fs.cwd().deleteTree(srcDir) catch {};
|
||||
defer std.fs.cwd().deleteFile(backupPath) catch {};
|
||||
|
||||
var src = try Db.open(allocator, srcDir, .{});
|
||||
defer src.close();
|
||||
try src.put("before", "1");
|
||||
const snap = try src.snapshot();
|
||||
try src.put("after", "2");
|
||||
|
||||
const count = try dumpToFile(allocator, snap, backupPath);
|
||||
try std.testing.expectEqual(@as(u64, 1), count);
|
||||
|
||||
const info = try inspectFile(allocator, backupPath);
|
||||
try std.testing.expectEqual(snap.seq, info.seq);
|
||||
}
|
||||
|
||||
test "restore into db with pre-existing disjoint keys merges without touching them" {
|
||||
const allocator = std.testing.allocator;
|
||||
const srcDir = "test-backup-src-merge";
|
||||
const dstDir = "test-backup-dst-merge";
|
||||
const backupPath = "test-backup-merge.cblb";
|
||||
std.fs.cwd().deleteTree(srcDir) catch {};
|
||||
std.fs.cwd().deleteTree(dstDir) catch {};
|
||||
defer std.fs.cwd().deleteTree(srcDir) catch {};
|
||||
defer std.fs.cwd().deleteTree(dstDir) catch {};
|
||||
defer std.fs.cwd().deleteFile(backupPath) catch {};
|
||||
|
||||
var src = try Db.open(allocator, srcDir, .{});
|
||||
defer src.close();
|
||||
try src.put("from-backup", "yes");
|
||||
const count = try dumpToFile(allocator, try src.snapshot(), backupPath);
|
||||
try std.testing.expectEqual(@as(u64, 1), count);
|
||||
|
||||
var dst = try Db.open(allocator, dstDir, .{});
|
||||
defer dst.close();
|
||||
try dst.put("already-here", "kept");
|
||||
|
||||
_ = try restoreFromFile(allocator, &dst, backupPath, 32);
|
||||
|
||||
const kept = try dst.get(allocator, "already-here");
|
||||
defer allocator.free(kept.?);
|
||||
try std.testing.expectEqualStrings("kept", kept.?);
|
||||
|
||||
const restored = try dst.get(allocator, "from-backup");
|
||||
defer allocator.free(restored.?);
|
||||
try std.testing.expectEqualStrings("yes", restored.?);
|
||||
}
|
||||
|
||||
test "restore chunking applies every entry across multiple write batches" {
|
||||
const allocator = std.testing.allocator;
|
||||
const srcDir = "test-backup-src-chunks";
|
||||
const dstDir = "test-backup-dst-chunks";
|
||||
const backupPath = "test-backup-chunks.cblb";
|
||||
std.fs.cwd().deleteTree(srcDir) catch {};
|
||||
std.fs.cwd().deleteTree(dstDir) catch {};
|
||||
defer std.fs.cwd().deleteTree(srcDir) catch {};
|
||||
defer std.fs.cwd().deleteTree(dstDir) catch {};
|
||||
defer std.fs.cwd().deleteFile(backupPath) catch {};
|
||||
|
||||
var src = try Db.open(allocator, srcDir, .{});
|
||||
defer src.close();
|
||||
var keyBuf: [16]u8 = undefined;
|
||||
var i: usize = 0;
|
||||
while (i < 97) : (i += 1) {
|
||||
const key = try std.fmt.bufPrint(&keyBuf, "c{d:0>6}", .{i});
|
||||
try src.put(key, "x");
|
||||
}
|
||||
_ = try dumpToFile(allocator, try src.snapshot(), backupPath);
|
||||
|
||||
var dst = try Db.open(allocator, dstDir, .{});
|
||||
defer dst.close();
|
||||
const info = try restoreFromFile(allocator, &dst, backupPath, 10);
|
||||
try std.testing.expectEqual(@as(u64, 97), info.entryCount);
|
||||
|
||||
var found: usize = 0;
|
||||
var iter = try dst.scan(allocator, null, null);
|
||||
defer iter.deinit();
|
||||
while (try iter.next()) |_| found += 1;
|
||||
try std.testing.expectEqual(@as(usize, 97), found);
|
||||
}
|
||||
|
||||
test "corrupted entry payload is rejected before any restore write happens" {
|
||||
const allocator = std.testing.allocator;
|
||||
const srcDir = "test-backup-src-corrupt";
|
||||
const dstDir = "test-backup-dst-corrupt";
|
||||
const backupPath = "test-backup-corrupt.cblb";
|
||||
std.fs.cwd().deleteTree(srcDir) catch {};
|
||||
std.fs.cwd().deleteTree(dstDir) catch {};
|
||||
defer std.fs.cwd().deleteTree(srcDir) catch {};
|
||||
defer std.fs.cwd().deleteTree(dstDir) catch {};
|
||||
defer std.fs.cwd().deleteFile(backupPath) catch {};
|
||||
|
||||
var src = try Db.open(allocator, srcDir, .{});
|
||||
defer src.close();
|
||||
try src.put("intact", "value-one");
|
||||
try src.put("victim", "value-two");
|
||||
_ = try dumpToFile(allocator, try src.snapshot(), backupPath);
|
||||
|
||||
{
|
||||
const file = try std.fs.cwd().openFile(backupPath, .{ .mode = .read_write });
|
||||
defer file.close();
|
||||
// flip a byte squarely inside the entry region (well past the 16 byte header).
|
||||
try file.seekTo(24);
|
||||
var byte: [1]u8 = undefined;
|
||||
_ = try file.read(&byte);
|
||||
try file.seekTo(24);
|
||||
try file.writeAll(&.{byte[0] ^ 0xFF});
|
||||
}
|
||||
|
||||
var dst = try Db.open(allocator, dstDir, .{});
|
||||
defer dst.close();
|
||||
const result = restoreFromFile(allocator, &dst, backupPath, 32);
|
||||
try std.testing.expectError(BackupError.Corrupt, result);
|
||||
|
||||
// nothing from the corrupt backup should have been applied: validation
|
||||
// happens over the whole in-memory buffer before the first WriteBatch.
|
||||
var iter = try dst.scan(allocator, null, null);
|
||||
defer iter.deinit();
|
||||
try std.testing.expect((try iter.next()) == null);
|
||||
}
|
||||
|
||||
test "truncated file is rejected as Truncated, not a crash" {
|
||||
const allocator = std.testing.allocator;
|
||||
const srcDir = "test-backup-src-trunc";
|
||||
const dstDir = "test-backup-dst-trunc";
|
||||
const backupPath = "test-backup-trunc.cblb";
|
||||
std.fs.cwd().deleteTree(srcDir) catch {};
|
||||
std.fs.cwd().deleteTree(dstDir) catch {};
|
||||
defer std.fs.cwd().deleteTree(srcDir) catch {};
|
||||
defer std.fs.cwd().deleteTree(dstDir) catch {};
|
||||
defer std.fs.cwd().deleteFile(backupPath) catch {};
|
||||
|
||||
var src = try Db.open(allocator, srcDir, .{});
|
||||
defer src.close();
|
||||
try src.put("a", "b");
|
||||
_ = try dumpToFile(allocator, try src.snapshot(), backupPath);
|
||||
|
||||
{
|
||||
const data = try std.fs.cwd().readFileAlloc(allocator, backupPath, 1 << 20);
|
||||
defer allocator.free(data);
|
||||
const truncated = data[0 .. data.len - 5];
|
||||
const file = try std.fs.cwd().createFile(backupPath, .{ .truncate = true });
|
||||
defer file.close();
|
||||
try file.writeAll(truncated);
|
||||
}
|
||||
|
||||
var dst = try Db.open(allocator, dstDir, .{});
|
||||
defer dst.close();
|
||||
const result = restoreFromFile(allocator, &dst, backupPath, 32);
|
||||
try std.testing.expectError(BackupError.Truncated, result);
|
||||
}
|
||||
|
||||
test "bad magic header is rejected without reading the rest of the file" {
|
||||
const allocator = std.testing.allocator;
|
||||
const backupPath = "test-backup-badmagic.cblb";
|
||||
defer std.fs.cwd().deleteFile(backupPath) catch {};
|
||||
|
||||
const file = try std.fs.cwd().createFile(backupPath, .{ .truncate = true });
|
||||
{
|
||||
defer file.close();
|
||||
try file.writeAll("XXXX" ++ [_]u8{0} ** 12);
|
||||
}
|
||||
|
||||
const result = inspectFile(allocator, backupPath);
|
||||
try std.testing.expectError(BackupError.BadMagic, result);
|
||||
}
|
||||
|
||||
test "unsupported version is rejected" {
|
||||
const allocator = std.testing.allocator;
|
||||
const backupPath = "test-backup-badversion.cblb";
|
||||
defer std.fs.cwd().deleteFile(backupPath) catch {};
|
||||
|
||||
const file = try std.fs.cwd().createFile(backupPath, .{ .truncate = true });
|
||||
{
|
||||
defer file.close();
|
||||
var buf: [16]u8 = undefined;
|
||||
@memcpy(buf[0..4], headerMagic);
|
||||
std.mem.writeInt(u32, buf[4..8], 99, encode.endian);
|
||||
std.mem.writeInt(u64, buf[8..16], 0, encode.endian);
|
||||
try file.writeAll(&buf);
|
||||
}
|
||||
|
||||
const result = inspectFile(allocator, backupPath);
|
||||
try std.testing.expectError(BackupError.UnsupportedVersion, result);
|
||||
}
|
||||
|
||||
test "declared entry count mismatch is caught even if per-entry crcs are valid" {
|
||||
const allocator = std.testing.allocator;
|
||||
const srcDir = "test-backup-src-countmismatch";
|
||||
const backupPath = "test-backup-countmismatch.cblb";
|
||||
std.fs.cwd().deleteTree(srcDir) catch {};
|
||||
defer std.fs.cwd().deleteTree(srcDir) catch {};
|
||||
defer std.fs.cwd().deleteFile(backupPath) catch {};
|
||||
|
||||
var src = try Db.open(allocator, srcDir, .{});
|
||||
defer src.close();
|
||||
try src.put("a", "1");
|
||||
try src.put("b", "2");
|
||||
_ = try dumpToFile(allocator, try src.snapshot(), backupPath);
|
||||
|
||||
{
|
||||
const data = try std.fs.cwd().readFileAlloc(allocator, backupPath, 1 << 20);
|
||||
defer allocator.free(data);
|
||||
// footer is the last 4(magic)+8(count)+4(crc)=16 bytes; corrupt count only,
|
||||
// which necessarily also breaks the whole-file crc, but count is checked first.
|
||||
const countPos = data.len - 12;
|
||||
var mutated = try allocator.dupe(u8, data);
|
||||
defer allocator.free(mutated);
|
||||
std.mem.writeInt(u64, mutated[countPos..][0..8], 999, encode.endian);
|
||||
const file = try std.fs.cwd().createFile(backupPath, .{ .truncate = true });
|
||||
defer file.close();
|
||||
try file.writeAll(mutated);
|
||||
}
|
||||
|
||||
const result = inspectFile(allocator, backupPath);
|
||||
try std.testing.expectError(BackupError.CountMismatch, result);
|
||||
}
|
||||
Reference in new issue
Block a user