Adds a per-frontend flush-on-down flag (default true) that causes maglevd to set is_flush=true on lb_as_set_weight when a backend transitions to StateDown, tearing down existing flows pinned to the dead AS instead of just draining them. rise/fall debouncing in the health checker already absorbs single-probe flaps, so a fall-counted down is almost always a real outage — and during a real outage the client-visible "connection refused" oscillation window (where VPP keeps steering existing flows at a dead AS until retry) is a reliability regression worth closing by default. Operators who want the pre-flag drain-only behaviour can set flush-on-down: false per frontend. BackendEffectiveWeight's truth table grows one axis: StateDown now returns (0, flushOnDown); StateDisabled still unconditionally flushes; StateUnknown / StatePaused still never flush. The unit test pins all four combinations. The flag surfaces in the gRPC FrontendInfo message and in `maglevc show frontend <name>` right next to src-ip-sticky.
134 lines
4.5 KiB
Go
134 lines
4.5 KiB
Go
// Copyright (c) 2026, Pim van Pelt <pim@ipng.ch>
|
|
|
|
package health
|
|
|
|
import (
|
|
"git.ipng.ch/ipng/vpp-maglev/internal/config"
|
|
)
|
|
|
|
// ActivePoolIndex returns the priority-failover pool index for fe given
|
|
// the current backend states. The active pool is the first pool that
|
|
// contains at least one backend which is both in StateUp AND has a
|
|
// non-zero configured weight: a pool whose up backends are all
|
|
// weight=0 contributes no serving capacity, so failover falls through
|
|
// to the next tier. Returns 0 when no pool can serve, in which case
|
|
// every backend maps to weight 0 and the return value is unobservable.
|
|
//
|
|
// pool[0] is the primary, pool[1] the first fallback, and so on.
|
|
func ActivePoolIndex(fe config.Frontend, states map[string]State) int {
|
|
for i, pool := range fe.Pools {
|
|
for bName, pb := range pool.Backends {
|
|
if states[bName] == StateUp && pb.Weight > 0 {
|
|
return i
|
|
}
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// BackendEffectiveWeight is the pure mapping from (pool index, active pool,
|
|
// backend state, config weight, flush-on-down policy) to the desired VPP AS
|
|
// weight and flush hint. This is the single source of truth for the state
|
|
// → dataplane rule.
|
|
//
|
|
// A backend gets its configured weight iff it is up AND belongs to the
|
|
// currently-active pool. Every other case yields weight 0.
|
|
//
|
|
// The flush hint controls whether VPP tears down existing flows pinned to
|
|
// the AS on the weight update (is_flush=true on lb_as_set_weight) or merely
|
|
// stops accepting new flows (drain, keep existing). StateDisabled always
|
|
// flushes — it's an operator-driven "this AS is going away" signal. StateDown
|
|
// flushes iff the frontend has flush-on-down enabled; the default is true,
|
|
// because rise/fall debouncing in the health checker already absorbs flaps
|
|
// and a fall-counted down is almost always a real outage the operator wants
|
|
// cleared from the session table fast. Unknown / paused never flush —
|
|
// unknown is pre-probe, and paused is an explicit drain-don't-kill signal.
|
|
//
|
|
// state in active pool not in active pool flush
|
|
// -------- -------------- ------------------- ----------------
|
|
// unknown 0 0 no
|
|
// up configured 0 (standby) no
|
|
// down 0 0 flushOnDown
|
|
// paused 0 0 no
|
|
// disabled 0 0 yes
|
|
func BackendEffectiveWeight(poolIdx, activePool int, state State, cfgWeight int, flushOnDown bool) (weight uint8, flush bool) {
|
|
switch state {
|
|
case StateUp:
|
|
if poolIdx == activePool {
|
|
return clampWeight(cfgWeight), false
|
|
}
|
|
return 0, false
|
|
case StateDown:
|
|
return 0, flushOnDown
|
|
case StateDisabled:
|
|
return 0, true
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|
|
|
|
// EffectiveWeights computes per-pool per-backend effective weights for fe,
|
|
// given a snapshot of backend states. Result layout: weights[poolIdx][backendName].
|
|
func EffectiveWeights(fe config.Frontend, states map[string]State) map[int]map[string]uint8 {
|
|
activePool := ActivePoolIndex(fe, states)
|
|
out := make(map[int]map[string]uint8, len(fe.Pools))
|
|
for poolIdx, pool := range fe.Pools {
|
|
out[poolIdx] = make(map[string]uint8, len(pool.Backends))
|
|
for bName, pb := range pool.Backends {
|
|
w, _ := BackendEffectiveWeight(poolIdx, activePool, states[bName], pb.Weight, fe.FlushOnDown)
|
|
out[poolIdx][bName] = w
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ComputeFrontendState derives the FrontendState for fe from a snapshot of
|
|
// backend states. Rules:
|
|
//
|
|
// - no backends → unknown
|
|
// - every referenced backend is in StateUnknown → unknown
|
|
// - any backend has effective weight > 0 → up
|
|
// - otherwise → down
|
|
func ComputeFrontendState(fe config.Frontend, states map[string]State) FrontendState {
|
|
// Unique set of backends referenced by this frontend (a single backend
|
|
// may appear in multiple pools; we count it once).
|
|
seen := make(map[string]struct{})
|
|
for _, pool := range fe.Pools {
|
|
for bName := range pool.Backends {
|
|
seen[bName] = struct{}{}
|
|
}
|
|
}
|
|
if len(seen) == 0 {
|
|
return FrontendStateUnknown
|
|
}
|
|
allUnknown := true
|
|
for bName := range seen {
|
|
if states[bName] != StateUnknown {
|
|
allUnknown = false
|
|
break
|
|
}
|
|
}
|
|
if allUnknown {
|
|
return FrontendStateUnknown
|
|
}
|
|
ew := EffectiveWeights(fe, states)
|
|
for _, poolMap := range ew {
|
|
for _, w := range poolMap {
|
|
if w > 0 {
|
|
return FrontendStateUp
|
|
}
|
|
}
|
|
}
|
|
return FrontendStateDown
|
|
}
|
|
|
|
func clampWeight(w int) uint8 {
|
|
if w < 0 {
|
|
return 0
|
|
}
|
|
if w > 100 {
|
|
return 100
|
|
}
|
|
return uint8(w)
|
|
}
|