state: preserve previous primary when all HA advertisers unhealthy

electPrimaryRoutes' all-unhealthy fallback picked candidates[0]
(lowest NodeID) regardless of who was prev. Under cable-pull
semantics IsOnline lags reality (long-poll TCP half-open), so
both routers stay in candidates and both go Unhealthy via the
prober — the fallback then churned primary to a node that was
itself unreachable.

Prefer prev when still in candidates; fall through to
candidates[0] only when prev is gone. Anti-blackhole holds.

Update the property test reference model and split the unit
test into existence (KeepsAPrimary) and identity
(PreservesPrevious) cases.

Fixes #3203
This commit is contained in:
Kristoffer Dalby 2026-04-29 13:34:51 +00:00
parent 27c9113af8
commit 3d5c0af4e7
3 changed files with 49 additions and 13 deletions

View file

@ -98,7 +98,12 @@ func (m *primariesModel) updatePrimaries() {
}
if !found && len(nodes) >= 1 {
selected = nodes[0]
if cur, ok := m.primary[p]; ok && slices.Contains(nodes, cur) {
selected = cur
} else {
selected = nodes[0]
}
found = true
}