feat(cluster): bidirektionaler Peer-Heartbeat (Primary→Secondary Push) — v1.2.97
Bisher pushte nur der Secondary seine Liveness an den Primary (runPrimaryPush). Der Primary pushte nichts → in der lokalen ha_nodes des Secondary fror die Primary-Row nach dem Boot ein → die vom Secondary ausgelieferte UI zeigte den Primary als offline. Neu: runPeerPush auf dem Primary/Founder pusht alle 30s self (role=primary) an jeden Peer via mTLS (/agent/cluster/peers). PushSelfToPeer(role) generalisiert PushSelfToPrimary; registerPeerRequest+AgentRegisterPeer akzeptieren ein role-Feld (default 'peer' → joining-Peer-Verhalten unverändert). Peer-Register-Log bei Routine-Pushes auf Debug (Info nur bei neuem Peer/IP-Wechsel) gegen 30s-Spam. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -866,6 +866,7 @@ type registerPeerRequest struct {
|
||||
MgmtIP string `json:"mgmt_ip"` // optional
|
||||
Version string `json:"version"`
|
||||
ConfigHash *string `json:"config_hash"` // nil=absent (don't change), ""=no user config
|
||||
Role string `json:"role"` // "" → "peer" (joining peer); "primary" beim Push des Primary
|
||||
}
|
||||
|
||||
// AgentRegisterPeer: vom Joiner nach issue-cert via mTLS aufgerufen.
|
||||
@@ -904,12 +905,21 @@ func (h *ClusterHandler) AgentRegisterPeer(c *gin.Context) {
|
||||
// Node, hier ist der „Self" der joining-Peer auf dieser Primary-Seite.
|
||||
// Der Name passt nicht 100% semantisch, aber das SQL ist exakt das was
|
||||
// wir brauchen.)
|
||||
// Rolle aus dem Request (default "peer"). Ein joining-Peer sendet keine
|
||||
// Rolle → "peer". Der Primary-Push sendet "primary", damit die vom
|
||||
// Secondary ausgelieferte UI den Primary korrekt als primary zeigt.
|
||||
// Cert-CN authentifiziert die FQDN; role ist node-lokal/Anzeige (echte
|
||||
// Rollenerkennung läuft über pg_publication).
|
||||
role := strings.TrimSpace(req.Role)
|
||||
if role == "" {
|
||||
role = "peer"
|
||||
}
|
||||
n := models.HANode{
|
||||
ID: req.ID,
|
||||
Name: req.Name,
|
||||
FQDN: req.FQDN,
|
||||
APIURL: req.APIURL,
|
||||
Role: "peer",
|
||||
Role: role,
|
||||
Status: "online", // peer IS online — it just connected via mTLS
|
||||
}
|
||||
if req.PublicIP != "" {
|
||||
@@ -965,7 +975,14 @@ func (h *ClusterHandler) AgentRegisterPeer(c *gin.Context) {
|
||||
}()
|
||||
}
|
||||
|
||||
slog.Info("cluster: peer registered via mTLS",
|
||||
// Bei neuem Peer / IP-Wechsel als Info loggen (relevantes Ereignis),
|
||||
// sonst Debug — die periodischen 30s-Pushes (runPrimaryPush/runPeerPush)
|
||||
// würden sonst das Log fluten.
|
||||
logFn := slog.Debug
|
||||
if ipChanged {
|
||||
logFn = slog.Info
|
||||
}
|
||||
logFn("cluster: peer registered via mTLS",
|
||||
"id", out.ID, "fqdn", out.FQDN, "role", out.Role, "status", out.Status,
|
||||
"client_cn", cn, "remote", c.ClientIP())
|
||||
response.OK(c, out)
|
||||
|
||||
Reference in New Issue
Block a user