Groundwork for M0: the node arena and overflow slab are about to move into an mmap'd data file where a write to a page belonging to the last durable checkpoint has to copy that page first (PLAN amendment A1). Two changes make that a small commit rather than a sixty-site one, plus the reformat of this file (see the preceding style commit for why it rides along here). Accessors. Every read of a node page now goes through page(), every write through page_mut(), and every overflow read through ovf(); nothing else touches nodes.items or overflow.items. Which of the 55 sites mutate was decided by the compiler rather than by inspection -- page() returns *const Node, so every mutating site failed to compile until flipped -- and the result is that the copy-on-write hook has exactly one home. Records the rule COW will impose (never hold a *Node across a page_mut of the same id) and the audit showing today's callers already comply. Comptime layout asserts. These structures are about to become an on-disk format, and nothing pinned them. Pinning also surfaced that @sizeOf(Slot) is 32, not the 20 its 160 declared bits suggest -- the backing integer's 16-byte alignment rounds it up, so 12 of every 32 slot bytes are padding and a node holds 127 slots where 203 would fit. Pinned, deliberately not fixed: narrowing the slot changes the fanout and so the on-disk shape of every index, which belongs in the commit that reshapes leaf records. reserve_for. The old bound stood in for "levels a batch can add" with n/8, which is ~125 levels for a 1000-entry batch and demands ~528 MiB of headroom. Growing by g levels needs at least 2^g entries, so log2_ceil(n+1)+1 bounds it, giving ~70 MiB for that batch. Harmless as ArrayList capacity; real file growth once the arena is file-backed. Overrunning the reservation is a buffer overrun on a path that has already appended to the log and cannot report failure, so alloc_node and store_record now assert, using assert.zig so the checks survive ReleaseFast. Mutation-checked by dropping the reservation entirely: six tests go red with the new message. Worth noting the assert guards the allocation, not the arithmetic -- ensureUnusedCapacity over-allocates, so a slightly-too-small bound is masked until the reservation becomes exact. build.zig gains a `fuzz` step. spill, spill2, stress and fuzz_split were in no build step and are not in lib.zig's test block, so `zig build test` could not see an API break in the only coverage for records past the inline limit and for randomized split/remove interleavings -- exactly what this work puts at risk.
79 lines
2.9 KiB
Zig
79 lines
2.9 KiB
Zig
const std = @import("std");
|
|
|
|
pub fn build(b: *std.Build) void {
|
|
const target = b.standardTargetOptions(.{});
|
|
// ReleaseFast by default: a Debug server is 10-200x slower (measured in
|
|
// tests/e2e/compare-run.sh). Devs can still opt into Debug or
|
|
// ReleaseSafe with -Doptimize=Debug / -Doptimize=ReleaseSafe.
|
|
const optimize = b.option(std.builtin.OptimizeMode, "optimize", "Prioritize performance, safety, or binary size") orelse .ReleaseFast;
|
|
|
|
const lib_mod = b.createModule(.{
|
|
.root_source_file = b.path("src/lib.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
});
|
|
|
|
const exe_mod = b.createModule(.{
|
|
.root_source_file = b.path("src/main.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
.imports = &.{
|
|
.{ .name = "mongo", .module = lib_mod },
|
|
},
|
|
});
|
|
|
|
const exe = b.addExecutable(.{
|
|
.name = "multiforadb",
|
|
.root_module = exe_mod,
|
|
});
|
|
b.installArtifact(exe);
|
|
|
|
const run_cmd = b.addRunArtifact(exe);
|
|
run_cmd.step.dependOn(b.getInstallStep());
|
|
if (b.args) |args| run_cmd.addArgs(args);
|
|
const run_step = b.step("run", "Run multiforadb server");
|
|
run_step.dependOn(&run_cmd.step);
|
|
|
|
const test_mod = b.createModule(.{
|
|
.root_source_file = b.path("src/lib.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
});
|
|
|
|
const test_step = b.addTest(.{
|
|
.root_module = test_mod,
|
|
});
|
|
const run_tests = b.addRunArtifact(test_step);
|
|
const test_help = b.step("test", "Run unit tests");
|
|
test_help.dependOn(&run_tests.step);
|
|
|
|
// The B+tree harnesses were in no build step, so `zig build test` -- which
|
|
// only compiles src/lib.zig's test block -- could not see an API break in
|
|
// them. They are also the only coverage for records past the inline limit
|
|
// and for randomized split/remove interleavings, i.e. exactly what M0's
|
|
// arena work puts at risk. Wire them up so they cannot rot unnoticed.
|
|
//
|
|
// Kept out of `test` because stress.zig runs for seconds and the three
|
|
// main harnesses print rather than assert-and-exit; `zig build fuzz` is
|
|
// the gate to run alongside the e2e matrix on any index change.
|
|
const fuzz_step = b.step("fuzz", "Run the B+tree stress and fuzz harnesses");
|
|
|
|
const fuzz_split_mod = b.createModule(.{
|
|
.root_source_file = b.path("src/fuzz_split.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
});
|
|
const fuzz_split = b.addTest(.{ .root_module = fuzz_split_mod });
|
|
fuzz_step.dependOn(&b.addRunArtifact(fuzz_split).step);
|
|
|
|
for ([_][]const u8{ "spill", "spill2", "stress" }) |name| {
|
|
const mod = b.createModule(.{
|
|
.root_source_file = b.path(b.fmt("src/{s}.zig", .{name})),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
});
|
|
const harness = b.addExecutable(.{ .name = name, .root_module = mod });
|
|
fuzz_step.dependOn(&b.addRunArtifact(harness).step);
|
|
}
|
|
}
|