docs/adr/0040-dns-consensus-fallback-parity.md
Rama master · réplica de lectura del repositorio original.
# ADR-0040: DNS Consensus Fallback Parity
## Metadata
| Field | Value |
|-------|-------|
| **Status** | Superseded by [ADR-0072](0072-dns-consensus-revalidation-hardening.md) and [ADR-0075](0075-dns-unbound-single-source-completion.md) |
| **Date** | 2026-08-07 |
| **Authors** | AlessioBrillo |
| **Deciders** | AlessioBrillo |
| **Supersedes** | N/A |
| **Relates to** | ADR-0002, ADR-0039 |
| **Project** | DOMINUS |
## Context
The 2-of-3 DNS consensus gate introduced by ADR-0039 only ran on the
`DnsPreFilterStage` bulk-success path and the cross-validation-recovered
path. When the bulk check failed entirely (`checkBulk` throws or returns a
mismatched count), `#perDomainFallback` resolved every candidate through the
primary provider alone and returned the verdicts **without** asking the
secondary provider for confirmation.
The effect was an ADR-0002 asymmetry that depended on the *happy path* of
the transport layer:
> A candidate that reached the retry path received a verdict resting on a
> single provider's opinion, while the identical candidate on the bulk path
> received a two-independent-opinion verdict.
Bulk failures are not exotic — they are exactly what happens under network
saturation, a temporarily unavailable resolver group, or a Redis rate-limiter
`RateLimiterWaitTimeoutError`. In other words, the availability guarantee
weakened precisely then the run was under stress, which is the moment the
engine must be strictest, not laxest (ADR-0002 conservatism).
A secondary symptom: fallback-path runs never produced `verified` /
`disagreed` / `unverifiable` tallies or the `consensus-unverified`
degradation signal, so a run that silently lost its guarantee was
indistinguishable from one that genuinely had no eligible domains (the
ADR-0039 "silent throughput collapse" failure mode, reappearing at the
fallback layer).
## Decision
**Consensus is now applied on every resolution path.** `#resolveBulkWithFallback`
routes every degraded exit through a single helper,
`#fallbackWithConsensus`, that runs the per-domain fallback and then the
same strict 2-of-3 cross-check (`#applyConsensusIfConfigured`) used on the
bulk path:
- A per-domain verdict of `Available` from the fallback is only final when
the secondary provider independently confirms it.
- A secondary failure (reject/`Unknown`/throw) downgrades the candidate to
`Unknown`, and is counted in `consensusStats` / the degradation signal
exactly as on the bulk path.
- Stages configured without a secondary (consensus disabled) behave
exactly as before — `#applyConsensusIfConfigured` is a no-op.
### Options considered and rejected
- **Accept primary-opinion verdicts on the fallback path (status quo)** —
rejected: it is the ADR-0002 gap being closed, and it only manifested on
the degraded path of a degraded network layer.
- **Abort the run instead of falling back when the bulk check fails** —
rejected: the bulk failure is typically transient and the primary check is
still a legitimate fast signal (ADR-0039 rationale); aborting forfeits the
run over a retryable transport hiccup.
- **Only log a warning without downgrading** — rejected: a warning does not
enforce the conservatism principle; the verdict itself must not rest on a
single provider.
## Consequences
### Positive
- ADR-0002 parity across **all** resolution paths: a bulk failure can no
longer strip the two-provider availability guarantee.
- Fallback runs produce the same `consensusStats` and degradation signals as
bulk runs, so degraded output is visible to operators (ADR-0039 machinery
is preserved on the fallback layer).
- Single source of truth: the consensus entry point is one helper used by
every exit of `#resolveBulkWithFallback`.
### Negative
- On a genuinely sick secondary, a run whose bulk check failed will now see
all Available verdicts downgraded to `Unknown` (fail-closed is the desired
outcome; there is no throughput win to re-claim).
- Adds one round of consensus queries on an already-degraded path, marginally
extending wall-clock where the fallback was already slow.
### Risks and mitigation
- **Regression risk in fallback timing:** the fallback path now runs an extra
consensus pass with the same `fallbackConcurrency` bound; the pass is
bounded per-query by the provider timeouts that already cover the bulk
path. Monitoring series (`dominus_dns_consensus_*`) expose whether
fallback-downgraded runs are becoming frequent.