Table of Contents

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 UowId and deadline.
  • One shared ChangeSet per 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; Immediate gives each transaction its own.
  • Deferred dispose does not flush โ€” WAL records committed under a Deferred UoW stay volatile until an explicit Flush()/FlushAsync() (or the WAL buffer's own back-pressure forces an earlier write); GroupCommit and Immediate flush automatically on Dispose().
  • Bounded registry slots โ€” UowId is allocated from a fixed-size registry; under sustained pressure CreateUnitOfWork blocks until a slot frees and throws ResourceExhaustedException if 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/CommittedTransactionCount track activity for diagnostics; they do not gate Flush() or Dispose().

๐Ÿงช Tests

  • UnitOfWorkTests โ€” lifecycle (Create/Dispose state transitions), shared UoW identity across transactions, durability-mode preservation, registry-backed UowId allocation, empty-UoW Flush/FlushAsync