From fba93d0f3ae34b0ab00dc1a095afa8f26e453cee Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Sat, 8 Aug 2026 11:53:52 -0500 Subject: [PATCH] compact: header-level watermark skip in foldSegment (upstream spec 3.4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit the startup tombstone rebuild read every sealed segment in full (readFileAlloc, ~1.7s each) before block-level seq pruning could reject it — hours on the production archive, the '3h restart pause'. upstream's rebuildLiveTombstones skips a segment whose header max_seq is at or below the watermark without reading its blocks: rebuild cost scales with the watermark backlog, not the archive. port that skip into foldSegment, which serves both the startup rebuild and steady chunk folds. test mirrors upstream TestRebuildLiveTombstones_BoundedByWatermark and goes one step further: the covered segment is truncated to its bare header, so the test fails if any code path reads its blocks. Co-Authored-By: Claude Fable 5 --- src/internal/compact/pass.zig | 12 +++++++ src/internal/compact/steady.zig | 58 +++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/src/internal/compact/pass.zig b/src/internal/compact/pass.zig index 5b334bc..d51f3da 100644 --- a/src/internal/compact/pass.zig +++ b/src/internal/compact/pass.zig @@ -279,6 +279,18 @@ pub fn foldSegment( ) !void { var name_buf: [64]u8 = undefined; const name = archive_mod.formatSegmentName(&name_buf, idx); + // Header-level watermark skip (upstream compact_deletes.go rebuildLive- + // Tombstones): a segment wholly at or below the watermark is already + // physically compacted and can contribute nothing. Deciding that from + // the header alone keeps rebuild cost scaled to the watermark backlog, + // not the archive — without it, startup re-reads every sealed segment + // in full (~1.7s each, hours on the production archive). + if (low_exclusive > 0) { + var file = try seg_dir.openFile(io, name, .{}); + defer file.close(io); + const header = try segment.readHeaderFile(io, &file); + if (header.max_seq <= low_exclusive) return; + } const bytes = try seg_dir.readFileAlloc(io, name, allocator, .limited(1 << 31)); defer allocator.free(bytes); var sealed = try segment.Sealed.parse(allocator, bytes); diff --git a/src/internal/compact/steady.zig b/src/internal/compact/steady.zig index 3b68d48..c444951 100644 --- a/src/internal/compact/steady.zig +++ b/src/internal/compact/steady.zig @@ -381,6 +381,64 @@ test "steady: hook feeds live set, runOnce force-rotates, compacts, evicts" { try testing.expectEqual(@as(usize, 0), c2.live.len()); } +test "steady: rebuild is bounded by the watermark (upstream spec 3.4)" { + // mirrors upstream TestRebuildLiveTombstones_BoundedByWatermark: the + // rebuilt set equals the fold over (compaction/seq, tip] — segments + // entirely at or below the watermark contribute nothing, decided from + // the header without reading their blocks. + var threaded: Io.Threaded = .init(testing.allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + var path_buf: [Io.Dir.max_path_bytes]u8 = undefined; + const n = try tmp.dir.realPath(io, &path_buf); + var data_dir_buf: [Io.Dir.max_path_bytes]u8 = undefined; + const data_dir = try std.fmt.bufPrint(&data_dir_buf, "{s}/data", .{path_buf[0..n]}); + + var a = try archive_mod.Archive.init(testing.allocator, io, data_dir); + defer a.deinit(); + var meta = try meta_store.Store.open(testing.allocator, data_dir); + defer meta.deinit(); + + // segment A (seqs 1-2): create + covering delete for did:plc:a + _ = try a.append(row(.create, "did:plc:a", "r", "x"), 1); + _ = try a.append(row(.delete, "did:plc:a", "r", ""), 2); + try a.rotate(); + // segment B (seq 3): delete marker for did:plc:b + _ = try a.append(row(.delete, "did:plc:b", "r", ""), 3); + try a.rotate(); + + // watermark covers all of segment A + var wm = watermark.Store.init(&meta); + defer wm.deinit(io); + try wm.save(io, 2); + + // truncate A to its bare header: the rebuild passes only if it decides + // "wholly below watermark" from the header alone — any block read of A + // now fails, so the block-pruning-after-full-read shape cannot sneak back + { + var seg_dir_buf: [Io.Dir.max_path_bytes]u8 = undefined; + const seg_path = try std.fmt.bufPrint(&seg_dir_buf, "{s}/segments/seg_0000000000.jss", .{data_dir}); + var f = try Io.Dir.cwd().openFile(io, seg_path, .{ .mode = .read_write }); + defer f.close(io); + try f.setLength(io, segment.header_size); + } + + var c = Compactor.init(testing.allocator, io, &a, &meta, data_dir); + defer c.deinit(); + try c.rebuild(); + + // only segment B's marker survives; A's is already physically applied + try testing.expectEqual(@as(usize, 1), c.live.len()); + var probe_a = row(.create, "did:plc:a", "r", "x"); + probe_a.seq = 1; + try testing.expectEqual(@as(?tombstone.DropReason, null), c.live.dropReason(probe_a)); + var probe_b = row(.create, "did:plc:b", "r", "x"); + probe_b.seq = 1; + try testing.expectEqual(@as(?tombstone.DropReason, .record), c.live.dropReason(probe_b)); +} + test "steady: cap sets the coalesced trigger" { var threaded: Io.Threaded = .init(testing.allocator, .{}); defer threaded.deinit(); -- 2.51.2