db/pager: the concurrency test writes the way the server does

"a checkpoint runs alongside writers on several collections" drove its
writers through `Engine.lock()` -- the legacy whole-engine lock, which
the server has not used since the locks were decomposed. That serialized
the writers against each other, so the overlap the test is named for
never happened: `write_catalog` takes each collection's lock shared, and
nothing it was racing against took that lock at all.

Drive them the way `commands.zig` dispatch does instead: catalog shared,
then the target collection exclusive. The test then does what it says,
and immediately found something -- two appenders on different
collections calling `bytes_mut` at the same time corrupt the pager's
`dirty` set, which is an unsynchronized hash map. ReleaseSafe aborts in
`getOrPutContextAdapted`; three runs in five.

`dirty` is test-only instrumentation (`track_dirty = builtin.is_test`),
so this is a harness bug rather than a server one -- but it is the one
shared structure on a write path whose writers are otherwise kept apart
by owning different pages, and it needs a lock of its own. Six ReleaseSafe
runs clean afterwards.

163/163 unit tests in ReleaseFast and ReleaseSafe, 82/82 fuzz.
This commit is contained in:
2026-08-09 12:09:47 +03:00
parent 992cc2a5ab
commit e15d7f2ed0
2 changed files with 39 additions and 8 deletions

View File

@@ -3146,8 +3146,19 @@ test "a checkpoint runs alongside writers on several collections" {
var doc = make_doc(alloc, @intCast(i), "user") catch return error.Canceled;
defer doc.deinit();
{
e.lock() catch return error.Canceled;
defer e.unlock();
// The server's discipline, not the legacy whole-engine
// lock: catalog shared, then the target collection
// exclusive (commands.zig dispatch). `write_catalog` takes
// the same two in the same order, and that is the whole
// reason its walk of a collection's counters and extents is
// safe -- a writer that skipped the collection lock would
// not be excluded by it, and the test would be checking
// nothing.
e.lock_catalog(false) catch return error.Canceled;
defer e.unlock_catalog(false);
const coll = (e.lock_collection("app", name, true, true) catch
return error.Canceled) orelse return error.Canceled;
defer e.unlock_collection(coll, true);
e.insert("app", name, &doc, undefined) catch return error.Canceled;
}
// As the dispatch epilogue does (commands.zig): the append bumps