Table of Contents

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

A concurrent OR-Flag enable and disable: enable wins. An order diagram drawn as a chain of two states. At the bottom the flag is off, with no enable dots. Cluster A enables it, minting dot A1, which moves A up. Cluster B, at the same time, disables it, but it has observed no enable dots, so there is nothing to tombstone and B's state stays at the bottom. Merging lifts B to A's state: the flag is on, because B's disable never saw A1.

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.