refactor: simplify HA backend peer handling

- parsePeer: use net.SplitHostPort (handles IPv6, stdlib convention)
- tryConnect: take Go strings, own CGO allocation internally
- RegisterToBackend: remove unreachable 'no HA candidates' error path
- Sync Peers field comment between utils and backend config structs
This commit is contained in:
Rongsong Shen 2026-05-08 03:41:11 +00:00
parent 99d13f5254
commit da6d967a33
3 changed files with 26 additions and 36 deletions

View file

@ -76,11 +76,15 @@ var idCtxOnce sync.Once
// Must be set via StartNotificationListener before RegisterToBackend.
var gHandler NotificationHandler
// tryConnect attempts to connect to a single controller candidate.
// Returns nil on success. The C strings passed in are owned by the caller.
func tryConnect(host string, port int, cFrom, cKey *C.char) error {
func tryConnect(host string, port int, from, key string) error {
cAddr := C.CString(host)
defer C.free(unsafe.Pointer(cAddr))
cFrom := C.CString(from)
cKey := C.CString(key)
defer func() {
C.free(unsafe.Pointer(cAddr))
C.free(unsafe.Pointer(cFrom))
C.free(unsafe.Pointer(cKey))
}()
var cConfig C.xce_client_config_t
cConfig.nmasters = 1
@ -99,10 +103,6 @@ func tryConnect(host string, port int, cFrom, cKey *C.char) error {
// RegisterToBackend initialises the XCE client library and joins as an API
// server. StartNotificationListener should be called first so that push
// events have a handler ready before the first push can arrive.
//
// When config.Peers is non-empty the function iterates each "host:port" entry
// in order and returns as soon as one succeeds (HA candidate fallback).
// When Peers is empty it connects to config.Address:config.Port directly.
func RegisterToBackend(config *BackendConfig) error {
from := config.From
if from == "" {
@ -113,14 +113,6 @@ func RegisterToBackend(config *BackendConfig) error {
key = "keepsecret$"
}
cFrom := C.CString(from)
cKey := C.CString(key)
defer func() {
C.free(unsafe.Pointer(cFrom))
C.free(unsafe.Pointer(cKey))
}()
// HA mode: iterate the peer list and connect to the first responsive primary.
if len(config.Peers) > 0 {
var lastErr error
for _, peer := range config.Peers {
@ -130,7 +122,7 @@ func RegisterToBackend(config *BackendConfig) error {
lastErr = err
continue
}
if err := tryConnect(host, port, cFrom, cKey); err != nil {
if err := tryConnect(host, port, from, key); err != nil {
logger.Warn("Candidate unreachable, trying next",
zap.String("host", host), zap.Int("port", port), zap.Error(err))
lastErr = err
@ -140,14 +132,10 @@ func RegisterToBackend(config *BackendConfig) error {
zap.String("host", host), zap.Int("port", port))
return nil
}
if lastErr != nil {
return fmt.Errorf("all HA candidates failed: %w", lastErr)
}
return fmt.Errorf("no HA candidates configured")
return fmt.Errorf("all HA candidates failed: %w", lastErr)
}
// Single-host mode (no peers configured).
if err := tryConnect(config.Address, config.Port, cFrom, cKey); err != nil {
if err := tryConnect(config.Address, config.Port, from, key); err != nil {
return err
}
logger.Info("Backend connection established",

View file

@ -2,21 +2,23 @@ package backend
import (
"fmt"
"net"
"strconv"
"strings"
)
// parsePeer splits a "host:port" string into host and port components.
// Returns an error if the format is invalid or the port is out of range.
// parsePeer splits a "host:port" string. Uses net.SplitHostPort so IPv6
// bracket notation (e.g. "[::1]:9990") is handled correctly.
func parsePeer(peer string) (host string, port int, err error) {
idx := strings.LastIndex(peer, ":")
if idx <= 0 {
return "", 0, fmt.Errorf("invalid peer %q: expected host:port", peer)
h, portStr, err := net.SplitHostPort(peer)
if err != nil {
return "", 0, fmt.Errorf("invalid peer %q: %w", peer, err)
}
host = peer[:idx]
port, err = strconv.Atoi(peer[idx+1:])
if h == "" {
return "", 0, fmt.Errorf("invalid peer %q: empty host", peer)
}
port, err = strconv.Atoi(portStr)
if err != nil || port <= 0 || port > 65535 {
return "", 0, fmt.Errorf("invalid port in peer %q", peer)
}
return host, port, nil
return h, port, nil
}

View file

@ -18,10 +18,10 @@ type BackendConfig struct {
From string `yaml:"from"`
Key string `yaml:"key"`
// Peers lists additional controller candidates for HA mode in "host:port"
// format. When non-empty, RegisterToBackend iterates all candidates and
// connects to whichever responds as primary. Address+Port remain the
// single-host fallback when Peers is empty.
Peers []string `yaml:"peers"`
// format. When non-empty, RegisterToBackend tries each candidate in order
// and connects to whichever accepts the join as primary. Address+Port
// serve as a single-host fallback when Peers is empty.
Peers []string `yaml:"peers"`
}
type AppServerConfig struct {