engine: decompose the global lock; cross-connection group commit (roadmap item 5)
The single engine-wide reader/writer lock is replaced by a lock hierarchy, so writes to different collections no longer serialize on one mutex: - Collections are heap-allocated, so their addresses are stable while a command holds a collection lock (the maps only store pointers). - A catalog rwlock guards the database/collection maps: shared for every command (so a concurrent DDL cannot mutate the maps underneath it), exclusive for create/drop/dropDatabase. Each collection has its own rwlock; the ordering is always catalog -> collection -> log lock, never two collection locks at once (TTL sweep and compaction take collections one at a time). - Command dispatch acquires the catalog + target collection locks for the handler's duration, resolving the collection (creating it for writes) under the catalog lock; create/drop upgrade to the exclusive catalog lock. - Appends never fsync. Each write command's epilogue releases the collection lock, then commits once (seal + fsync) with a leader/follower group commit: the leader waits for writers mid-append (a pending counter) so its seal covers them, and followers whose records the seal covered skip their own fsync. Every acknowledged write is fsynced before its reply (crash pair verified); an unacknowledged write may vanish and a reader may observe a write before its fsync — ordinary w:1 j:true semantics instead of 'the log describes >= memory'. - Compaction snapshots collections without the log lock (so a concurrent writer holding one can always finish its append) and retries when a writer appended mid-snapshot (detected via the record seq), then swaps under the log lock — no deadlock. The compaction trigger moved to the command epilogue and the TTL monitor. - Engine.dup_index moved to the collection (per-command error paths). Also lands two B-tree edge-case fixes driven by tests that were in flight: a churned leaf full of dead bytes no longer splits with an empty right half (the leaf is repacked before splitting, and an emptied node's page is fully free again), and a slot-count split with all large records on one side shifts records between the halves until the new record fits. Plus a randomised fuzz test over key sizes (src/fuzz_split.zig) and the two regression tests. Measured (tests/e2e/results/phase6.txt): no regression on the single-connection benchmark; concurrent durable-insert throughput ~5.1k -> 12.5k docs/s from 1 -> 8 clients, ~14.8k at 32. Verified: unit suite in all three modes, all e2e suites, the kill -9 crash pair.
This commit is contained in:
43
ROADMAP.md
43
ROADMAP.md
@@ -1,14 +1,13 @@
|
||||
# Remaining performance work
|
||||
|
||||
Status: **items 1 (B+tree), 2 (ordered `_id` index), 3 (block-framed
|
||||
compressed log) and 4 (byte storage) are done** — verified in
|
||||
`tests/e2e/results/phase2.txt` through `phase5.txt`: updateMany 17.3 →
|
||||
1.7 ms, createIndex 62 → 51 ms, `_id` sort+limit 6.2 → 2.4 ms, `db on
|
||||
disk` 1025 → 97 MB, and `server RSS` 1979 → 539 MB with the range scan at
|
||||
parity (best run faster than MongoDB). Only item 5 (decompose the global
|
||||
lock) remains. Each is sized to be landed and verified on
|
||||
its own; the ordering constraints between them are the load-bearing part, so
|
||||
read those before picking one up.
|
||||
Status: **all five items are done** — verified in `tests/e2e/results/phase2.txt`
|
||||
through `phase6.txt`: updateMany 17.3 → 1.7 ms, createIndex 62 → 51 ms,
|
||||
`_id` sort+limit 6.2 → 2.4 ms, `db on disk` 1025 → 97 MB, `server RSS`
|
||||
1979 → 539 MB with the range scan at parity (best run faster than
|
||||
MongoDB), and the global lock decomposed into catalog + per-collection
|
||||
locks with cross-connection group commit (item 5, no regression on the
|
||||
single-connection benchmark; concurrent-write throughput 1 → 8 clients
|
||||
~5.1k → 12.5k docs/s, 32 clients ~14.8k).
|
||||
|
||||
Current numbers and what they mean are in the README; the recorded baseline
|
||||
is `tests/e2e/results/phase1.txt`, reproduced with
|
||||
@@ -254,7 +253,31 @@ when `Document` changes meaning.
|
||||
|
||||
---
|
||||
|
||||
## 5. Decompose the global lock
|
||||
## 5. Decompose the global lock — DONE
|
||||
|
||||
Landed: collections are heap-allocated (stable pointers; the map only holds
|
||||
them), a catalog rwlock guards the database/collection maps (shared for
|
||||
commands, exclusive for create/drop), and one rwlock per collection guards
|
||||
its docs/slab/indexes, with the catalog → collection → log-lock ordering and
|
||||
never two collection locks at once (TTL sweep and compaction take
|
||||
collections one at a time). Appends never fsync; each write command's
|
||||
epilogue commits once (seal + fsync) under a leader/follower group commit —
|
||||
the leader waits for writers mid-append (a pending counter) so its seal
|
||||
covers them, and followers whose records the seal covered skip their own
|
||||
fsync. `Engine.dup_index` moved per-collection. Compaction snapshots the
|
||||
collections without the log lock and retries if a writer appended during
|
||||
the snapshot (detected via the record seq), then swaps under the log lock —
|
||||
no deadlock against a writer holding a collection lock. The durability
|
||||
guarantee weakened from "the log always describes >= memory" to ordinary
|
||||
`w:1, j:true`: an acknowledged write is fsynced before its reply (the crash
|
||||
pair verifies it), an unacknowledged write may vanish, and a reader can
|
||||
observe a write before its fsync completes.
|
||||
|
||||
Measured: no regression on the single-connection benchmark (phase6);
|
||||
concurrent durable-insert throughput scales ~5.1k → 12.5k docs/s from 1 →
|
||||
8 clients and ~14.8k at 32 — the fsync per commit still dominates
|
||||
sequential-per-client workloads, and the group commit coalesces when
|
||||
appends from different collections overlap.
|
||||
|
||||
**Why.** One reader/writer lock covers the entire engine and is held across
|
||||
fsync, compaction and reply construction, so writes cannot use more than one
|
||||
|
||||
Reference in New Issue
Block a user