Optimistic Concurrency Conflict Resolution
Plug a handler into
Committo reconcile write-write conflicts per entity, instead of accepting silent last-writer-wins.
Status: โ Implemented ยท Visibility: Public ยท Level: ๐ต Core ยท Category: Transactions
๐ฏ What it solves
Typhon transactions take no locks during execution (ADR-003) โ two transactions can read the same entity and both intend to write it, and by default the second one to commit simply overwrites the first. That's fine for most state, but some writes are not safe to clobber blindly: counters, deltas, accumulators, or anything where "last writer wins" silently discards real work. Without a way to inspect what changed underneath a transaction before its write lands, the only options are accept data loss or build retry/merge logic entirely outside the engine.
โ๏ธ How it works (in brief)
Pass a ConcurrencyConflictHandler to Commit. While committing, each entity is checked for a conflict (another
transaction committed a newer value since this transaction's read); detection uses a monotonic per-entity commit
counter rather than chain position, so background revision-chain compaction can't produce a false negative. When a
conflict fires, a ConcurrencyConflictSolver is populated with the entity's read/committed/committing state and the
handler is invoked once for that entity, under the entity's revision-chain lock โ detection and resolution are one
atomic step, no other writer can interleave. The handler resolves the conflict by writing the value it wants
committed; if it writes nothing, the pre-seeded default (the committing transaction's own value) is what commits.
๐ป Usage
void RebaseDelta(ref ConcurrencyConflictSolver solver)
{
var delta = solver.CommittingData<Gold>().Amount - solver.ReadData<Gold>().Amount;
solver.ToCommitData<Gold>().Amount = solver.CommittedData<Gold>().Amount + delta;
}
using var t = dbe.CreateQuickTransaction();
t.Open(playerId).Read(GoldArch.Gold);
ref var w = ref t.OpenMut(playerId).Write(GoldArch.Gold);
w.Amount -= 10; // spend 10 gold
t.Commit(RebaseDelta); // conflicting writers rebase instead of clobbering each other
| Helper | Effect |
|---|---|
solver.TakeRead<T>() |
Discard the change, revert to the read snapshot |
solver.TakeCommitted<T>() |
Accept the other transaction's committed value |
solver.TakeCommitting<T>() |
Keep this transaction's value (also the default with no handler) |
solver.ToCommitData<T>() = ... |
Write a custom resolution (rebase, merge, clamp, โฆ) |
โ ๏ธ Guarantees & limits
- No handler = last writer wins โ the default
Commit()overload never inspects conflicts; the committing value always replaces the prior one. - One call per conflicting entity, not per field โ a multi-component commit can invoke the handler several
times in one
Commit(), once per entity that actually conflicted. - Runs under the revision-chain lock โ detection, handler execution, and publish are atomic; keep handlers fast and allocation-light (no I/O, no calling back into the engine, no blocking waits).
ConcurrencyConflictSolveris a reused, thread-local instance โ valid only for the duration of the handler call; don't store a reference to it or its data pointers beyond that call.- No abort path โ the handler can't reject the conflict and fail the commit; it can only choose what value
commits. Application-level "give up and roll back" logic has to be expressed by writing back the read value
(
TakeRead) and checking for that outcome afterward, if that distinction matters. - Versioned components only โ applies where a revision chain exists (
StorageMode.Versioned);Committed/SingleVersionwrites have no prior revision to compare against.
๐งช Tests
- ConcurrencyConflictTests โ no-handler
last-writer-wins, delta-rebase and
TakeCommitted/TakeReadresolutions, per-entity handler invocation on multi-entity commits, concurrent-thread rebase race,ConcurrencyConflictSolverthread-local reuse
๐ Related
- Related feature: Write-Conflict Baseline Tracking (the underlying per-entity baseline/CommitSequence mechanism), Unit of Work
- Source:
ConcurrencyConflictSolver,Transaction.DetectAndResolveConflict/Commit,CommitContext