4 Commits

Author SHA1 Message Date
noroot
25bc9c3673 chore(release): v1.3.36 stable 2026-09-11 12:37:09 +02:00
noroot
a54d367c24 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>
2026-09-11 12:36:30 +02:00
noroot
6149670375 chore(release): v1.3.35 stable 2026-09-11 12:26:20 +02:00
noroot
1b13df4032 fix(cluster): pg_role fehlte im SELECT — Feld kam nie beim Client an
baseSelect in cluster/store.go selektierte pg_role nicht. HANode.PGRole
war dadurch ueberall ein leerer String, wo Store.List/Get benutzt wird:
/cluster/nodes, /cluster/status und damit auch die pg_role-Spalte der
Cluster-Seite. Aufgefallen ist es erst, als die Dashboard-Cluster-Karte
in v1.3.33 auf pg_role umgestellt wurde und daraufhin bei beiden Knoten
"—" zeigte — der Fehler lag aber schon vorher im Store.

pg_role ist jetzt in baseSelect, im RETURNING von UpsertSelf und in
scanNode ergaenzt; die drei muessen spaltenweise zueinander passen.
UpsertSelf SCHREIBT pg_role weiterhin bewusst nicht (ON CONFLICT laesst
die Spalte unangetastet, damit eine Self-Registrierung den per promote
gesetzten Wert nicht ueberbuegelt) — liest sie aber im RETURNING mit,
sonst stimmt die Scan-Reihenfolge nicht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 12:25:40 +02:00
3 changed files with 49 additions and 18 deletions

View File

@@ -1 +1 @@
1.3.34
1.3.36

View File

@@ -25,8 +25,15 @@ type Store struct {
func NewStore(pool *pgxpool.Pool) *Store { return &Store{Pool: pool} }
// baseSelect MUSS spaltenweise zu scanNode passen. pg_role fehlte hier
// urspruenglich (Befund 2026-09-11): HANode.PGRole kam dadurch ueberall als
// leerer String an, wo Store.List/Get benutzt wird — /cluster/nodes,
// /cluster/status und damit auch die pg_role-Spalte der Cluster-Seite.
// UpsertSelf schreibt pg_role bewusst NICHT (ON CONFLICT laesst die Spalte
// unangetastet), liest sie aber im RETURNING mit — sonst passt die
// Scan-Reihenfolge nicht.
const baseSelect = `
SELECT id, name, fqdn, api_url, public_ip, internal_ip, mgmt_ip, role,
SELECT id, name, fqdn, api_url, public_ip, internal_ip, mgmt_ip, role, pg_role,
version, config_hash, status,
last_seen, joined_at, created_at, updated_at
FROM ha_nodes
@@ -90,7 +97,7 @@ ON CONFLICT (id) DO UPDATE SET
last_seen = EXCLUDED.last_seen,
updated_at = NOW()
RETURNING id, name, fqdn, api_url, public_ip, internal_ip, mgmt_ip,
role, version, config_hash, status,
role, pg_role, version, config_hash, status,
last_seen, joined_at, created_at, updated_at`,
n.ID, n.Name, n.FQDN, n.APIURL,
n.PublicIP, n.InternalIP, n.MgmtIP,
@@ -181,7 +188,7 @@ func scanNode(row interface{ Scan(...any) error }) (*models.HANode, error) {
if err := row.Scan(
&n.ID, &n.Name, &n.FQDN, &n.APIURL,
&n.PublicIP, &n.InternalIP, &n.MgmtIP,
&n.Role, &n.Version, &n.ConfigHash, &n.Status,
&n.Role, &n.PGRole, &n.Version, &n.ConfigHash, &n.Status,
&n.LastSeen, &n.JoinedAt,
&n.CreatedAt, &n.UpdatedAt,
); err != nil {

View File

@@ -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-<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, ".", "-"))
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