From a54d367c242ed05300a5bbec84bf3a2f35eefd89 Mon Sep 17 00:00:00 2001 From: noroot Date: Fri, 11 Sep 2026 12:36:30 +0200 Subject: [PATCH] fix(cluster): duplicate-key-Warnung bei jedem API-Start auf dem Standby MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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-" 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 --- internal/handlers/setup.go | 52 ++++++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/internal/handlers/setup.go b/internal/handlers/setup.go index 3cd71da..f71733c 100644 --- a/internal/handlers/setup.go +++ b/internal/handlers/setup.go @@ -253,22 +253,46 @@ func (h *SetupHandler) preRegisterPrimary(primaryFQDN string) { } ip := addrs[0] - // Stable ID so repeated calls (join + startup) don't accumulate rows. - nodeID := fmt.Sprintf("prenode-%s", strings.ReplaceAll(primaryFQDN, ".", "-")) - n := models.HANode{ - ID: nodeID, - Name: primaryFQDN, - FQDN: primaryFQDN, - APIURL: "https://" + primaryFQDN + ":3443", - Role: "primary", - Status: "online", - } - n.PublicIP = &ip - - if _, err := h.ClusterStore.UpsertSelf(ctx, n); err != nil { - slog.Warn("setup: pre-register primary in ha_nodes failed", "fqdn", primaryFQDN, "error", err) + // 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-" 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, ".", "-")) + n := models.HANode{ + ID: nodeID, + Name: primaryFQDN, + FQDN: primaryFQDN, + APIURL: "https://" + primaryFQDN + ":3443", + Role: "primary", + Status: "online", + } + n.PublicIP = &ip + if _, err := h.ClusterStore.UpsertSelf(ctx, n); err != nil { + slog.Warn("setup: pre-register primary in ha_nodes failed", + "fqdn", primaryFQDN, "error", err) + return + } + } if err := h.PeerReloader(ctx); err != nil { slog.Warn("setup: PeerReloader failed after primary pre-register", "error", err) return