Table of Contents

Anti-entropy remediation guards (opt-in, rate cap, circuit breaker)

This page documents Orleans.Lattice.Replication 9.9.0, in 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 anti-entropy-remediation-guards.md, and llms.txt lists every page.

The anti-entropy chain detects drift (the digest probe and the read-only Merkle walk) and, optionally, repairs it (targeted leaf re-replay and the bootstrap-snapshot fallback). The remediation guards wrap the repair stage with three operator controls so automatic repair is opt-in, rate-limited, and self-fencing. Detection is never gated by these controls - an operator can watch drift telemetry without opting into automatic repair.

All three guards ship dark: with defaults unchanged, an un-opted host detects and probes drift exactly as before and attempts no automatic repair.

1. Operator opt-in master gate

AutoRemediateOnDigestMismatch (default false) is the master switch for all automatic remediation - both leaf re-replay and the bootstrap-snapshot fallback. While it is off, a localised drift records a single skip with reason opt_out and the digest_remediation.disabled gauge reports the affected (tree, peer); no repair traffic is sent. It is an additional AND-gate in front of the per-stage repair flags (LeafReReplayEnabled, BootstrapFallbackEnabled), which still apply on top of it; the read-only localisation stage (MerkleWalkEnabled) runs before it and is never gated by it.

2. Per-(tree, peer) rate cap

When remediation is enabled, repair re-ship volume is rate-limited per (tree, peer) to a small fraction of the ordinary ship-batch budget. The effective per-window entry budget is max(1, ceil(RemediationTrafficBudgetFraction * ShipBatchSize)) - about 3 entries with the defaults (0.01 * 256). The first pass in a fresh window always runs (so one repair burst is permitted); once a pair has spent its window budget, further passes are skipped with reason budget_exhausted until the RemediationTrafficWindow rolls over. The window is a deterministic elapsed-time accounting interval, kept in memory by the per-tree digest-probe scheduler, so it is not persisted and restarts from zero when that scheduler reactivates.

3. Per-(tree, peer) circuit breaker

After RemediationFailureThreshold consecutive failures for a (tree, peer), the circuit breaker opens: remediation is skipped with reason circuit_open and the gauge reports the disabled state for RemediationCircuitResetInterval. After the cooldown the breaker half-opens and the next evaluation runs one trial pass - success closes the breaker and clears the gauge, a failed trial re-opens it for a fresh cooldown. Any success resets the consecutive-failure count. A "failure" is a remediation pass whose re-ship sink reported zero entries shipped despite candidate entries having been selected. In the current code a pass whose re-ship throws (for example a transport error reaching the peer) is caught inside the pass and counted as a success: it resets the consecutive-failure count, and closes a half-open breaker, instead of counting toward RemediationFailureThreshold.

Enabling it

siloBuilder.AddLatticeReplication(o =>
{
    o.ClusterId = "cluster-a";
    o.ReplicatedTrees = new Dictionary<string, LatticeMergeMode>
    {
        ["orders"] = LatticeMergeMode.LwwRegister,
    };

    // Detection + localisation + repair (all off by default).
    o.DigestProbeEnabled = true;
    o.MerkleWalkEnabled = true;
    o.LeafReReplayEnabled = true;
    o.BootstrapFallbackEnabled = true;

    // Master gate for automatic repair (off by default).
    o.AutoRemediateOnDigestMismatch = true;

    // Rate cap: 1% of ShipBatchSize per (tree, peer) per window.
    o.RemediationTrafficBudgetFraction = 0.01;
    o.RemediationTrafficWindow = TimeSpan.FromMinutes(1);

    // Circuit breaker: open after 3 consecutive failures, cool down for 5 minutes.
    o.RemediationFailureThreshold = 3;
    o.RemediationCircuitResetInterval = TimeSpan.FromMinutes(5);
});
Option Default Notes
AutoRemediateOnDigestMismatch false Master gate for all automatic repair. When false, drift is detected and probed but never repaired (digest_remediation.skipped{reason=opt_out}).
RemediationTrafficBudgetFraction 0.01 Fraction of ShipBatchSize a (tree, peer) may re-ship per window. Validated to (0.0, 1.0].
RemediationTrafficWindow 00:01:00 Deterministic accounting window for the rate cap. Validated > TimeSpan.Zero.
RemediationFailureThreshold 3 Consecutive failures that open the circuit breaker. Validated >= 1.
RemediationCircuitResetInterval 00:05:00 Cooldown before the breaker half-opens. Validated > TimeSpan.Zero.

Observability

Metric Tags Emitted
orleans.lattice.replication.digest_remediation.disabled tree, peer, reason, tenant Observable gauge, value 1 for each (tree, peer) whose remediation is currently disabled. No series means remediation is permitted.
orleans.lattice.replication.digest_remediation.skipped tree, peer, reason, tenant Counter, once per remediation pass skipped before sending repair traffic.

Reasons: opt_out (the host has not set AutoRemediateOnDigestMismatch), budget_exhausted (the per-window rate cap is spent), and circuit_open (the breaker tripped on consecutive failures). A pair's gauge series is cleared only when a later remediation pass for that pair completes without failing, so until then it keeps reporting its last reason - for example after the rate-cap window has rolled over, or after AutoRemediateOnDigestMismatch has been turned on.

The metric-name constants and the reason mapping are exposed for dashboards built from the public surface:

_ = LatticeReplicationMetrics.DigestRemediationDisabledName;
_ = LatticeReplicationMetrics.DigestRemediationSkippedName;

string tag = LatticeReplicationMetrics.DigestRemediationDisabledReasonTag(RemediationDisabledReason.CircuitOpen);
System.Diagnostics.Debug.Assert(tag == LatticeReplicationMetrics.DigestRemediationReasonCircuitOpen);