Verified WAL Concurrency
This page is part of the documentation for Orleans.Lattice 9.9.0 (release line 9.9), built 2026-10-04. It is also published as markdown, with every table and list, at verified-wal.md, and llms.txt lists every page.The write-ahead log (WAL) is Orleans.Lattice's durability boundary: every mutation lands in the log before it is acknowledged, and a background garbage collector trims the log once every consumer has durably consumed past a point. That machinery runs under heavy concurrency - producers appending, peer ship loops and leaf materialisers reporting cursors, the GC trimming, and shard moves quiescing and re-fencing the log - and a handful of small decision points keep it safe. Orleans.Lattice drives those decision points from a set of verified cores - pure, deterministic functions that both the production grains and a Coyote verification layer execute - so the WAL's safety properties are machine-checked, not just asserted by prose and integration tests.
This document describes the verification apparatus for the WAL: the proven-core
pattern applied to the WAL seams, and the Coyote concurrency tier that
model-checks each core under adversarial interleavings. It is an assurance
document; the runtime behaviour it protects is documented in
the Write-Ahead Log and
Cross-cluster replication. It shares the
proven-core pattern, harness, and tier policy with the
Verified Atomic-Commit Protocol; read that first for
the full description of the pattern and the [Category("Coyote")] tier.
The proven-core pattern, applied to the WAL
As with the atomic-commit protocol, each WAL decision point is extracted into a
pure core - a single function (or small pure type) that takes explicit inputs and
returns a verdict, with no Task/await, no wall-clock or HLC read, no Orleans
types, and no storage. The production grain hot path calls the core to make the
real decision, and a Coyote model calls the same core to check it under every
explored ordering, so a property proven of the core is a property of production. The
extracted cores are internal and exposed to the test assembly through
InternalsVisibleTo; the in-memory cursor registry the cursor models drive
directly is the public InMemoryWalCursorRegistry.
The extracted WAL cores
| Core | Decision it owns |
|---|---|
WalShippingWatermark |
The durable-contiguous tail of a WAL shard that has several flushes in flight and out of completion order - the start of the oldest in-flight flush, or the next offset when none is in flight - and whether an offset may be shown to a cursor-advancing reader (the replication shipper, the view maintainer, leaf replay): only offsets strictly below the tail, so no reader is ever handed an offset above a still-unfilled prefix hole. |
WalGcTrimCore.IsEntryEligible |
Whether one log entry may be trimmed, given the GC's min-acked cursor, the optional retention ceiling, the durable materialiser offset admission (which can also refuse an entry only the in-memory cursor would admit), the causal-stable frontier, and any buffer-pin blocked-floor - the exact per-entry predicate the GC scan applies. |
InMemoryWalCursorRegistry (driven directly) |
The per-consumer cursor max-merge and the min(cursor) GC floor scan - a consumer cursor never regresses under a stale re-delivery, and the floor is the minimum across consumers. |
WalMoveFenceCore |
Whether an append is admitted while a shard move has fenced the log (!moveFenced), and whether a stale quiesce observation must abort (observed > expected) - the fence check that must be atomic with the offset assignment. |
WalAdmissionGateCore.IsDispatchRefused |
Whether the commit-log writer refuses a new dispatch because it is draining for shutdown - the pre-admission gate paired with a drain that must release every parked caller. |
WalOffsetAllocationCore.Assign |
The per-shard log-offset handed to an append and the single-step advance of the offset counter - the read-and-advance that must be atomic so two concurrent appends never share an offset and the sequence stays dense. |
WalBlockedFloorCore.Meet |
The lowest buffer-pin HLC across consumers - the meet (minimum) each consumer's live pin is folded into, so the GC's blocked floor tracks the slowest buffering consumer and never trims an entry a live buffer still needs. |
WalMoveResumeCore |
Whether a move's target is a clean prefix of the source tail, and the offset a crashed-and-re-driven copy resumes just past - the resume arithmetic that makes an interrupted placement move copy each retained offset exactly once. |
The core files live under src/lattice/, src/lattice/BPlusTree/, and
src/lattice/BPlusTree/Grains/ next to the grains that call them.
The Coyote concurrency tier
The WAL cores are model-checked with Microsoft Coyote
using the same shared harness (CoyoteModelHarness) and the same explicit
cooperative step-ordering style (a model implements ICoyoteModel and advances
the steps itself; Coyote drives runtime.RandomBoolean() to explore the
resulting choice space, which is not a thread schedule space - the models run at
a concurrency degree of zero) described in the
atomic-commit verification doc.
There is no coyote rewrite pass; the concurrency is encoded as data so it is
fully enumerable.
The WAL models live under test/lattice/BPlusTree/Coyote/:
| Model | Core(s) exercised | Property checked |
|---|---|---|
WalShippingWatermarkModel |
WalShippingWatermark |
Under every explored order of out-of-order flush completions and reader polls, a reader that advances its cursor to an offset has every lower offset already persisted - no prefix hole is ever shipped. |
WalGcTrimFloorModel |
WalGcTrimCore |
The GC trims only past the minimum acked cursor across all peers; flooring under the maximum strands a lagging consumer. |
WalCursorMonotonicityModel |
InMemoryWalCursorRegistry (real) |
A consumer's cursor never regresses below its highest report; a stale re-delivery is max-merged away, not applied last-writer-wins. |
WalMoveQuiesceModel |
WalMoveFenceCore |
The fence check and the offset assignment are atomic, so no append is assigned an offset once a shard move has raised the fence - every offset lands at or below the stable tail the move copies. |
WalCommitLogWriterDrainModel |
WalAdmissionGateCore |
A shutdown drain releases every parked admission caller; observing the drain token in the wait set (rather than sampling it before parking) closes the lost-wakeup. |
WalOffsetContiguityModel |
WalOffsetAllocationCore |
Reading and advancing the offset counter is atomic, so two concurrent appends never receive the same offset and the assigned sequence stays dense and strictly ascending. |
WalBlockedFloorLifecycleModel |
WalBlockedFloorCore |
The GC's blocked floor is the minimum live buffer pin across consumers, so through every interleaving of pin-take, pin-raise, and pin-clear it never rises above a live pin and never trims an entry a buffering consumer still needs. |
WalMoveRedriveModel |
WalMoveResumeCore |
A placement move's tail copy resumes just past what the target already holds, so a coordinator that crashes and re-drives at any offset boundary copies every retained offset exactly once with no duplicate and no gap. |
Every model ships a non-vacuous guard test
As in the atomic-commit tier, a model that checks a property only has value if
the property can actually fail. Every WAL model therefore ships a companion
guard test that removes exactly the one fix the property depends on and
asserts Coyote finds the resulting violation
(AssertViolationFoundInSomeExploredRun):
WalShippingWatermarkModel- the guard clamps the reader at the raw next-offset tail, ignoring in-flight flushes, and Coyote finds the order in which a higher window persists first, the reader advances past the hole, and the still-in-flight lower offset is stranded.WalGcTrimFloorModel- the guard floors the trim at the maximum consumer cursor, and Coyote finds the schedule that strands a lagging consumer.WalCursorMonotonicityModel- the guard replaces the max-merge with a last-writer-wins assignment, and Coyote finds the stale re-delivery that regresses a consumer cursor.WalMoveQuiesceModel- the guard splits the atomic fence-check-and-assign into two steps, and Coyote finds the schedule where a quiesce fences between them.WalCommitLogWriterDrainModel- the guard samples the drain token before parking, and Coyote finds the lost-wakeup that leaves a caller parked after the drain.WalOffsetContiguityModel- the guard splits the atomic read-and-advance of the offset counter, and Coyote finds the schedule where two appends are handed the same offset.WalBlockedFloorLifecycleModel- the guard joins the floor at the maximum live buffer pin instead of the minimum, and Coyote finds the schedule where the floor rises above a lagging consumer's pin and the GC trims an entry it is still buffering.WalMoveRedriveModel- the guard resumes every re-drive from the source floor instead of past what the target already holds, and Coyote finds the crash point after which the copy re-appends an offset the target already has (a duplicate).
A model with a green fix test and a green guard test is proven load-bearing.
Running the tier
The Coyote tier is opt-in and held out of the fast development loop and the
deterministic CI step. Every model and guard test is tagged
[Category("Coyote")].
dotnet test test/lattice/Orleans.Lattice.Tests.csproj -c Release --filter "Category=Coyote"
See the "Coyote concurrency tier" section of
.github/instructions/testing.instructions.md
for the tier policy and the procedure for adding a new model.
Related
- Verified Atomic-Commit Protocol - the sibling verification effort whose proven-core pattern, harness, and tier policy this work reuses.
- Verified WAL Durability sample - a runnable demonstration of the cursor-monotonicity and trim-floor properties these models prove.
- Chaos Tests - the end-to-end integration contract that exercises the same WAL guarantees against a live cluster under fault injection.