fix(cluster): duplicate-key-Warnung bei jedem API-Start auf dem Standby

preRegisterPrimary legte unbedingt eine Platzhalter-Zeile fuer den
Primary an. Sobald der Primary sich aber selbst gemeldet hat, existiert
bereits eine Zeile mit seiner ECHTEN Node-ID und demselben FQDN — und
UpsertSelf nutzt ON CONFLICT (id), greift also nicht. Die Synthetik-ID
"prenode-<fqdn>" lief damit in den fqdn-Unique-Index:

  duplicate key value violates unique constraint "ha_nodes_fqdn_unique"

bei JEDEM API-Start auf dem Standby. Folgenlos — die echte Zeile ist ja
korrekt und der Firewall-Reload lief trotzdem — aber es sah nach einem
Defekt aus und verdeckte echte Warnungen im selben Log.

Jetzt wird zuerst die IP einer vorhandenen Zeile aufgefrischt (mehr will
die Funktion gar nicht) und nur bei RowsAffected()==0 der Platzhalter
angelegt — also genau dann, wenn der Primary sich noch nie gemeldet hat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
noroot
2026-09-11 12:36:30 +02:00
parent 6149670375
commit a54d367c24

View File

@@ -253,7 +253,30 @@ func (h *SetupHandler) preRegisterPrimary(primaryFQDN string) {
} }
ip := addrs[0] ip := addrs[0]
// Stable ID so repeated calls (join + startup) don't accumulate rows. // Sobald der Primary sich selbst gemeldet hat (Heartbeat-Push), steht
// hier bereits eine Zeile mit seiner ECHTEN Node-ID. Dann ist nur die
// IP frisch zu halten — mehr will diese Funktion gar nicht.
//
// Vorher lief in dem Fall unbedingt der Platzhalter-Upsert unten, der
// am fqdn-Unique-Index scheiterte: UpsertSelf nutzt ON CONFLICT (id),
// und die Synthetik-ID "prenode-<fqdn>" trifft die echte Zeile nicht.
// Ergebnis war ein "duplicate key value violates unique constraint
// ha_nodes_fqdn_unique" bei JEDEM API-Start auf dem Standby — folgenlos
// (die echte Zeile stimmt ja), aber es sah nach einem Defekt aus und
// verdeckte echte Warnungen.
tag, err := h.ClusterStore.Pool.Exec(ctx,
`UPDATE ha_nodes SET public_ip = $1, updated_at = NOW() WHERE fqdn = $2`,
ip, primaryFQDN)
if err != nil {
slog.Warn("setup: refreshing primary IP in ha_nodes failed",
"fqdn", primaryFQDN, "error", err)
return
}
if tag.RowsAffected() == 0 {
// Noch keine Zeile: Platzhalter anlegen, damit @peer_ipv4 den
// Primary schon kennt, bevor er sich das erste Mal meldet.
// Stabile ID, damit wiederholte Aufrufe (Join + Start) keine
// Zeilen anhaeufen.
nodeID := fmt.Sprintf("prenode-%s", strings.ReplaceAll(primaryFQDN, ".", "-")) nodeID := fmt.Sprintf("prenode-%s", strings.ReplaceAll(primaryFQDN, ".", "-"))
n := models.HANode{ n := models.HANode{
ID: nodeID, ID: nodeID,
@@ -264,11 +287,12 @@ func (h *SetupHandler) preRegisterPrimary(primaryFQDN string) {
Status: "online", Status: "online",
} }
n.PublicIP = &ip n.PublicIP = &ip
if _, err := h.ClusterStore.UpsertSelf(ctx, n); err != nil { if _, err := h.ClusterStore.UpsertSelf(ctx, n); err != nil {
slog.Warn("setup: pre-register primary in ha_nodes failed", "fqdn", primaryFQDN, "error", err) slog.Warn("setup: pre-register primary in ha_nodes failed",
"fqdn", primaryFQDN, "error", err)
return return
} }
}
if err := h.PeerReloader(ctx); err != nil { if err := h.PeerReloader(ctx); err != nil {
slog.Warn("setup: PeerReloader failed after primary pre-register", "error", err) slog.Warn("setup: PeerReloader failed after primary pre-register", "error", err)
return return