pager: copy-on-write above the stable mark
The invariant everything else in the crash story rests on (PLAN amendment A1): no page belonging to the last published image is ever stored into, so recovery is `image + replay(seq > watermark)` and the image's bytes are exactly what the watermark described. `page_mut_cow` takes a *pointer to the owner's page number*. That is the load- bearing detail: copy-on-write relocates the page, so the owner has to be told, and a second reference would still aim at the abandoned copy. For the B+tree the owner is the id->page table slot -- which is precisely why node ids are not page numbers. Inert until a checkpoint publishes something, since the stable mark starts at zero. Append-only consumers keep writing in place, except that a checkpoint landing mid-extent freezes the page their tail points into, so both slabs now start a fresh extent rather than writing inside the image. Waste is bounded by one extent per collection per checkpoint. The free list is wired into allocation, which it was not before: copy-on-write abandons every page it touches in every generation, so without reuse the file grows by `generations x touched_set` without bound. That is the difference between a free list being defense-in-depth and being a prerequisite (A2). -- Three things I got wrong on the way, all worth recording. I added a `p >= stable_pages` assert to `page_mut` and had to take it back out. A page recycled off the free list *is* below the mark and *is* legitimately writable -- freed two generations ago, referenced by no live image -- so the page number alone cannot tell a violation from a reuse. The invariant is enforced the two ways A1 actually describes: structurally through `page_mut_cow`, and mechanically through mprotect. The comment says so, since the assert looks like an obvious thing to add. The watermark slots needed a narrow exception, because overwriting the inactive slot is the publication mechanism rather than a violation. It is a separate non-public accessor that asserts its argument is a slot, so it cannot become a general escape hatch. And the mprotect belt: `std.posix.mprotect` does not exist in Zig 0.16, so it is a libc call. It compiled only in ReleaseFast, where the branch is comptime- eliminated -- ReleaseSafe caught that immediately, which is the argument for running both. What the belt's test asserts is that the protection is really applied, not that a violating write faults. A SIGSEGV cannot be caught in-process, and the fault is the OS's behaviour rather than this code's; an mprotect that failed silently would leave a belt that looks present and does nothing, which is the failure worth guarding here. Stated in the test rather than implied. Mutation-checked, all red: COW returning without copying; copying without moving the slot; copying when already above the mark; never reusing a freed page.
This commit is contained in:
@@ -453,7 +453,10 @@ pub const Index = struct {
|
||||
if (rec_len > inline_limit) overflow_bytes += rec_len;
|
||||
}
|
||||
if (overflow_bytes == 0) return;
|
||||
if (self.ovf_tail + overflow_bytes <= self.ovf_end) return;
|
||||
// Same rule as the document slab: a checkpoint freezes the page the tail
|
||||
// points into, so a frozen tail means starting a fresh extent rather
|
||||
// than writing inside the durable image.
|
||||
if (self.ovf_tail >= self.pager.stable_bytes() and self.ovf_tail + overflow_bytes <= self.ovf_end) return;
|
||||
// One extent for the whole batch, or a bespoke one when a single
|
||||
// record is larger than the standard extent (a BSON string reaches
|
||||
// 16 MB).
|
||||
@@ -972,8 +975,15 @@ pub const Index = struct {
|
||||
}
|
||||
|
||||
/// The page holding node `id`, for writing.
|
||||
/// Writable, via copy-on-write: a node inside the published image is copied
|
||||
/// to a fresh page and its table slot updated, so the durable bytes are
|
||||
/// never disturbed. Infallible in practice because `reserve_for` reserves
|
||||
/// the pages a batch can need -- the `catch` here would mean the reservation
|
||||
/// was short, which its own assert reports first and more precisely.
|
||||
inline fn page_mut(self: *Index, id: u32) *Node {
|
||||
return @ptrCast(self.pager.page_mut(self.node_pages.items[id]));
|
||||
const p = self.pager.page_mut_cow(&self.node_pages.items[id]) catch
|
||||
@panic("multiforadb: out of pages while writing an index node");
|
||||
return @ptrCast(p);
|
||||
}
|
||||
|
||||
/// Overflow-slab bytes in `[from, to)`. Slot offsets are u64 because a
|
||||
|
||||
Reference in New Issue
Block a user