myxce/src/frontend/apiserver/backend/candidates.go
Rongsong Shen 8b1e13a52b
feat(component-failover-adoption): route apiserver's candidate parsing through the real C parser
Replaces apiserver's hand-written Go parsePeer() with a CGO wrapper
around the shared C parser+validator pair (xce_parse_peers then
xce_cluster_config_validate, both src/lib/cluster.c) that worker and
scheduler already use from their own main.c's. Gives apiserver the same
[name=]host:port[,...] syntax (including the name= prefix the old Go
parser could not express) and identical malformed-candidate rejection
(empty host, port <= 0, non-numeric port) -- round-1 review caught the
first pass skipping the validate() half, silently accepting inputs
worker/scheduler both reject.

A malformed entry now rejects the whole candidate list rather than
being skipped with a warning (matches xce_parse_peers()'s own
all-or-nothing construction and worker/scheduler's precedent) --
surfaced loudly via RegisterToBackend's error, not silently.

Parsing only -- RegisterToBackend()'s connect loop and tryConnect()
are unchanged; collapsing them into a single connect call is task
4.2's job.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNB4zGPCsHyDUCDJyMbiQN
2026-08-12 04:34:05 +00:00

97 lines
3.6 KiB
Go

//go:build cgo
package backend
/*
#include <stdlib.h>
#include "xce/cluster.h"
*/
import "C"
import (
"fmt"
"unsafe"
)
// Candidate is one parsed controller candidate: an optional logical name
// plus the host:port it resolves to. Mirrors xce_candidate_t (xce/cluster.h).
type Candidate struct {
Name string
Host string
Port int
}
// parseCandidates parses a comma-joined "[name=]host:port,..." candidate
// list through the same shared C parsing+validation pair (xce_parse_peers
// then xce_cluster_config_validate, both src/lib/cluster.c) that worker and
// scheduler already call directly, in that same order, from their C
// main.c's -- retiring apiserver's own hand-written parsePeer() in favor of
// it (Req 6.1: apiserver accepts the identical candidate-list syntax
// scheduler and worker already accept).
//
// xce_parse_peers alone is SYNTAX-only: it splits on ','/'='/':' but never
// rejects a zero/non-numeric port (atoi() silently defaults malformed input
// to 0) or an empty hostname. xce_cluster_config_validate is what actually
// rejects those (src/lib/cluster.c:85-100) -- worker/scheduler always call
// it immediately after a successful xce_parse_peers
// (src/scheduler/src/main.c:300-309, src/worker/src/main.c:414-422), so
// this wrapper must too, or apiserver would silently accept candidate
// strings worker/scheduler reject (Req 6.2).
// xce_cluster_config_validate also requires cfg->identity to be non-empty;
// parseCandidates has no real identity to give it at this layer (clib.go's
// RegisterToBackend assembles that separately, after parsing, from
// operator-supplied From/Key config) -- a placeholder is stamped here
// purely to satisfy that one precondition so the per-candidate
// hostname/port checks actually run. It is never read or reused beyond
// this call.
//
// On any malformed entry -- whether xce_parse_peers' own syntax check or
// xce_cluster_config_validate's semantic check fails -- the WHOLE string is
// rejected; neither function identifies which token was the bad one, and
// neither does this wrapper (Req 6.2): that matches worker's and
// scheduler's own established behavior, which likewise echoes the full
// attempted --peers string back on any parse/validate failure (main.c in
// both) rather than pinpointing one entry.
//
// If candidatesCount exceeds MAX_HA_CANDIDATES, xce_parse_peers silently
// stops reading further tokens (no error, no warning at this layer) --
// this wrapper does not add stricter behavior than the C functions have.
func parseCandidates(peersStr string) ([]Candidate, error) {
cPeers := C.CString(peersStr)
defer C.free(unsafe.Pointer(cPeers))
malformedErr := func() error {
return fmt.Errorf(
"apiserver: --peers parse error: expected \"[name=]host:port,...\" got %q",
peersStr)
}
var cCfg C.xce_cluster_config_t
if rc := C.xce_parse_peers(cPeers, &cCfg); rc != 0 {
return nil, malformedErr()
}
// Placeholder identity: satisfies xce_cluster_config_validate's
// non-empty-identity precondition only -- see doc comment above. cCfg is
// a fresh zero-valued struct, so the array is already NUL-padded past
// what we write here.
const placeholderIdentity = "apiserver-candidates-validate"
for i := 0; i < len(placeholderIdentity); i++ {
cCfg.identity[i] = C.char(placeholderIdentity[i])
}
if rc := C.xce_cluster_config_validate(&cCfg); rc != 0 {
return nil, malformedErr()
}
n := int(cCfg.candidates_count)
out := make([]Candidate, n)
for i := 0; i < n; i++ {
cand := &cCfg.candidates[i]
out[i] = Candidate{
Name: C.GoString(&cand.name[0]),
Host: C.GoString(&cand.hostname[0]),
Port: int(cand.port),
}
}
return out, nil
}