pager: a page reservation belongs to its consumer, not to the pager
The promise `reserve_pages` makes was a single counter on the pager, and the
first concurrent benchmark since the data file landed aborted the server on
it, reliably, at four clients:
assertion failed: page allocation overran reserve_pages' promise
src/index.zig:955 in alloc_node
src/db.zig:794 in upsert
Two upserts on different collections hold different collection locks, so they
run at the same time. Each ends by dropping "whatever is still promised" --
and `release_reservation` zeroed the shared counter, so the first to publish
released the second's promise while the second was still between its log
append and its supposedly infallible allocation. The tripwire fired, which is
the good outcome; the bad one is a growth that never happened and a store past
the mapped end.
This is PLAN risk 3 ("a shared pager makes alloc_tail and free_pending a
global mutex on every insert"), whose mitigation -- private pre-allocated runs
-- was never built. So: `pager.Reservation` is a per-consumer promise, held by
every Index, every Collection (for its doc slab) and the checkpoint, and each
one releases only its own. The pager keeps the sum, which is all `grow_to`
needs. `Engine.release_write_reservations` drops exactly the buckets one
upsert reserved through.
The allocator's own state -- the tail, the total, the free lists, the
unpublished set, file growth -- is now behind `alloc_lock`, taken
uncancelable. It is never held across the log append: that is precisely what
per-consumer reservations buy, and why group commit is unaffected.
concurrent durable insertOne 4 clients 21697 docs/s (was aborting)
16 clients 30678 docs/s
Mutation: make `release_reservation` zero `self.reserved_pages` again. Red on
the new pager test and on three command tests.
This commit is contained in:
@@ -217,6 +217,10 @@ pub const Index = struct {
|
||||
/// `Slot.off` means two different things depending on `spill` is unchanged,
|
||||
/// only the second meaning moved.
|
||||
ovf_extents: std.ArrayListUnmanaged(pgr.Extent),
|
||||
/// This index's outstanding page promise. Per index rather than per pager
|
||||
/// because two collections are written concurrently and each one's promise
|
||||
/// has to survive the other's release -- see `pager.Reservation`.
|
||||
hold: pgr.Reservation,
|
||||
ovf_tail: u64,
|
||||
ovf_end: u64,
|
||||
/// Bulk-build staging: entries appended unsorted by append_doc_entries,
|
||||
@@ -255,6 +259,7 @@ pub const Index = struct {
|
||||
.pager = pager,
|
||||
.node_pages = .empty,
|
||||
.ovf_extents = .empty,
|
||||
.hold = .{},
|
||||
.ovf_tail = 0,
|
||||
.ovf_end = 0,
|
||||
.staging = .empty,
|
||||
@@ -281,9 +286,9 @@ pub const Index = struct {
|
||||
// Slot 0 is a dummy (0 is the null node id); the root is one empty
|
||||
// leaf, so a fresh index is always a valid tree.
|
||||
try self.node_pages.ensureUnusedCapacity(gpa, 2);
|
||||
try pager.reserve_pages(2);
|
||||
self.node_pages.appendAssumeCapacity(pager.alloc_pages_assume_reserved(1));
|
||||
self.node_pages.appendAssumeCapacity(pager.alloc_pages_assume_reserved(1));
|
||||
try pager.reserve_pages(&self.hold, 2);
|
||||
self.node_pages.appendAssumeCapacity(pager.alloc_pages_assume_reserved(&self.hold, 1));
|
||||
self.node_pages.appendAssumeCapacity(pager.alloc_pages_assume_reserved(&self.hold, 1));
|
||||
self.page_mut(0).* = empty_node(0);
|
||||
self.page_mut(1).* = empty_node(1);
|
||||
self.root = 1;
|
||||
@@ -435,7 +440,7 @@ pub const Index = struct {
|
||||
// a slot in the id->page table, and the insertion after the log append
|
||||
// must not be able to fail on either.
|
||||
try self.node_pages.ensureUnusedCapacity(gpa, @intCast(extra_nodes));
|
||||
try self.pager.reserve_pages(@intCast(extra_nodes));
|
||||
try self.pager.reserve_pages(&self.hold, @intCast(extra_nodes));
|
||||
try self.reserve_overflow(gpa, entries);
|
||||
}
|
||||
|
||||
@@ -467,8 +472,8 @@ pub const Index = struct {
|
||||
ovf_extent_pages,
|
||||
(overflow_bytes + pgr.page_size - 1) / pgr.page_size,
|
||||
));
|
||||
try self.pager.reserve_pages(want_pages);
|
||||
const first = self.pager.alloc_pages_assume_reserved(want_pages);
|
||||
try self.pager.reserve_pages(&self.hold, want_pages);
|
||||
const first = self.pager.alloc_pages_assume_reserved(&self.hold, want_pages);
|
||||
try self.ovf_extents.append(gpa, .{ .first = first, .pages = want_pages });
|
||||
self.ovf_tail = @as(u64, first) << pgr.page_shift;
|
||||
self.ovf_end = self.ovf_tail + (@as(u64, want_pages) << pgr.page_shift);
|
||||
@@ -593,9 +598,9 @@ pub const Index = struct {
|
||||
self.ovf_tail = 0;
|
||||
self.ovf_end = 0;
|
||||
try self.node_pages.ensureUnusedCapacity(gpa, 2);
|
||||
try self.pager.reserve_pages(2);
|
||||
self.node_pages.appendAssumeCapacity(self.pager.alloc_pages_assume_reserved(1));
|
||||
self.node_pages.appendAssumeCapacity(self.pager.alloc_pages_assume_reserved(1));
|
||||
try self.pager.reserve_pages(&self.hold, 2);
|
||||
self.node_pages.appendAssumeCapacity(self.pager.alloc_pages_assume_reserved(&self.hold, 1));
|
||||
self.node_pages.appendAssumeCapacity(self.pager.alloc_pages_assume_reserved(&self.hold, 1));
|
||||
self.page_mut(0).* = empty_node(0);
|
||||
self.page_mut(1).* = empty_node(1);
|
||||
self.root = 1;
|
||||
@@ -952,7 +957,7 @@ pub const Index = struct {
|
||||
/// honest response.
|
||||
fn alloc_node(self: *Index) u32 {
|
||||
assert_msg(self.node_pages.items.len < self.node_pages.capacity, "node allocation overran reserve_for's bound");
|
||||
const p = self.pager.alloc_pages_assume_reserved(1);
|
||||
const p = self.pager.alloc_pages_assume_reserved(&self.hold, 1);
|
||||
self.node_pages.appendAssumeCapacity(p);
|
||||
const id: u32 = @intCast(self.node_pages.items.len - 1);
|
||||
self.page_mut(id).* = empty_node(0);
|
||||
|
||||
Reference in New Issue
Block a user