TickFence Discipline (Default)
The default, lowest-cost
SingleVersionwrite — durable at the next tick fence, not at commit.
Status: ✅ Implemented · Visibility: Public · Level: 🔵 Core · Category: Transactions
Assumes: SingleVersion (Tick-Fence Durability)
🎯 What it solves
The bulk of a real-time simulation's writes — positions, velocities, AI state, anything the next tick
recomputes anyway — don't need per-write durability. Paying a WAL append, or worse a revision-chain allocation,
on every one of these writes would dominate the tick budget for no benefit: if the process crashes, the next
tick would have overwritten the value regardless. TickFence discipline gives these writes the cheapest
possible path while still bounding the data-at-risk window to a single tick.
⚙️ How it works (in brief)
A TickFence-discipline write is an in-place store directly into the SingleVersion cluster slot (HEAD) — no
staging, no per-write WAL append. The write marks the slot's chunk in the table's DirtyBitmap; at the tick
fence, the engine batches every dirty slot since the last fence into WAL records in one pass. This is the same
path every SingleVersion write has always used — TickFence is simply the name for it now that Commit exists
as an alternative. It is the implicit default: any transaction created without an explicit discipline: argument
uses it.
💻 Usage
[Archetype]
partial class Player : Archetype<Player>
{
public static readonly Comp<Position> Pos = Register<Position>(); // StorageMode.SingleVersion
}
// Game tick — TickFence is the default, no argument needed
using var uow = dbe.CreateUnitOfWork(DurabilityMode.Deferred);
using var tx = uow.CreateTransaction();
ref var pos = ref tx.OpenMut(playerId).Write(Player.Pos);
pos.X += velocity.X * dt;
pos.Y += velocity.Y * dt;
tx.Commit(); // ~40 ns write; value rides the next tick fence to the WAL
// Equally explicit, if calling out the choice matters at the call site:
using var tx2 = uow.CreateTransaction(discipline: DurabilityDiscipline.TickFence);
⚠️ Guarantees & limits
- Commit latency is unaffected by this write — there is no per-write WAL or commit-time cost; the cost is paid
once per tick at the fence, amortized across every dirty
SingleVersionslot. - Data-at-risk window is up to one tick (~16ms at 60fps): a crash between the write and the next fence loses it. Acceptable for state the simulation regenerates or where a one-tick rollback is harmless.
- Tick-fence isolation — distinct from
Commit's read-committed: bulk and point reads by other transactions always read HEAD directly (no chain to walk, no staging to consult), so a reader can observe a value the writer hasn't yet flushed to WAL. - A
SingleVersioncomponent used withReadsSnapshotis rejected at schedulerBuild()(rule CM-04) under either discipline —TickFencehas no history to freeze to. - Mixing disciplines on the same component across different transactions is fine —
TickFenceandCommitwrites to the same slot compose correctly at recovery (last-writer-wins by LSN). - A component declared
[Component(DefaultDiscipline = DurabilityDiscipline.Commit)]cannot be writtenTickFence-style: the first write to it escalates the whole transaction toCommit(CM-02). Once anyTickFencein-place write has already happened in that transaction, later escalation toCommitthrows — pick the discipline before the first write.
🧪 Tests
- StorageModeTickFenceTests —
WriteTickFence_ClearsDirtyBitmap(in-place write, dirty-bitmap batching),WriteTickFence_VersionedAndTransient_Skipped(discipline only applies toSingleVersion) - TickFenceE2ETests — end-to-end write/reopen survival, last-write-wins across multiple updates, multi-entity recovery, only-dirty-entities-written
🔗 Related
- Parent feature: SingleVersion Durability Discipline
- Sibling: Commit discipline (Variant-A staging)