fix(cluster): Placeholder zuerst löschen vor UpsertSelf (UNIQUE-fqdn-Bug)

ha_nodes hat UNIQUE(fqdn). UpsertSelf nutzt ON CONFLICT(id) — schlägt
bei fqdn-Konflikten fehl. Wenn Placeholder und echter Node die gleiche
FQDN haben, schlug der INSERT der echten Row mit "duplicate key on
ha_nodes_fqdn_unique" fehl.

Root cause für "joining" stuck forever: der Peer blieb ewig als
Placeholder weil AgentRegisterPeer + reconcileJoiningPeers die Echte-
ID-Row nie erfolgreich einfügen konnten.

Fix: DeletePlaceholdersByFQDN vor UpsertSelf in beiden Code-Paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-29 19:45:02 +02:00
parent ffb579879d
commit c1a4ccff8f
5 changed files with 14 additions and 9 deletions

View File

@@ -532,13 +532,16 @@ func (h *ClusterHandler) reconcileJoiningPeers(placeholders []models.HANode) {
if n.InternalIP == nil {
n.InternalIP = placeholder.InternalIP
}
// Placeholder zuerst löschen: ha_nodes hat UNIQUE(fqdn). Ohne
// dieses Delete würde UpsertSelf (ON CONFLICT(id)) mit fqdn-
// unique-Violation scheitern.
_ = h.Store.DeletePlaceholdersByFQDN(ctx, n.FQDN, n.ID)
out, err := h.Store.UpsertSelf(ctx, n)
if err != nil {
slog.Warn("cluster: reconcile joining peer: upsert failed",
"fqdn", n.FQDN, "real_id", n.ID, "error", err)
continue
}
_ = h.Store.DeletePlaceholdersByFQDN(ctx, n.FQDN, n.ID)
changed = true
slog.Info("cluster: joining peer reconciled via identity pull",
"id", out.ID, "fqdn", out.FQDN, "placeholder_id", placeholder.ID)
@@ -690,16 +693,18 @@ func (h *ClusterHandler) AgentRegisterPeer(c *gin.Context) {
v := req.Version
n.Version = &v
}
// Placeholder zuerst löschen: ha_nodes hat UNIQUE(fqdn). Der INSERT
// in UpsertSelf verwendet ON CONFLICT(id) — greift NICHT bei fqdn-
// Konflikten. Ohne das Delete würde der INSERT mit "duplicate key on
// ha_nodes_fqdn_unique" scheitern und der Peer bliebe ewig "joining".
_ = h.Store.DeletePlaceholdersByFQDN(c.Request.Context(), req.FQDN, req.ID)
out, err := h.Store.UpsertSelf(c.Request.Context(), n)
if err != nil {
response.Internal(c, err)
return
}
// Remove ALL placeholder rows for this FQDN (both legacy "pre-{timestamp}"
// and current "prenode-{fqdn}" style) — the real row just took their place.
_ = h.Store.DeletePlaceholdersByFQDN(c.Request.Context(), req.FQDN, req.ID)
// Firewall-Reload damit peer_ipv4-Set die neue IP aufnimmt. Best-
// effort: Fehler loggen, Response weiter durchreichen — der Peer
// hat seine Identity erfolgreich registriert, Operator kann manuell