OR-Flag (Observed-Remove Flag, enable-wins)
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 orflag.md, and llms.txt lists every page.tree.OrFlag(key) -> OrFlagAccessor, merge mode LatticeMergeMode.OrFlag.
Semantics
An OR-Flag is a single boolean presence bit - "enabled" or not - with enable-wins convergence. It is the OR-Set idea reduced to a single implicit element: the flag tracks presence rather than a payload, so you get OR-Set semantics without carrying a set's element bytes.
Every Enable mints a fresh causal dot. Disable tombstones only the enable
dots it has observed. The flag is on whenever at least one enable dot
survives, so a Disable concurrent with an Enable it never saw leaves the flag
enabled.
Enabling a flag that is already on is safe to repeat: it does not make the stored value grow. Turning the same flag on a million times costs no more space than turning it on once.
Use it for: membership rows in a secondary index (a tag/key pair is present or not), feature toggles, presence bits - where a concurrent enable should beat a concurrent disable.
Behaviour
Both clusters hold enable A1: ENABLED. A disable cancels only the enable dots it has observed, and B's had observed none.
Disable tombstones only the enable dots it has observed, so a concurrent enable always survives: enable-wins.
sequenceDiagram
participant A as Cluster A
participant B as Cluster B
A->>A: Enable (dot A1)
B->>B: Disable (observed: none yet)
A-->>B: merge ships enable dot A1
B-->>A: merge ships disable tombstones
Note over A,B: A1 was not observed by B's disable
Note over A,B: converged = ENABLED (enable-wins)
Example
var beta = tree.OrFlag("tenant:5:beta");
// Cluster A turns the flag on.
await beta.EnableAsync("cluster-A", cancellationToken);
// Meanwhile cluster B turned it off before A's enable had reached it, so its
// disable observed no enable dots and cancelled nothing. Replication delivers
// B's flag; MergeAsync stands in for that here. (A DisableAsync on this cluster,
// after the enable, would observe A's dot and turn the flag off.)
var fromB = new OrFlag();
fromB.Disable();
await beta.MergeAsync(fromB, cancellationToken);
// The disable only cancels the enable dots it saw, so a concurrent enable
// survives - the flag converges ENABLED.
bool isOn = await beta.IsEnabledAsync(cancellationToken); // true
Marking many flags at once
Enabling flags one at a time costs two round trips per key - a read to mint the
enable dot, then the apply. EnableManyAsync reads every current flag in one
batched call, mints all the deltas against that snapshot, and applies them through
a single batched CRDT write, so a presence- or membership-marking pass costs one
read and one write per leaf rather than two round trips per key.
The snapshot is treated as possibly incomplete. A batched read reports an absent key by omission, so a row it did not return is indistinguishable from a row that does not exist, and deriving the dot counter from the snapshot alone would mint counter 1 for both. Because OR-Flag cancellation is coverage-based, such a dot is cancelled outright by any tombstone the unread row already carries - the write would report success while the flag stayed off, permanently, since the next attempt would repeat the read and mint the same dead dot. The batched helpers therefore mint each dot from a strictly increasing source instead. A dot only has to be unused for its replica, never dense, so this costs nothing semantically and makes an enable impossible to cancel by a tombstone authored before it.
Pass the same non-empty replica identity you would give EnableAsync; an empty
one is rejected, because writers sharing a blank identity could cancel each
other's enables.
// Mark a whole batch of feature flags on in one write.
string[] keys = ["tenant:5:beta", "tenant:6:beta", "tenant:7:beta"];
await tree.EnableManyAsync(keys, "cluster-A", cancellationToken);
// Enabling is idempotent under OR-Flag merge, so a retried batch converges
// rather than clobbering a concurrent writer.
await tree.EnableManyAsync(keys, "cluster-A", cancellationToken);
The batch is not atomic: a partial failure leaves it half-applied. When the
marks must land all-or-nothing, stage them instead with StageEnableManyAsync,
which mints every delta the same way from one batched read, and hand the tokens
to the cross-tree atomic builder:
IReadOnlyList<LatticeStagedCrdtWrite> staged =
await tree.StageEnableManyAsync(["tenant:8:beta", "tenant:9:beta"], "cluster-A", cancellationToken);
See also: its remove-wins mirror RW-Flag, the fuller OR-Set, and the CRDT overview.