// SPDX-License-Identifier: Apache-2.0 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) }