Unit of Work (durability boundary)
Batches one or more Transactions under a single flush/durability boundary.
Status: โ Implemented ยท Visibility: Public ยท Level: ๐ข Start Here ยท Category: Transactions
๐ฏ What it solves
A single flat CreateTransaction() call cannot express "how many writes should share one fsync." Some workloads
want thousands of writes behind one flush (a game tick), others want every write durable before it returns (a
financial trade) โ and that choice is a property of the batch, not of any one write. The Unit of Work is the
object that owns this choice: it is the durability boundary, separate from the Transaction's atomicity/isolation
boundary, so callers pick batching granularity once per UoW instead of accepting a one-size-fits-all default per
write.
โ๏ธ How it works (in brief)
DatabaseEngine.CreateUnitOfWork(mode, timeout) allocates a UoW: a UowId (stamped on every revision created
within it, used for crash recovery), an absolute deadline, and โ for Deferred/GroupCommit โ a ChangeSet shared
by every Transaction the UoW creates. uow.CreateTransaction() draws transactions from this scope; each commits
independently (its own atomicity/isolation), but none of them control when their WAL records become crash-safe โ
that is Flush()/FlushAsync()'s job, driven by the UoW's DurabilityMode. Dispose() always releases the
UowId back to the registry; for GroupCommit/Immediate it also flushes for durability, while Deferred leaves
that decision to the caller.
๐ป Usage
// One UoW batches many transactions; caller controls the flush.
using var uow = dbe.CreateUnitOfWork(DurabilityMode.GroupCommit, timeout: TimeSpan.FromSeconds(5));
foreach (var move in pendingMoves)
{
using var tx = uow.CreateTransaction();
tx.Spawn<Unit>(Unit.Pos.Set(move.Position));
tx.Commit(); // ~1-2ยตs, visible immediately
}
await uow.FlushAsync(); // waits for this UoW's records to reach stable media
Console.WriteLine(uow.CommittedTransactionCount);
| Option | Default | Effect |
|---|---|---|
durabilityMode |
DurabilityMode.Deferred |
See Durability Modes โ controls when Flush/Dispose make WAL records crash-safe. |
timeout |
TimeoutOptions.Current.DefaultUowTimeout |
Absolute deadline for everything created under this UoW (transactions, locks, flush waits). |
โ ๏ธ Guarantees & limits
- Flat, not nestable โ there is no API to open a UoW "inside" another UoW's scope; doing so just creates a
second, independent UoW with its own
UowIdand deadline. - One shared
ChangeSetper UoW (Deferred/GroupCommit) โ every transaction's dirty pages funnel through it, so the checkpoint (not a per-UoW write) is what eventually persists data pages;Immediategives each transaction its own. Deferreddispose does not flush โ WAL records committed under aDeferredUoW stay volatile until an explicitFlush()/FlushAsync()(or the WAL buffer's own back-pressure forces an earlier write);GroupCommitandImmediateflush automatically onDispose().- Bounded registry slots โ
UowIdis allocated from a fixed-size registry; under sustained pressureCreateUnitOfWorkblocks until a slot frees and throwsResourceExhaustedExceptionif its deadline expires first. - Cross-UoW concurrency is cheap โ independent UoWs on different threads contend only if they touch the same page or the same B+Tree index node; otherwise they run fully in parallel.
- Counters are observational only โ
TransactionCount/CommittedTransactionCounttrack activity for diagnostics; they do not gateFlush()orDispose().
๐งช Tests
- UnitOfWorkTests โ lifecycle (
Create/Disposestate transitions), shared UoW identity across transactions, durability-mode preservation, registry-backedUowIdallocation, empty-UoWFlush/FlushAsync
๐ Related
- Related features: Transaction Creation Patterns (
CreateQuickTransaction/CreateReadOnlyTransaction), Transaction Lifecycle, Thread Affinity & Pooling, Durability Modes, Deadline & Cooperative Cancellation