feat: add bootstrap and close to version

This commit is contained in:
dualfroz committed 2026-09-13 11:36:05 +02:00
1 parent d2ea602ceb
commit 38078e8106
1 file changed
+160
+160
View File
@@ -154,3 +154,163 @@ pub const VersionSet = struct {
const data = try file.readToEndAlloc(allocator, std.math.maxInt(usize)); const data = try file.readToEndAlloc(allocator, std.math.maxInt(usize));
defer allocator.free(data); 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);
}