Cross-tree atomic writes
This page is part of the documentation for Orleans.Lattice 9.9.0 (release line 9.9), built 2026-10-04. It is also published as markdown, with every table and list, at cross-tree-atomic-writes.md, and llms.txt lists every page.Part of Lattice Public API Reference.
IGrainFactory / cluster-client extension methods
(LatticeCrossTreeAtomicWriteExtensions) that commit a batch spanning two or
more distinct ILattice trees all-or-nothing, with the same atomic-visibility
guarantee SetManyAtomicAsync gives within a single tree. A stable
operationId is required (no auto-generated overload) because a cross-tree
saga touches multiple registries and a stable idempotency key is mandatory for
safe retry; it must not be null, empty, or whitespace and must not contain '/'.
| Method | Signature |
|---|---|
SetManyAtomicAsync |
Task<CrossTreeAtomicWriteOutcome> SetManyAtomicAsync(this IGrainFactory factory, IReadOnlyList<LatticeTreeBatch> batches, string operationId, CancellationToken = default) |
BeginAtomicWrite |
LatticeAtomicWriteBuilder BeginAtomicWrite(this IGrainFactory factory, string operationId) |
The fluent builder LatticeAtomicWriteBuilder accumulates per-tree slices and
commits them as one cross-tree saga:
| Method | Signature |
|---|---|
ForTree |
LatticeAtomicWriteBuilder ForTree(string treeId) |
Set |
LatticeAtomicWriteBuilder Set(string key, byte[] value) |
Set<T> |
LatticeAtomicWriteBuilder Set<T>(string key, T value, ILatticeSerializer<T> serializer) |
Set<T> (default serializer) |
LatticeAtomicWriteBuilder Set<T>(string key, T value) |
Set (staged CRDT) |
LatticeAtomicWriteBuilder Set(LatticeStagedCrdtWrite staged) |
SetMany (staged CRDT batch) |
LatticeAtomicWriteBuilder SetMany(IEnumerable<LatticeStagedCrdtWrite> staged) |
SetWhere<T> |
LatticeAtomicWriteBuilder SetWhere<T>(string key, T value, Expression<Func<T, bool>> predicate, ILatticeSerializer<T> serializer) |
SetWhere<T> (default serializer) |
LatticeAtomicWriteBuilder SetWhere<T>(string key, T value, Expression<Func<T, bool>> predicate) |
Delete |
LatticeAtomicWriteBuilder Delete(string key) |
CommitAsync |
Task<CrossTreeAtomicWriteOutcome> CommitAsync(CancellationToken = default) |
CommitAsync (and SetManyAtomicAsync) returns
CrossTreeAtomicWriteOutcome.Committed when every tree's optional guard passed
and all writes committed, or CrossTreeAtomicWriteOutcome.PreconditionFailed
when a guard failed and nothing committed on any tree. Before anything is staged it throws ArgumentException when an entry exceeds its tree's LatticeOptions.MaxKeyLength or MaxValueSizeBytes, and LatticeQuotaExceededException when a tree whose slice carries an upsert has reached an enforcing MaxLiveKeys or MaxEstimatedBytes cap. It throws
InvalidOperationException if a write fails (after the saga compensates), or
LatticeIdempotencyKeyMismatchException (a subclass of InvalidOperationException) if
the same operationId is re-submitted with a different tree-set or key-set. A saga state write that loses an optimistic-concurrency (ETag) check makes the affected saga reload its durable state on a fresh activation; the call re-attaches by operationId up to three attempts (after 1 s and 2 s) and, if the conflict persists, throws LatticeStateWriteFailedException with Conflict set - re-submitting the same batch under the same operationId resumes the saga, and its durable decision is never lost or applied twice. When a slice targets a tree declared for cross-cluster replication, that slice must use the tree's declared shape - a plain Set/Delete for an LwwRegister tree, or a staged CRDT Set(LatticeStagedCrdtWrite) for a CRDT-mode tree (see Replication modes - Single shape per tree).
Supporting public types: LatticeTreeBatch (per-tree slice: TreeId,
Entries, optional Predicate, optional EntryDeltas, optional
EntryDeletes), the CrossTreeAtomicWriteOutcome enum
(Committed, PreconditionFailed), and LatticeStagedCrdtWrite (the
client-side staging token a CRDT accessor's Stage* method returns; see
CRDT value-surface accessors). See
Atomic Writes - Cross-tree atomic writes.
The Delete(key) builder method stages a retraction (tombstone) delete that
rides the all-or-nothing cross-tree batch alongside any sibling upserts, so a
re-key projection (a row moving from one view key to another) flips the upsert
at the new key and the delete at the old key as a single atomic visibility
change. On LatticeTreeBatch the optional EntryDeletes list (when non-null)
is aligned 1:1 with Entries: a true slot marks that entry a tombstone
delete; the whole list is null for an upsert-only slice.
The Set(LatticeStagedCrdtWrite) overload couples a typed CRDT mutation
(prepared by a CRDT accessor's Stage* method) into the cross-tree saga
so it commits all-or-nothing alongside sibling LWW writes. The
ForTree(...) it is added under must be the same CRDT-mode tree the
accessor was obtained from. The staged merged state is stored locally and
replicated through the prepared/terminal path, which now also carries the
staged typed delta and merge mode to the receiver: the receiver folds the
delta into its current visible state on the saga's terminal commit, so
concurrent same-key writes from multiple clusters converge by the
per-replica typed-delta union (a +5 and a +3 reach 8 on both
clusters), identical to the live accessor path. See
Atomic Writes - Coupling a CRDT mutation into an atomic write.