Commit Discipline (Variant-A Staging)
Atomic, zero-loss
SingleVersionwrites — durable and visible together atCommit(), with no revision chain.
Status: ✅ Implemented · Visibility: Public · Level: 🟣 Advanced · Category: Transactions
Assumes: SingleVersion (Tick-Fence Durability)
🎯 What it solves
A SingleVersion write under the default TickFence discipline can lose up to one tick on crash — fine for
regenerable state, unacceptable for a teleport, an item pickup, or a currency debit. The traditional fix is to
make the component Versioned, but that pays a ~6x write-cost tax (copy-on-write content-chunk allocation,
revision-chain append, eventual GC) for snapshot isolation and AS-OF history these writes never use. Commit
discipline closes that gap: zero-loss, atomic durability at near-SingleVersion write cost, with no chain.
⚙️ How it works (in brief)
A Commit-discipline write stages its value into a per-transaction native arena — the cluster HEAD slot is never
touched before commit (CM-01). The first write to a given (entity, component) pair seeds the staging slot from
the current HEAD, so partial-field writes are correct. At Commit(), every staged value is appended to the WAL
as an ordinary slot record (carrying a Committed flag, telemetry only) and, after the append, published in
place: memcpy into HEAD, exact secondary B+Tree index reconciled, slot marked dirty — the same publish step
Versioned uses for its exact-index guarantee. Rollback() is O(1): the arena is discarded and HEAD was never
touched, so there is nothing to undo. Discipline is uniform per transaction (CM-02) — once any write escalates,
every SingleVersion write that transaction makes is staged the same way.
💻 Usage
[Component("Game.Wallet", 1, StorageMode = StorageMode.SingleVersion,
DefaultDiscipline = DurabilityDiscipline.Commit)] // optional: every tx touching this escalates
struct Wallet { public long Gold; }
[Archetype]
partial class Player : Archetype<Player>
{
public static readonly Comp<Position> Pos = Register<Position>();
public static readonly Comp<Wallet> Wallet = Register<Wallet>();
}
// Explicit escalation for one critical operation:
using var tx = dbe.CreateQuickTransaction(DurabilityMode.Immediate, DurabilityDiscipline.Commit);
ref var pos = ref tx.OpenMut(playerId).Write(Player.Pos);
pos.X = teleportTarget.X;
pos.Y = teleportTarget.Y;
tx.Commit(); // staged value WAL-logged then published — zero loss on crash
// From inside a scheduled system, via the TickContext side-transaction idiom:
using var side = ctx.CreateSideTransaction(DurabilityMode.Immediate, DurabilityDiscipline.Commit);
side.OpenMut(playerId).Write(Player.Wallet).Gold -= price;
side.Commit();
| Discipline | Write cost (Zen 4) | Durable when | Loss window | Isolation |
|---|---|---|---|---|
TickFence (default) |
~40 ns | at the next tick fence | ≤ 1 tick | tick-fence |
Commit |
~40 ns + commit publish | at Commit() |
zero | read-committed |
⚠️ Guarantees & limits
- All of a transaction's
Commit-discipline writes become visible together at commit, or none do —Rollback()discards the staged values and HEAD is unaffected (CM-01). - Read-your-own-writes works for point reads (
EntityRef.Read/Write) inside the writing transaction. Bulk span reads (ClusterRef.GetSpan<T>) taken inside that same transaction do not see staged values — they read HEAD. Read after commit, or through a different transaction, instead. - Isolation is read-committed, not snapshot — a
Commit-discipline component used withReadsSnapshotfails loudly at schedulerBuild()(rule CM-04); useVersionedfor snapshot/AS-OF reads. - No revision chain and no deferred-cleanup GC cost — rollback is an arena reset, not a chain unwind (AC-6).
- An indexed field additionally pays a B+Tree
Move(~200 ns) at commit, the same cost aVersionedwrite pays — the ≤25 ns / ≤60 ns targets are for non-indexed fields. Spatial indexing stays fence-batched for all modes (no extra commit-time cost). - A duplicate WAL record from a concurrent tick fence re-emitting the just-committed value is benign (last-writer-wins by LSN, rule CM-03) — at most one redundant record, never a correctness issue.
- Recovery needs no discipline-specific code: a
Commit-discipline write is an ordinary WAL slot record, replayed by the sameRecoveryApplierslot upsert as every other write (AC-7). - Composes with any
DurabilityMode: discipline picks the write→WAL mechanism, mode still picks the flush timing — pairingCommitdiscipline withDurabilityMode.Deferredstill buffers the WAL record untilFlush().
🧪 Tests
- CommittedDisciplineTests —
staging/publish (
CommitDiscipline_Write_PublishesAtCommit), read-your-own-writes, rollback discards staged values (CommitDiscipline_Rollback_DiscardsStaged), multi-write atomicity, indexed-field commit cost - CommittedDisciplineRecoveryTests
— zero-loss crash survival, mixed
Commit/TickFencewrites under churn, indexed cluster spawn across a consolidating checkpoint crash
🔗 Related
- Parent feature: SingleVersion Durability Discipline
- Sibling: TickFence discipline (default)