Sooner or later every backend is down at once. The balancer must say so with a clear error - never divide by zero, panic, or loop forever hunting for a healthy backend that does not exist.
Return an explicit no-healthy-backend error from selection and dispatch when the available set is empty.
Every algorithm in this project selects from Available(), and until now the specs
kept at least one backend up so the set was never empty. Production reality is not so
kind: a bad deploy, a network partition, or a cascading failure can take every
backend down at once. The balancer’s job then is not to guess or spin - it is to
return a clear, catchable error so the caller can shed load, return a 503, or retry
later.
The bug this guards against is concrete: selecting Available()[n % len] on an empty
slice divides by zero (a panic in most languages), and a naive “keep trying until one
is healthy” loop would never terminate. A single sentinel error - the same
ErrNoHealthyBackend value every time, so callers can compare against it - is the
honest answer. Add the empty check to each selector’s front door; the other
algorithms follow the exact same one-line guard. Because Begin and Dispatch
already propagate the selector’s error, the whole stack reports it, and the moment a
backend comes back up, selection resumes with no special handling.
var ErrNoHealthyBackend = errors.New("no healthy backend")func (r *RoundRobin) Select() (*Backend, error) {avail := r.pool.Available()if len(avail) == 0 { return nil, ErrNoHealthyBackend } // guard before the modulob := avail[r.n % len(avail)]; r.n++return b, nil}// Begin already forwards Select's error; Dispatch already forwards Begin's.