659 lines
23 KiB
Zig
659 lines
23 KiB
Zig
const std = @import("std");
|
|
const encode = @import("encode.zig");
|
|
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
// VersionSet/VersionEdit: append-only log of incremental changes to the
|
|
// live sstable set (fileId -> level), replacing a full-state rewrite on
|
|
// every flush/compact. Modeled on LevelDB's VersionSet: a CURRENT file
|
|
// names the active manifest log, the log holds a sequence of edit
|
|
// records, and recovery replays them into a map. The log is periodically
|
|
// rotated (a single snapshot record replaces the whole history) so it
|
|
// never grows without bound.
|
|
|
|
pub const VersionError = error{
|
|
Corrupt,
|
|
Truncated,
|
|
};
|
|
|
|
pub const FileEdit = struct {
|
|
fileId: u64,
|
|
level: u32,
|
|
};
|
|
|
|
pub const VersionEdit = struct {
|
|
allocator: Allocator,
|
|
added: std.ArrayList(FileEdit),
|
|
deleted: std.ArrayList(u64),
|
|
/// Optional counter watermarks, stamped by the caller alongside a file
|
|
/// add/delete: the exact `Db.nextFileId`/`Db.nextSeq` value in effect
|
|
/// right after this edit was built. Recorded so recovery can recover
|
|
/// these counters from the manifest instead of relying solely on
|
|
/// scanning which sstable files currently happen to be on disk - a scan
|
|
/// alone regresses whenever a compaction fully garbage-collects a run of
|
|
/// tombstones into zero output files (see runMerge: no output file means
|
|
/// no fresh fileId is minted, and if the GC'd inputs held the highest
|
|
/// seq/fileId ever used, a post-restart scan would compute a lower
|
|
/// floor than what was really already consumed, risking seq reuse
|
|
/// across a restart).
|
|
nextFileId: ?u64 = null,
|
|
nextSeq: ?u64 = null,
|
|
|
|
pub fn init(allocator: Allocator) VersionEdit {
|
|
return .{
|
|
.allocator = allocator,
|
|
.added = std.ArrayList(FileEdit).init(allocator),
|
|
.deleted = std.ArrayList(u64).init(allocator),
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *VersionEdit) void {
|
|
self.added.deinit();
|
|
self.deleted.deinit();
|
|
}
|
|
|
|
pub fn addFile(self: *VersionEdit, fileId: u64, level: usize) !void {
|
|
try self.added.append(.{ .fileId = fileId, .level = @intCast(level) });
|
|
}
|
|
|
|
pub fn deleteFile(self: *VersionEdit, fileId: u64) !void {
|
|
try self.deleted.append(fileId);
|
|
}
|
|
|
|
pub fn setNextFileId(self: *VersionEdit, nextFileId: u64) void {
|
|
self.nextFileId = nextFileId;
|
|
}
|
|
|
|
pub fn setNextSeq(self: *VersionEdit, nextSeq: u64) void {
|
|
self.nextSeq = nextSeq;
|
|
}
|
|
|
|
pub fn isEmpty(self: *const VersionEdit) bool {
|
|
return self.added.items.len == 0 and self.deleted.items.len == 0;
|
|
}
|
|
|
|
fn encode_(self: *const VersionEdit, out: *std.ArrayList(u8)) !void {
|
|
const writer = encode.ByteWriter.init(out);
|
|
try writer.putU32(@intCast(self.added.items.len));
|
|
for (self.added.items) |f| {
|
|
try writer.putU64(f.fileId);
|
|
try writer.putU32(f.level);
|
|
}
|
|
try writer.putU32(@intCast(self.deleted.items.len));
|
|
for (self.deleted.items) |id| try writer.putU64(id);
|
|
try writer.byte(if (self.nextFileId != null) 1 else 0);
|
|
if (self.nextFileId) |v| try writer.putU64(v);
|
|
try writer.byte(if (self.nextSeq != null) 1 else 0);
|
|
if (self.nextSeq) |v| try writer.putU64(v);
|
|
}
|
|
};
|
|
|
|
/// Applies an edit's added/deleted entries onto a live fileId->level map.
|
|
pub fn applyEdit(state: *std.AutoHashMap(u64, usize), added: []const FileEdit, deleted: []const u64) !void {
|
|
for (deleted) |id| _ = state.remove(id);
|
|
for (added) |f| try state.put(f.fileId, f.level);
|
|
}
|
|
|
|
/// Recovered counter watermarks, threaded out of `VersionSet.open`'s replay
|
|
/// so `Db.open` can fold them into its own directory-scan-based recovery
|
|
/// (taking the max of both sources - see `VersionEdit.nextFileId`/`nextSeq`
|
|
/// doc comment for why the scan alone is not always sufficient).
|
|
pub const Counters = struct {
|
|
nextFileId: ?u64 = null,
|
|
nextSeq: ?u64 = null,
|
|
};
|
|
|
|
const currentName = "CURRENT";
|
|
const manifestPrefix = "MANIFEST-";
|
|
|
|
fn manifestFileName(buf: []u8, id: u64) ![]const u8 {
|
|
return std.fmt.bufPrint(buf, "{s}{d:0>10}", .{ manifestPrefix, id });
|
|
}
|
|
|
|
pub const VersionSet = struct {
|
|
allocator: Allocator,
|
|
dir: std.fs.Dir,
|
|
file: std.fs.File,
|
|
manifestId: u64,
|
|
/// Number of edit records appended to the current log since the last
|
|
/// rotation (used as the rotation heuristic).
|
|
pendingRecords: usize,
|
|
/// Latest counter watermarks seen across every replayed (or since
|
|
/// applied) edit - see `VersionEdit.nextFileId`/`nextSeq`. Carried
|
|
/// forward into `rotate`'s snapshot record so they survive log rotation
|
|
/// even though the individual edits that set them get discarded.
|
|
counters: Counters,
|
|
|
|
/// Opens (or bootstraps) the version set, replaying the active
|
|
/// manifest log into a fresh fileId->level map that the caller owns.
|
|
pub fn open(allocator: Allocator, dir: std.fs.Dir) !struct { set: VersionSet, state: std.AutoHashMap(u64, usize) } {
|
|
var state = std.AutoHashMap(u64, usize).init(allocator);
|
|
errdefer state.deinit();
|
|
|
|
const currentContents = dir.readFileAlloc(allocator, currentName, 256) catch |err| switch (err) {
|
|
error.FileNotFound => {
|
|
const set = try bootstrap(allocator, dir);
|
|
return .{ .set = set, .state = state };
|
|
},
|
|
else => return err,
|
|
};
|
|
defer allocator.free(currentContents);
|
|
|
|
const trimmed = std.mem.trim(u8, currentContents, " \t\r\n");
|
|
const id = parseManifestId(trimmed) orelse return VersionError.Corrupt;
|
|
|
|
const file = dir.openFile(trimmed, .{ .mode = .read_write }) catch |err| switch (err) {
|
|
error.FileNotFound => {
|
|
const set = try bootstrap(allocator, dir);
|
|
return .{ .set = set, .state = state };
|
|
},
|
|
else => return err,
|
|
};
|
|
errdefer file.close();
|
|
|
|
const data = try file.readToEndAlloc(allocator, std.math.maxInt(usize));
|
|
defer allocator.free(data);
|
|
|
|
var pos: usize = 0;
|
|
var goodEnd: usize = 0;
|
|
var records: usize = 0;
|
|
var counters = Counters{};
|
|
while (pos + 8 <= data.len) {
|
|
const storedCrc = encode.readU32(data, pos);
|
|
const bodyStart = pos + 4;
|
|
const bodyLen = recordBodyLen(data, bodyStart) orelse break;
|
|
if (bodyStart + bodyLen > data.len) break;
|
|
const body = data[bodyStart .. bodyStart + bodyLen];
|
|
if (encode.crc(body) != storedCrc) break;
|
|
|
|
try replayRecord(&state, &counters, body);
|
|
records += 1;
|
|
pos = bodyStart + bodyLen;
|
|
goodEnd = pos;
|
|
}
|
|
|
|
if (goodEnd != data.len) {
|
|
try file.setEndPos(goodEnd);
|
|
try file.sync();
|
|
}
|
|
try file.seekFromEnd(0);
|
|
|
|
return .{
|
|
.set = .{
|
|
.allocator = allocator,
|
|
.dir = dir,
|
|
.file = file,
|
|
.manifestId = id,
|
|
.pendingRecords = records,
|
|
.counters = counters,
|
|
},
|
|
.state = state,
|
|
};
|
|
}
|
|
|
|
fn bootstrap(allocator: Allocator, dir: std.fs.Dir) !VersionSet {
|
|
var nameBuf: [64]u8 = undefined;
|
|
const name = try manifestFileName(&nameBuf, 1);
|
|
const file = try dir.createFile(name, .{ .read = true, .truncate = true });
|
|
errdefer file.close();
|
|
try writeCurrent(dir, name);
|
|
return .{
|
|
.allocator = allocator,
|
|
.dir = dir,
|
|
.file = file,
|
|
.manifestId = 1,
|
|
.pendingRecords = 0,
|
|
.counters = .{},
|
|
};
|
|
}
|
|
|
|
pub fn close(self: *VersionSet) void {
|
|
self.file.close();
|
|
}
|
|
|
|
/// Applies `edit` to `state` and durably appends it to the manifest
|
|
/// log. Rotates (compacts the log to a single snapshot record) once
|
|
/// the number of pending records grows past the live file count.
|
|
pub fn logAndApply(self: *VersionSet, edit: *const VersionEdit, state: *std.AutoHashMap(u64, usize)) !void {
|
|
if (edit.isEmpty()) return;
|
|
try applyEdit(state, edit.added.items, edit.deleted.items);
|
|
if (edit.nextFileId) |v| self.counters.nextFileId = v;
|
|
if (edit.nextSeq) |v| self.counters.nextSeq = v;
|
|
try self.appendRecord(edit);
|
|
self.pendingRecords += 1;
|
|
|
|
const liveCount = state.count();
|
|
if (self.pendingRecords > @max(liveCount, 4) * 2) {
|
|
try self.rotate(state);
|
|
}
|
|
}
|
|
|
|
fn appendRecord(self: *VersionSet, edit: *const VersionEdit) !void {
|
|
var body = std.ArrayList(u8).init(self.allocator);
|
|
defer body.deinit();
|
|
try edit.encode_(&body);
|
|
|
|
var frame = std.ArrayList(u8).init(self.allocator);
|
|
defer frame.deinit();
|
|
const frameWriter = encode.ByteWriter.init(&frame);
|
|
try frameWriter.putU32(encode.crc(body.items));
|
|
try frameWriter.slice(body.items);
|
|
|
|
try self.file.seekFromEnd(0);
|
|
try self.file.writeAll(frame.items);
|
|
try self.file.sync();
|
|
}
|
|
|
|
/// Replaces the whole log history with a single snapshot record that
|
|
/// reproduces the current live state, then switches CURRENT to it and
|
|
/// deletes the old log. Bounds manifest size to O(live files) instead
|
|
/// of O(edits ever applied).
|
|
fn rotate(self: *VersionSet, state: *const std.AutoHashMap(u64, usize)) !void {
|
|
var snapshot = std.ArrayList(FileEdit).init(self.allocator);
|
|
defer snapshot.deinit();
|
|
var it = state.iterator();
|
|
while (it.next()) |entry| {
|
|
try snapshot.append(.{ .fileId = entry.key_ptr.*, .level = @intCast(entry.value_ptr.*) });
|
|
}
|
|
|
|
const newId = self.manifestId + 1;
|
|
var nameBuf: [64]u8 = undefined;
|
|
const newName = try manifestFileName(&nameBuf, newId);
|
|
|
|
const newFile = try self.dir.createFile(newName, .{ .read = true, .truncate = true });
|
|
errdefer newFile.close();
|
|
|
|
{
|
|
var body = std.ArrayList(u8).init(self.allocator);
|
|
defer body.deinit();
|
|
const writer = encode.ByteWriter.init(&body);
|
|
try writer.putU32(@intCast(snapshot.items.len));
|
|
for (snapshot.items) |f| {
|
|
try writer.putU64(f.fileId);
|
|
try writer.putU32(f.level);
|
|
}
|
|
try writer.putU32(0);
|
|
// Carry the latest known counters forward into the snapshot so
|
|
// they survive rotation even though the edits that originally
|
|
// set them are being discarded along with the old log.
|
|
try writer.byte(if (self.counters.nextFileId != null) 1 else 0);
|
|
if (self.counters.nextFileId) |v| try writer.putU64(v);
|
|
try writer.byte(if (self.counters.nextSeq != null) 1 else 0);
|
|
if (self.counters.nextSeq) |v| try writer.putU64(v);
|
|
|
|
var frame = std.ArrayList(u8).init(self.allocator);
|
|
defer frame.deinit();
|
|
const frameWriter = encode.ByteWriter.init(&frame);
|
|
try frameWriter.putU32(encode.crc(body.items));
|
|
try frameWriter.slice(body.items);
|
|
try newFile.writeAll(frame.items);
|
|
try newFile.sync();
|
|
}
|
|
|
|
try writeCurrent(self.dir, newName);
|
|
|
|
var oldNameBuf: [64]u8 = undefined;
|
|
const oldName = try manifestFileName(&oldNameBuf, self.manifestId);
|
|
self.file.close();
|
|
self.dir.deleteFile(oldName) catch {};
|
|
|
|
self.file = newFile;
|
|
self.manifestId = newId;
|
|
self.pendingRecords = snapshot.items.len;
|
|
}
|
|
};
|
|
|
|
fn writeCurrent(dir: std.fs.Dir, manifestName: []const u8) !void {
|
|
const tmpName = "CURRENT.tmp";
|
|
{
|
|
const file = try dir.createFile(tmpName, .{ .truncate = true });
|
|
defer file.close();
|
|
try file.writeAll(manifestName);
|
|
try file.sync();
|
|
}
|
|
try dir.rename(tmpName, currentName);
|
|
}
|
|
|
|
fn parseManifestId(name: []const u8) ?u64 {
|
|
if (!std.mem.startsWith(u8, name, manifestPrefix)) return null;
|
|
return std.fmt.parseInt(u64, name[manifestPrefix.len..], 10) catch null;
|
|
}
|
|
|
|
fn recordBodyLen(data: []const u8, bodyStart: usize) ?usize {
|
|
if (bodyStart + 4 > data.len) return null;
|
|
const addedCount = encode.readU32(data, bodyStart);
|
|
var p = bodyStart + 4;
|
|
var i: u32 = 0;
|
|
while (i < addedCount) : (i += 1) {
|
|
if (p + 12 > data.len) return null;
|
|
p += 12;
|
|
}
|
|
if (p + 4 > data.len) return null;
|
|
const deletedCount = encode.readU32(data, p);
|
|
p += 4;
|
|
var j: u32 = 0;
|
|
while (j < deletedCount) : (j += 1) {
|
|
if (p + 8 > data.len) return null;
|
|
p += 8;
|
|
}
|
|
// Trailing counter watermarks: one presence flag byte plus an optional
|
|
// u64 each, for nextFileId then nextSeq.
|
|
var k: u32 = 0;
|
|
while (k < 2) : (k += 1) {
|
|
if (p + 1 > data.len) return null;
|
|
const present = data[p];
|
|
p += 1;
|
|
if (present != 0) {
|
|
if (p + 8 > data.len) return null;
|
|
p += 8;
|
|
}
|
|
}
|
|
return p - bodyStart;
|
|
}
|
|
|
|
fn replayRecord(state: *std.AutoHashMap(u64, usize), counters: *Counters, body: []const u8) !void {
|
|
const addedCount = encode.readU32(body, 0);
|
|
var p: usize = 4;
|
|
var i: u32 = 0;
|
|
while (i < addedCount) : (i += 1) {
|
|
const fileId = encode.readU64(body, p);
|
|
const level = encode.readU32(body, p + 8);
|
|
try state.put(fileId, level);
|
|
p += 12;
|
|
}
|
|
const deletedCount = encode.readU32(body, p);
|
|
p += 4;
|
|
var j: u32 = 0;
|
|
while (j < deletedCount) : (j += 1) {
|
|
const fileId = encode.readU64(body, p);
|
|
_ = state.remove(fileId);
|
|
p += 8;
|
|
}
|
|
if (p < body.len) {
|
|
const hasNextFileId = body[p];
|
|
p += 1;
|
|
if (hasNextFileId != 0) {
|
|
counters.nextFileId = encode.readU64(body, p);
|
|
p += 8;
|
|
}
|
|
const hasNextSeq = body[p];
|
|
p += 1;
|
|
if (hasNextSeq != 0) {
|
|
counters.nextSeq = encode.readU64(body, p);
|
|
p += 8;
|
|
}
|
|
}
|
|
}
|
|
|
|
test "fresh directory bootstraps an empty version set" {
|
|
const allocator = std.testing.allocator;
|
|
var tmp = std.testing.tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
var opened = try VersionSet.open(allocator, tmp.dir);
|
|
defer opened.state.deinit();
|
|
defer opened.set.close();
|
|
|
|
try std.testing.expectEqual(@as(usize, 0), opened.state.count());
|
|
try std.testing.expectEqual(@as(u64, 1), opened.set.manifestId);
|
|
try std.testing.expect(tmp.dir.access(currentName, .{}) != error.FileNotFound);
|
|
}
|
|
|
|
test "logAndApply records adds and survives reopen" {
|
|
const allocator = std.testing.allocator;
|
|
var tmp = std.testing.tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
{
|
|
var opened = try VersionSet.open(allocator, tmp.dir);
|
|
defer opened.state.deinit();
|
|
defer opened.set.close();
|
|
|
|
var edit = VersionEdit.init(allocator);
|
|
defer edit.deinit();
|
|
try edit.addFile(1, 0);
|
|
try edit.addFile(2, 0);
|
|
try opened.set.logAndApply(&edit, &opened.state);
|
|
|
|
try std.testing.expectEqual(@as(usize, 2), opened.state.count());
|
|
try std.testing.expectEqual(@as(usize, 0), opened.state.get(1).?);
|
|
}
|
|
|
|
var reopened = try VersionSet.open(allocator, tmp.dir);
|
|
defer reopened.state.deinit();
|
|
defer reopened.set.close();
|
|
try std.testing.expectEqual(@as(usize, 2), reopened.state.count());
|
|
try std.testing.expectEqual(@as(usize, 0), reopened.state.get(2).?);
|
|
}
|
|
|
|
test "logAndApply replaces a deleted file with new outputs (compaction pattern)" {
|
|
const allocator = std.testing.allocator;
|
|
var tmp = std.testing.tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
var opened = try VersionSet.open(allocator, tmp.dir);
|
|
defer opened.state.deinit();
|
|
defer opened.set.close();
|
|
|
|
var e1 = VersionEdit.init(allocator);
|
|
defer e1.deinit();
|
|
try e1.addFile(1, 0);
|
|
try e1.addFile(2, 0);
|
|
try opened.set.logAndApply(&e1, &opened.state);
|
|
|
|
var e2 = VersionEdit.init(allocator);
|
|
defer e2.deinit();
|
|
try e2.deleteFile(1);
|
|
try e2.deleteFile(2);
|
|
try e2.addFile(3, 1);
|
|
try opened.set.logAndApply(&e2, &opened.state);
|
|
|
|
try std.testing.expectEqual(@as(usize, 1), opened.state.count());
|
|
try std.testing.expectEqual(@as(usize, 1), opened.state.get(3).?);
|
|
try std.testing.expectEqual(@as(?usize, null), opened.state.get(1));
|
|
}
|
|
|
|
test "manifest rotates once pending records outgrow live file count" {
|
|
const allocator = std.testing.allocator;
|
|
var tmp = std.testing.tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
var opened = try VersionSet.open(allocator, tmp.dir);
|
|
defer opened.state.deinit();
|
|
defer opened.set.close();
|
|
|
|
// Simulate steady-state leveled compaction: the live file count stays
|
|
// fixed at 1 (each round replaces the previous output), but the edit
|
|
// log keeps growing until it is worth rotating to a fresh snapshot.
|
|
var fileId: u64 = 1;
|
|
{
|
|
var edit = VersionEdit.init(allocator);
|
|
defer edit.deinit();
|
|
try edit.addFile(fileId, 0);
|
|
try opened.set.logAndApply(&edit, &opened.state);
|
|
}
|
|
|
|
var n: usize = 0;
|
|
while (n < 20) : (n += 1) {
|
|
var edit = VersionEdit.init(allocator);
|
|
defer edit.deinit();
|
|
try edit.deleteFile(fileId);
|
|
fileId += 1;
|
|
try edit.addFile(fileId, 0);
|
|
try opened.set.logAndApply(&edit, &opened.state);
|
|
}
|
|
|
|
try std.testing.expect(opened.set.manifestId > 1);
|
|
try std.testing.expectEqual(@as(usize, 1), opened.state.count());
|
|
try std.testing.expectEqual(@as(usize, 0), opened.state.get(fileId).?);
|
|
|
|
var nameBuf: [64]u8 = undefined;
|
|
const oldName = try manifestFileName(&nameBuf, 1);
|
|
try std.testing.expectError(error.FileNotFound, tmp.dir.access(oldName, .{}));
|
|
}
|
|
|
|
test "reopen after rotation still reflects live state" {
|
|
const allocator = std.testing.allocator;
|
|
var tmp = std.testing.tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
var lastId: u64 = 0;
|
|
var finalFileId: u64 = 1;
|
|
{
|
|
var opened = try VersionSet.open(allocator, tmp.dir);
|
|
defer opened.state.deinit();
|
|
defer opened.set.close();
|
|
|
|
{
|
|
var edit = VersionEdit.init(allocator);
|
|
defer edit.deinit();
|
|
try edit.addFile(finalFileId, 0);
|
|
try opened.set.logAndApply(&edit, &opened.state);
|
|
}
|
|
var n: usize = 0;
|
|
while (n < 12) : (n += 1) {
|
|
var edit = VersionEdit.init(allocator);
|
|
defer edit.deinit();
|
|
try edit.deleteFile(finalFileId);
|
|
finalFileId += 1;
|
|
try edit.addFile(finalFileId, 0);
|
|
try opened.set.logAndApply(&edit, &opened.state);
|
|
}
|
|
lastId = opened.set.manifestId;
|
|
}
|
|
|
|
try std.testing.expect(lastId > 1);
|
|
|
|
var reopened = try VersionSet.open(allocator, tmp.dir);
|
|
defer reopened.state.deinit();
|
|
defer reopened.set.close();
|
|
try std.testing.expectEqual(@as(usize, 1), reopened.state.count());
|
|
try std.testing.expectEqual(@as(usize, 0), reopened.state.get(finalFileId).?);
|
|
try std.testing.expectEqual(reopened.set.manifestId, lastId);
|
|
}
|
|
|
|
test "torn trailing record is ignored on recovery" {
|
|
const allocator = std.testing.allocator;
|
|
var tmp = std.testing.tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
{
|
|
var opened = try VersionSet.open(allocator, tmp.dir);
|
|
defer opened.state.deinit();
|
|
defer opened.set.close();
|
|
|
|
var edit = VersionEdit.init(allocator);
|
|
defer edit.deinit();
|
|
try edit.addFile(7, 0);
|
|
try opened.set.logAndApply(&edit, &opened.state);
|
|
|
|
try opened.set.file.seekFromEnd(0);
|
|
try opened.set.file.writeAll(&[_]u8{ 0xde, 0xad, 0xbe });
|
|
try opened.set.file.sync();
|
|
}
|
|
|
|
var reopened = try VersionSet.open(allocator, tmp.dir);
|
|
defer reopened.state.deinit();
|
|
defer reopened.set.close();
|
|
try std.testing.expectEqual(@as(usize, 1), reopened.state.count());
|
|
try std.testing.expectEqual(@as(usize, 0), reopened.state.get(7).?);
|
|
}
|
|
|
|
test "logAndApply persists nextFileId and nextSeq counters across reopen" {
|
|
const allocator = std.testing.allocator;
|
|
var tmp = std.testing.tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
{
|
|
var opened = try VersionSet.open(allocator, tmp.dir);
|
|
defer opened.state.deinit();
|
|
defer opened.set.close();
|
|
try std.testing.expectEqual(@as(?u64, null), opened.set.counters.nextFileId);
|
|
|
|
var edit = VersionEdit.init(allocator);
|
|
defer edit.deinit();
|
|
try edit.addFile(1, 0);
|
|
edit.setNextFileId(2);
|
|
edit.setNextSeq(500);
|
|
try opened.set.logAndApply(&edit, &opened.state);
|
|
|
|
try std.testing.expectEqual(@as(?u64, 2), opened.set.counters.nextFileId);
|
|
try std.testing.expectEqual(@as(?u64, 500), opened.set.counters.nextSeq);
|
|
}
|
|
|
|
var reopened = try VersionSet.open(allocator, tmp.dir);
|
|
defer reopened.state.deinit();
|
|
defer reopened.set.close();
|
|
try std.testing.expectEqual(@as(?u64, 2), reopened.set.counters.nextFileId);
|
|
try std.testing.expectEqual(@as(?u64, 500), reopened.set.counters.nextSeq);
|
|
}
|
|
|
|
test "counters survive manifest rotation" {
|
|
const allocator = std.testing.allocator;
|
|
var tmp = std.testing.tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
var lastId: u64 = 0;
|
|
{
|
|
var opened = try VersionSet.open(allocator, tmp.dir);
|
|
defer opened.state.deinit();
|
|
defer opened.set.close();
|
|
|
|
var fileId: u64 = 1;
|
|
{
|
|
var edit = VersionEdit.init(allocator);
|
|
defer edit.deinit();
|
|
try edit.addFile(fileId, 0);
|
|
edit.setNextFileId(fileId + 1);
|
|
edit.setNextSeq(10);
|
|
try opened.set.logAndApply(&edit, &opened.state);
|
|
}
|
|
// Force rotation the same way the existing rotation test does, each
|
|
// round bumping the seq counter so the final persisted value is
|
|
// distinguishable from an earlier one if rotation dropped it.
|
|
var n: usize = 0;
|
|
while (n < 20) : (n += 1) {
|
|
var edit = VersionEdit.init(allocator);
|
|
defer edit.deinit();
|
|
try edit.deleteFile(fileId);
|
|
fileId += 1;
|
|
try edit.addFile(fileId, 0);
|
|
edit.setNextFileId(fileId + 1);
|
|
edit.setNextSeq(10 + n + 1);
|
|
try opened.set.logAndApply(&edit, &opened.state);
|
|
}
|
|
lastId = opened.set.manifestId;
|
|
try std.testing.expectEqual(@as(?u64, fileId + 1), opened.set.counters.nextFileId);
|
|
try std.testing.expectEqual(@as(?u64, 30), opened.set.counters.nextSeq);
|
|
}
|
|
try std.testing.expect(lastId > 1);
|
|
|
|
var reopened = try VersionSet.open(allocator, tmp.dir);
|
|
defer reopened.state.deinit();
|
|
defer reopened.set.close();
|
|
try std.testing.expectEqual(@as(?u64, 30), reopened.set.counters.nextSeq);
|
|
}
|
|
|
|
test "missing CURRENT after previous run bootstraps fresh, empty state" {
|
|
const allocator = std.testing.allocator;
|
|
var tmp = std.testing.tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
{
|
|
var opened = try VersionSet.open(allocator, tmp.dir);
|
|
defer opened.state.deinit();
|
|
defer opened.set.close();
|
|
var edit = VersionEdit.init(allocator);
|
|
defer edit.deinit();
|
|
try edit.addFile(1, 0);
|
|
try opened.set.logAndApply(&edit, &opened.state);
|
|
}
|
|
|
|
try tmp.dir.deleteFile(currentName);
|
|
|
|
var reopened = try VersionSet.open(allocator, tmp.dir);
|
|
defer reopened.state.deinit();
|
|
defer reopened.set.close();
|
|
try std.testing.expectEqual(@as(usize, 0), reopened.state.count());
|
|
}
|