From 38078e810663fc1bf0d89088a5aef9df39efcd3f Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 13 Sep 2026 11:36:05 +0200 Subject: [PATCH] feat: add bootstrap and close to version --- src/version.zig | 160 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/src/version.zig b/src/version.zig index f14b056..8731a36 100644 --- a/src/version.zig +++ b/src/version.zig @@ -154,3 +154,163 @@ pub const VersionSet = struct { 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); +} +