config: drop primary/prefer and treat all backends equally

- unmerged /pdb/query/v4/* paths now go to the first backend that answers, not a designated primary
This commit is contained in:
2026-09-05 11:53:45 +10:00
parent ffc2499f98
commit 2391f56a11
12 changed files with 424 additions and 432 deletions
+58 -43
View File
@@ -8,7 +8,6 @@ import (
"log"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"sync"
@@ -82,7 +81,7 @@ func (s *Server) handleQuery(w http.ResponseWriter, r *http.Request) {
s.serveFirstHolder(w, r)
return
}
s.proxyPrimary(w, r)
s.proxyUnmerged(w, r)
}
}
@@ -125,7 +124,7 @@ func (s *Server) serveUnion(w http.ResponseWriter, r *http.Request, path string,
return
}
merged := mergeUnion(s.byPrecedence(alive), key)
merged := mergeUnion(alive, key)
sortRecords(merged, page.order)
if page.wantTotal {
if total := sumTotals(alive); total >= 0 {
@@ -158,7 +157,7 @@ func (s *Server) serveSummed(w http.ResponseWriter, r *http.Request, path string
return
}
merged := sumRows(s.byPrecedence(alive), columns)
merged := sumRows(alive, columns)
sortRecords(merged, page.order)
if page.wantTotal {
w.Header().Set(recordsHeader, strconv.Itoa(len(merged)))
@@ -182,7 +181,7 @@ func (s *Server) serveFirstHolder(w http.ResponseWriter, r *http.Request) {
http.Error(w, "no backend holds this report", http.StatusNotFound)
return
}
for _, res := range s.byPrecedence(alive) {
for _, res := range alive {
if len(res.records) > 0 {
writeJSON(w, rawRecords(res.records))
return
@@ -226,33 +225,15 @@ func rawRecords(recs []record) []json.RawMessage {
}
func (s *Server) mergeNodesResponse(results []backendResult) []json.RawMessage {
return mergeNodes(s.byPrecedence(results))
return mergeNodes(results)
}
func (s *Server) mergeFactsResponse(results []backendResult) []json.RawMessage {
ordered := s.byPrecedence(results)
if s.cfg.Merge == mergeStatic {
prefer := s.cfg.Prefer
return mergeFacts(ordered, func(string) string { return prefer })
return mergeFacts(results, nil)
}
fresh := s.freshnessMap(context.Background(), ordered)
prefer := s.cfg.Prefer
return mergeFacts(ordered, func(cn string) string {
if b, ok := fresh[cn]; ok {
return b
}
return prefer
})
}
// Puts Prefer first so it wins ties; the rest keep config order.
func (s *Server) byPrecedence(results []backendResult) []backendResult {
ordered := make([]backendResult, len(results))
copy(ordered, results)
sort.SliceStable(ordered, func(i, j int) bool {
return ordered[i].name == s.cfg.Prefer && ordered[j].name != s.cfg.Prefer
})
return ordered
fresh := s.freshnessMap(context.Background(), results)
return mergeFacts(results, func(cn string) string { return fresh[cn] })
}
// Queries /nodes unfiltered rather than reusing the request's results, because a /facts query's certname set can differ.
@@ -274,7 +255,7 @@ func (s *Server) freshnessMap(ctx context.Context, _ []backendResult) freshness
}
alive = append(alive, res)
}
f := buildFreshness(s.byPrecedence(alive))
f := buildFreshness(alive)
s.mu.Lock()
s.freshData = f
@@ -329,29 +310,63 @@ func (s *Server) queryBackend(ctx context.Context, b Backend, path string, param
return recs, total, err
}
func (s *Server) proxyPrimary(w http.ResponseWriter, r *http.Request) {
b := s.cfg.PrimaryBackend()
// The record shape is unknown, so a union would be guesswork: the first 2xx wins and the first error response is replayed when none succeeds.
func (s *Server) proxyUnmerged(w http.ResponseWriter, r *http.Request) {
var fallback *bufferedResponse
for _, b := range s.cfg.Backends {
resp, err := s.passThrough(r, b)
if err != nil {
s.log.Printf("warning: backend %q pass-through failed for %s: %v", b.Name, r.URL.Path, err)
continue
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
setContentType(w, resp.Header.Get("Content-Type"))
w.WriteHeader(resp.StatusCode)
_, _ = io.Copy(w, resp.Body)
_ = resp.Body.Close()
return
}
body, _ := io.ReadAll(resp.Body)
_ = resp.Body.Close()
if fallback == nil {
fallback = &bufferedResponse{
status: resp.StatusCode,
contentType: resp.Header.Get("Content-Type"),
body: body,
}
}
}
if fallback == nil {
http.Error(w, "all backends failed", http.StatusBadGateway)
return
}
setContentType(w, fallback.contentType)
w.WriteHeader(fallback.status)
_, _ = w.Write(fallback.body)
}
type bufferedResponse struct {
status int
contentType string
body []byte
}
func (s *Server) passThrough(r *http.Request, b Backend) (*http.Response, error) {
target := strings.TrimRight(b.URL, "/") + r.URL.Path
if r.URL.RawQuery != "" {
target += "?" + r.URL.RawQuery
}
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, target, nil)
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
return nil, err
}
resp, err := s.client.Do(req)
if err != nil {
s.log.Printf("warning: primary %q pass-through failed for %s: %v", b.Name, r.URL.Path, err)
http.Error(w, "primary backend failed", http.StatusBadGateway)
return
return s.client.Do(req)
}
func setContentType(w http.ResponseWriter, contentType string) {
if contentType != "" {
w.Header().Set("Content-Type", contentType)
}
defer func() { _ = resp.Body.Close() }()
if ct := resp.Header.Get("Content-Type"); ct != "" {
w.Header().Set("Content-Type", ct)
}
w.WriteHeader(resp.StatusCode)
_, _ = io.Copy(w, resp.Body)
}
type healthReport struct {