forked from lei.chen/myxce
feat: add HA candidate failover to API server backend
- BackendConfig.Peers []string carries host:port candidates from YAML (backend.peers) - RegisterToBackend iterates Peers in order, connecting to first responsive primary - parsePeer extracted to peers.go (no build tag) so it's testable without CGO - Single-host path (no Peers) preserved unchanged - Tests: parsePeer table-driven (11 cases), stub RegisterToBackend with/without peers
This commit is contained in:
parent
9bc84f1ca1
commit
99d13f5254
7 changed files with 213 additions and 16 deletions
|
|
@ -76,9 +76,33 @@ 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 {
|
||||
cAddr := C.CString(host)
|
||||
defer C.free(unsafe.Pointer(cAddr))
|
||||
|
||||
var cConfig C.xce_client_config_t
|
||||
cConfig.nmasters = 1
|
||||
cConfig.masters[0] = cAddr
|
||||
cConfig.master_ports[0] = C.int(port)
|
||||
cConfig.from = cFrom
|
||||
cConfig.key = cKey
|
||||
|
||||
rc := C.xce_backend_init(&cConfig, &gCtx)
|
||||
if rc < 0 {
|
||||
return fmt.Errorf("xce_backend_init failed on %s:%d: %d", host, port, int(rc))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 == "" {
|
||||
|
|
@ -89,27 +113,43 @@ func RegisterToBackend(config *BackendConfig) error {
|
|||
key = "keepsecret$"
|
||||
}
|
||||
|
||||
cAddr := C.CString(config.Address)
|
||||
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
|
||||
cConfig.masters[0] = cAddr
|
||||
cConfig.master_ports[0] = C.int(config.Port)
|
||||
cConfig.from = cFrom
|
||||
cConfig.key = cKey
|
||||
|
||||
rc := C.xce_backend_init(&cConfig, &gCtx)
|
||||
if rc < 0 {
|
||||
return fmt.Errorf("xce_backend_init failed: %d", int(rc))
|
||||
// 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 {
|
||||
host, port, err := parsePeer(peer)
|
||||
if err != nil {
|
||||
logger.Warn("Skipping invalid peer", zap.String("peer", peer), zap.Error(err))
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
if err := tryConnect(host, port, cFrom, cKey); err != nil {
|
||||
logger.Warn("Candidate unreachable, trying next",
|
||||
zap.String("host", host), zap.Int("port", port), zap.Error(err))
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
logger.Info("Backend connection established (HA)",
|
||||
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")
|
||||
}
|
||||
|
||||
// Single-host mode (no peers configured).
|
||||
if err := tryConnect(config.Address, config.Port, cFrom, cKey); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Info("Backend connection established",
|
||||
zap.String("address", config.Address),
|
||||
zap.Int("port", config.Port))
|
||||
|
|
|
|||
|
|
@ -7,4 +7,10 @@ type BackendConfig struct {
|
|||
Port int // controller port (e.g. 9990)
|
||||
From string // identity sent to controller (default: "xce_apiserver")
|
||||
Key string // authentication key
|
||||
|
||||
// Peers lists additional controller candidates for HA mode in "host:port"
|
||||
// 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
|
||||
}
|
||||
|
|
|
|||
22
src/frontend/apiserver/backend/peers.go
Normal file
22
src/frontend/apiserver/backend/peers.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package backend
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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.
|
||||
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)
|
||||
}
|
||||
host = peer[:idx]
|
||||
port, err = strconv.Atoi(peer[idx+1:])
|
||||
if err != nil || port <= 0 || port > 65535 {
|
||||
return "", 0, fmt.Errorf("invalid port in peer %q", peer)
|
||||
}
|
||||
return host, port, nil
|
||||
}
|
||||
95
src/frontend/apiserver/backend/peers_test.go
Normal file
95
src/frontend/apiserver/backend/peers_test.go
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
package backend
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParsePeer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
peer string
|
||||
wantHost string
|
||||
wantPort int
|
||||
wantError bool
|
||||
}{
|
||||
{
|
||||
name: "valid simple host:port",
|
||||
peer: "ctrl01:9990",
|
||||
wantHost: "ctrl01",
|
||||
wantPort: 9990,
|
||||
},
|
||||
{
|
||||
name: "valid IPv4",
|
||||
peer: "192.168.1.10:9990",
|
||||
wantHost: "192.168.1.10",
|
||||
wantPort: 9990,
|
||||
},
|
||||
{
|
||||
name: "valid FQDN",
|
||||
peer: "ctrl01.cluster.local:9990",
|
||||
wantHost: "ctrl01.cluster.local",
|
||||
wantPort: 9990,
|
||||
},
|
||||
{
|
||||
name: "min valid port",
|
||||
peer: "host:1",
|
||||
wantHost: "host",
|
||||
wantPort: 1,
|
||||
},
|
||||
{
|
||||
name: "max valid port",
|
||||
peer: "host:65535",
|
||||
wantHost: "host",
|
||||
wantPort: 65535,
|
||||
},
|
||||
{
|
||||
name: "missing colon",
|
||||
peer: "ctrl019990",
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
name: "port zero",
|
||||
peer: "ctrl01:0",
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
name: "port out of range",
|
||||
peer: "ctrl01:65536",
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
name: "non-numeric port",
|
||||
peer: "ctrl01:abc",
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
name: "colon at start (empty host)",
|
||||
peer: ":9990",
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
peer: "",
|
||||
wantError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
host, port, err := parsePeer(tc.peer)
|
||||
if tc.wantError {
|
||||
if err == nil {
|
||||
t.Errorf("parsePeer(%q): expected error, got host=%q port=%d", tc.peer, host, port)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("parsePeer(%q): unexpected error: %v", tc.peer, err)
|
||||
}
|
||||
if host != tc.wantHost {
|
||||
t.Errorf("host: got %q, want %q", host, tc.wantHost)
|
||||
}
|
||||
if port != tc.wantPort {
|
||||
t.Errorf("port: got %d, want %d", port, tc.wantPort)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -29,4 +29,31 @@ func TestEvalSelectForHost_StubErrNoCGO(t *testing.T) {
|
|||
func TestCleanup_StubNoOp(t *testing.T) {
|
||||
// Should not panic and completes immediately
|
||||
Cleanup()
|
||||
}
|
||||
|
||||
// TestRegisterToBackend_StubSingleHost verifies the stub returns errNoCGO
|
||||
// for a single-host BackendConfig (no Peers).
|
||||
func TestRegisterToBackend_StubSingleHost(t *testing.T) {
|
||||
err := RegisterToBackend(&BackendConfig{
|
||||
Address: "ctrl01",
|
||||
Port: 9990,
|
||||
From: "xce_apiserver",
|
||||
Key: "keepsecret$",
|
||||
})
|
||||
if err != errNoCGO {
|
||||
t.Errorf("expected errNoCGO, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRegisterToBackend_StubWithPeers verifies the stub returns errNoCGO
|
||||
// even when Peers are configured (HA mode), since CGO is not available.
|
||||
func TestRegisterToBackend_StubWithPeers(t *testing.T) {
|
||||
err := RegisterToBackend(&BackendConfig{
|
||||
Peers: []string{"ctrl01:9990", "ctrl02:9990"},
|
||||
From: "xce_apiserver",
|
||||
Key: "keepsecret$",
|
||||
})
|
||||
if err != errNoCGO {
|
||||
t.Errorf("expected errNoCGO with peers configured, got %v", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -66,6 +66,7 @@ func main() {
|
|||
Port: utils.BackendYamlConfig.Port,
|
||||
From: utils.BackendYamlConfig.From,
|
||||
Key: utils.BackendYamlConfig.Key,
|
||||
Peers: utils.BackendYamlConfig.Peers,
|
||||
}
|
||||
if err := backend.RegisterToBackend(bcfg); err != nil {
|
||||
logger.Error("Failed to connect to backend", zap.Error(err))
|
||||
|
|
|
|||
|
|
@ -13,10 +13,15 @@ import (
|
|||
)
|
||||
|
||||
type BackendConfig struct {
|
||||
Address string `yaml:"address"`
|
||||
Port int `yaml:"port"`
|
||||
From string `yaml:"from"`
|
||||
Key string `yaml:"key"`
|
||||
Address string `yaml:"address"`
|
||||
Port int `yaml:"port"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type AppServerConfig struct {
|
||||
|
|
@ -250,6 +255,7 @@ func InitConf() error {
|
|||
Port: viper.GetInt("backend.port"),
|
||||
From: viper.GetString("backend.from"),
|
||||
Key: viper.GetString("backend.key"),
|
||||
Peers: viper.GetStringSlice("backend.peers"),
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue