feat(store): add leveled compaction

This commit is contained in:
dualfroz committed 2026-08-25 22:40:00 +02:00
1 parent 57360650ba
commit f11ba18b9d
1 file changed
+227
+227
View File
@@ -238,3 +238,230 @@ pub const Db = struct {
std.posix.fsync(self.dir.fd) catch {};
}
pub fn compact(self: *Db) !void {
if (self.sstables.items.len <= 1) return;
const fileId = self.nextFileId;
var nameBuf: [64]u8 = undefined;
const name = try sstName(&nameBuf, fileId);
var expected: usize = 0;
for (self.sstables.items) |reader| expected += @intCast(reader.numEntries);
const cursors = try self.allocator.alloc(sstable.Cursor, self.sstables.items.len);
defer self.allocator.free(cursors);
for (self.sstables.items, 0..) |reader, i| cursors[i] = reader.cursor(null);
{
var builder = try Builder.init(self.allocator, self.dir, name, expected);
defer builder.deinit();
while (pickWinner(cursors)) |winner| {
if (winner.kind == .value) try builder.add(winner);
}
try builder.finish();
}
const reader = try self.allocator.create(Reader);
errdefer self.allocator.destroy(reader);
reader.* = try Reader.open(self.allocator, self.dir, name, fileId);
errdefer reader.deinit();
for (self.sstables.items) |old| {
var oldNameBuf: [64]u8 = undefined;
const oldName = sstName(&oldNameBuf, old.fileId) catch unreachable;
old.deinit();
self.allocator.destroy(old);
self.dir.deleteFile(oldName) catch {};
}
self.sstables.clearRetainingCapacity();
reader.level = 0;
try self.sstables.append(reader);
self.nextFileId += 1;
try self.writeManifest();
std.posix.fsync(self.dir.fd) catch {};
}
pub fn maybeCompact(self: *Db) !void {
while (true) {
if (self.countLevel(0) >= self.l0Trigger) {
try self.compactL0();
continue;
}
var acted = false;
var level: usize = 1;
const bottom = self.maxDataLevel();
while (level <= bottom) : (level += 1) {
if (self.levelSize(level) > self.levelLimit(level)) {
try self.compactDown(level);
acted = true;
break;
}
}
if (!acted) break;
}
}
fn compactL0(self: *Db) !void {
var inputs = std.ArrayList(*Reader).init(self.allocator);
defer inputs.deinit();
var lo: ?[]const u8 = null;
var hi: ?[]const u8 = null;
for (self.sstables.items) |reader| {
if (reader.level != 0) continue;
try inputs.append(reader);
if (reader.index.len == 0) continue;
if (lo == null or order(reader.minKey, lo.?) == .lt) lo = reader.minKey;
if (hi == null or order(reader.maxKey, hi.?) == .gt) hi = reader.maxKey;
}
if (inputs.items.len == 0) return;
if (lo) |lowBound| {
for (self.sstables.items) |reader| {
if (reader.level != 1 or reader.index.len == 0) continue;
if (rangesOverlap(reader.minKey, reader.maxKey, lowBound, hi.?)) try inputs.append(reader);
}
}
const dropTombstones = !self.hasDataBelow(1);
try self.runMerge(inputs.items, 1, dropTombstones);
}
fn compactDown(self: *Db, level: usize) !void {
var picked: ?*Reader = null;
for (self.sstables.items) |reader| {
if (reader.level != level or reader.index.len == 0) continue;
if (picked == null or order(reader.minKey, picked.?.minKey) == .lt) picked = reader;
}
if (picked == null) return;
var inputs = std.ArrayList(*Reader).init(self.allocator);
defer inputs.deinit();
try inputs.append(picked.?);
const target = level + 1;
for (self.sstables.items) |reader| {
if (reader.level != target or reader.index.len == 0) continue;
if (rangesOverlap(reader.minKey, reader.maxKey, picked.?.minKey, picked.?.maxKey)) try inputs.append(reader);
}
const dropTombstones = !self.hasDataBelow(target);
try self.runMerge(inputs.items, target, dropTombstones);
}
fn runMerge(self: *Db, inputs: []*Reader, targetLevel: usize, dropTombstones: bool) !void {
const cursors = try self.allocator.alloc(sstable.Cursor, inputs.len);
defer self.allocator.free(cursors);
for (inputs, 0..) |reader, i| cursors[i] = reader.cursor(null);
var outputs = std.ArrayList(*Reader).init(self.allocator);
errdefer {
for (outputs.items) |produced| {
produced.deinit();
self.allocator.destroy(produced);
}
outputs.deinit();
}
var estimate: usize = 0;
for (inputs) |reader| estimate += @intCast(reader.numEntries);
if (estimate == 0) estimate = 1;
var builder: ?Builder = null;
var outputId: u64 = 0;
var outputName: [64]u8 = undefined;
errdefer if (builder) |*b| b.deinit();
while (pickWinner(cursors)) |winner| {
if (dropTombstones and winner.kind == .tombstone) continue;
if (builder == null) {
outputId = self.nextFileId;
self.nextFileId += 1;
const name = try sstName(&outputName, outputId);
builder = try Builder.init(self.allocator, self.dir, name, estimate);
}
try builder.?.add(winner);
if (builder.?.logicalBytes >= self.sstableTargetBytes) {
const done = builder.?;
builder = null;
try self.finishOutput(done, outputId, targetLevel, &outputs);
}
}
if (builder) |done| {
builder = null;
try self.finishOutput(done, outputId, targetLevel, &outputs);
}
for (inputs) |old| {
self.removeReader(old);
var oldNameBuf: [64]u8 = undefined;
const oldName = sstName(&oldNameBuf, old.fileId) catch unreachable;
old.deinit();
self.allocator.destroy(old);
self.dir.deleteFile(oldName) catch {};
}
for (outputs.items) |produced| try self.sstables.append(produced);
outputs.deinit();
try self.writeManifest();
std.posix.fsync(self.dir.fd) catch {};
}
fn finishOutput(self: *Db, builderIn: Builder, fileId: u64, targetLevel: usize, outputs: *std.ArrayList(*Reader)) !void {
var builder = builderIn;
{
errdefer builder.deinit();
try builder.finish();
}
builder.deinit();
var nameBuf: [64]u8 = undefined;
const name = try sstName(&nameBuf, fileId);
const reader = try self.allocator.create(Reader);
errdefer self.allocator.destroy(reader);
reader.* = try Reader.open(self.allocator, self.dir, name, fileId);
errdefer reader.deinit();
reader.level = targetLevel;
try outputs.append(reader);
}
fn removeReader(self: *Db, target: *Reader) void {
for (self.sstables.items, 0..) |reader, i| {
if (reader == target) {
_ = self.sstables.orderedRemove(i);
return;
}
}
}
fn countLevel(self: *Db, level: usize) usize {
var n: usize = 0;
for (self.sstables.items) |reader| {
if (reader.level == level) n += 1;
}
return n;
}
fn levelSize(self: *Db, level: usize) usize {
var total: usize = 0;
for (self.sstables.items) |reader| {
if (reader.level == level) total += reader.buffer.len;
}
return total;
}
fn levelLimit(self: *Db, level: usize) usize {
var limit = self.l1BaseBytes;
var i: usize = 1;
while (i < level) : (i += 1) limit *= self.levelMultiplier;
return limit;
}
fn maxDataLevel(self: *Db) usize {
var maxLevel: usize = 0;
for (self.sstables.items) |reader| {
if (reader.level > maxLevel) maxLevel = reader.level;
}
return maxLevel;
}
fn hasDataBelow(self: *Db, level: usize) bool {
for (self.sstables.items) |reader| {
if (reader.level > level and reader.numEntries > 0) return true;
}
return false;
}