Skip to content

repro for oxidecomputer/omicron#10658 - #71

Draft
sunshowers wants to merge 4 commits into
release-22.1-oxidefrom
omicron-10658-repro
Draft

repro for oxidecomputer/omicron#10658#71
sunshowers wants to merge 4 commits into
release-22.1-oxidefrom
omicron-10658-repro

Conversation

@sunshowers

Copy link
Copy Markdown

Run this with:

go test ./pkg/kv/kvserver/  -run 'TestAllocatorDownReplicatesOnColdLivenessCache'  -count=1 -v

Comment on lines +1 to +9
// Copyright 2014 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

@iliana iliana Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming we wrote this and it wasn't largely copied/pasted from another similar test:

Suggested change
// Copyright 2014 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.
// Copyright 2026 Oxide Computer Company
//
// Use of this software is governed by the Apache License, Version 2.0,
// included in the file licenses/APL.txt.

If it was copied from another test please let me know the file so I can determine whether the changes were significant enough to warrant adding Oxide to this header.

sunshowers and others added 4 commits July 6, 2026 16:27
Drive the effective replication factor exclusively by operator policy — the
configured RF and operator decommissioning — never by transient cluster health.
ComputeAction floors neededVoters at the range's own non-decommissioned voter
count (capped at the configured RF), using the authoritative range descriptor.
A dead-but-not-decommissioned voter still counts, so a cold liveness cache can no
longer size a healthy range below the replicas it has and trim it (the
omicron#10658 trigger). Lowering num_replicas or decommissioning (both operator
policy) still reduce the effective RF.

Unlike experiment 1 (dropping the downshift), this keeps GetNeededVoters intact,
so genuinely small / bringing-up clusters stay happy (no purgatory churn) and
decommissioning still completes. Verified: madrid trim becomes a no-op; a 5->3
decommission completes; small clusters and the dead-but-present control are
correct. Two DynamicNumReplicas cases shift from trimming a healthy/dead-bearing
even-4 range toward replacing/keeping replicas, per the don't-reduce-RF-for-
transient-state invariant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…he gauges

Two fixes from the policy-floor review (policy-floor-review.md in the
findings repo):

1. Key the floor exclusion on liveness membership, not store-pool status.
   The floor subtracted decommissioningVoters, which keys on
   storeStatusDecommissioning and therefore only matches LIVE decommissioning
   nodes. A dead node under decommission reports DECOMMISSIONED (dead +
   non-active membership), which classifies as storeStatusDead, so it kept
   counting toward the floor: its replica could only ever be replaced, never
   shed, and on a cluster with no spare node the replacement has no
   allocation target, stalling the decommission in purgatory forever. This
   violated the floor's own principle: decommissioning is operator policy
   and must be able to lower the effective RF regardless of node health.
   Subtract replicas whose NodeLivenessStatus is DECOMMISSIONING or
   DECOMMISSIONED instead: membership is the policy bit, store status mixes
   health back in. UNAVAILABLE decommissioning nodes stay unsubtracted
   deliberately, since excluding them while they sit in neither the dead nor
   the decommissioning status set would open a liveness-blind RemoveVoter
   window; they resolve to live or dead within the store-dead threshold. The
   invariant is preserved: RemoveVoter still fires only when haveVoters
   exceeds the configured RF.

2. Apply the same floor in calcRangeCounter so the gauges agree with the
   allocator. Without it, a phantom-low count blinds ranges_underreplicated
   exactly when it matters, false-alarms ranges_overreplicated on every
   healthy RF-5 range, and reports states the allocator deliberately
   preserves (4 voters on 4 nodes after a shrink) as over-replicated
   forever. With it, a healthy range whose voters the leaseholder cannot
   account for reads under-replicated: the honest signal that the view is
   degraded. Genuine over-replication versus the configured RF is still
   detected via the cap.

Verified: TestAllocator*, TestStorePool*, *Decommission*,
TestReplicateQueue*, *Metrics* all pass. New coverage: dead-node
decommission with and without a spare node (completes in both; settles at 4
voters without a spare, at the configured RF 5 with one), the
unavailable-decommissioning pause, the even steady state, and the floored
gauges.
No behavior change. The floor arithmetic was duplicated inline in
computeAction and calcRangeCounter with two subtly different exclusion
predicates, and could only be tested through the full allocator and metrics
harnesses. Two copies of logic that must agree, drifting independently, is
the same failure mode this incident is about, so consolidate:

allocator_policyfloor.go now holds the whole concept:

- policyFloorNeededVoters: the floor arithmetic as a pure function of
  (needed, have, policyRemoved, configured).
- nodeLivenessStatusIsPolicyRemoved: the allocator-side predicate, an
  exhaustive switch over the status enum with a fail-safe default.
- policyRemovedVoterCount: the metric-side count for IsLiveMap consumers,
  with the deliberate allocator/gauge divergence for
  unavailable-but-not-yet-dead decommissioning nodes documented in one place.

computeAction, calcRangeCounter, and the StorePool helper reduce to calls
into these; the long inline rationale comments move to the helpers.

New unit tests, no cluster harness required:

- TestPolicyFloorNeededVoters: named scenarios (madrid, decommission,
  dead-node decommission, even state, RF lowered, over-RF).
- TestPolicyFloorNeededVotersInvariants: exhaustive enumeration over the
  input space (replica counts and RFs live in [0, 7], so full coverage is a
  few thousand cases and beats sampling): the floor never lowers the target,
  never exceeds max(needed, configured RF), never invents replicas, is
  monotone in policy removals, idempotent, and never sizes a healthy in-RF
  range below the replicas it already has (the madrid invariant).
- TestNodeLivenessStatusIsPolicyRemoved: exhaustive over the enum via its
  generated name map, so a newly added status fails the test and forces an
  explicit policy classification instead of silently inheriting the default.
- TestPolicyRemovedVoterCount: membership/absence cases for the gauge path.

Verified unchanged behavior: TestAllocator*, TestStorePool*, *Decommission*,
TestReplicateQueue*, *Metrics* all pass unmodified; go vet clean.
@sunshowers
sunshowers force-pushed the omicron-10658-repro branch from 111db48 to ec55fa4 Compare July 6, 2026 23:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants