Treat host names, not list positions, as identity: fix wrong keeper replica drops and host renames#2044
Open
xavierleune wants to merge 2 commits into
Open
Conversation
…moved ActionPlan.WalkRemoved relied on messagediff's positional slice diff: removing hosts from the head or the middle of the replicas list reported the TAIL hosts as removed (they survive by name), while the actually removed hosts appeared as renames. dropZKReplicas then issued SYSTEM DROP REPLICA for replicas that stay in the cluster and never for the removed ones, leaving stale replicas in (Zoo)Keeper. K8s objects are managed by name, so compute removed hosts by name as well. Additionally, run the drop on a host that survives in the new CR: the previous executor - first host of the OLD shard - is itself one of the removed hosts when the head of the list is removed, so the SQL was sent to an already deleted pod and always failed.
normalizeHostName kept an explicitly specified host name verbatim unless
it looked auto-generated FOR ITS CURRENT POSITION: IsAutoGeneratedHostName
compares the name against index-derived patterns. With replicas named
"0".."4" under shard "0", removing entry "2" shifts hosts "3" and "4" to
positions 2 and 3: their names no longer match the patterns, they are kept
verbatim instead of normalizing to "0-3"/"0-4", and the operator creates
brand-new StatefulSets ("chi-...-3") with empty PVCs while purging the old
ones - silent data loss.
A bare-number name is an identity, not a position: canonicalize it into
the current naming scheme along the axis the hosts list varies on
("3" under shard "0" -> "0-3", "3" under replica "0" -> "3-0"), so
removing entries before it no longer changes the host's identity.
Applies to both CHI and CHK normalizers. Names matching auto-generated
patterns at their natural position keep the historical behavior, custom
non-numeric names are still kept verbatim.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1545
Problem
Host identity is positional in two places where everything else in the operator (StatefulSet names, PVC names, k8s object cleanup) is name-based. When replicas are removed from the head or middle of the
replicaslist (instead of the tail), two independent defects fire:1. The wrong replicas are dropped from (Zoo)Keeper, on a dead pod. K8s objects are cleaned up correctly, but the removed replicas stay registered in Keeper forever (
system.replicaskeeps showing them as inactive,total_replicasnever goes down). Operator logs from a 5-replica shard (0-0…0-4) after removing0-0and0-1from the spec:The operator tried to drop the surviving replicas
0-3/0-4, executed on the removed host0-0. The only reason live replicas were not actually dropped is that the executor host was already dead.2. Bare-number replica names silently change identity — with data loss. With replicas explicitly named
"0"…"4", removing entry"2"shifts"3"and"4"to list positions 2 and 3.normalizeHostNameonly recognizes a name as "auto-generated" if it matches patterns derived from the host's current index (IsAutoGeneratedHostName), so"3"at index 2 no longer normalizes to0-3— it is kept verbatim. The operator then creates a brand-new StatefulSetchi-…-3with an empty PVC and purges the oldchi-…-0-3STS and its PVC. Two healthy replicas are wiped for removing one list entry.Root cause
Both defects come from treating list position as identity while the rest of the operator uses names:
ActionPlan.WalkRemovedis built onmessagediff.DeepDiff, which compares slices positionally. Removing N hosts from the head of the list makes the diff report the tail N hosts as "removed" (they survive by name) and the head hosts as renames.dropZKReplicastherefore walked the wrong host set, and it ran the SQL onhostToDrop.GetShard().FirstHost()— the first host of the old shard, itself one of the removed hosts.normalizeHostName(CHI and CHK) decides whether an explicit host name may be rewritten by matching it against index-derived patterns, so the meaning of a bare-number name like"3"depends on where it currently sits in the list.Fix
ActionPlan.WalkRemovednow computes removed hosts by name: hosts of the old CR whose cluster/shard still exists in the new CR but whose name is no longer present there. Whole shard/cluster removals keep their existing behavior (covered by the shard/cluster callbacks). This also fixes the otherWalkRemovedconsumers:status.HostsDeleteCountand the per-host pre-delete hooks, which used to fire on surviving hosts.dropZKReplicanow accepts an explicithostToRunOn;dropZKReplicasresolves it from the new CR (first host of the same shard), i.e. a host that is guaranteed to stay. The previous behavior is kept as a fallback for the storage-loss re-provisioning path.normalizeHostName(CHI and CHK) canonicalizes an explicitly specified bare-number host name position-independently, substituting the number into the current naming scheme along the axis the hosts list varies on:"3"under shard"0"→0-3(hosts declared under shards),"3"under replica"0"→3-0(hosts declared under replicas). Names matching the auto-generated patterns at their natural position keep the historical behavior; custom non-numeric names are still kept verbatim; when hosts are declared under both shards and replicas the provenance is ambiguous and nothing changes.Behavior change / migration note
A deployment that already went through the rename footgun (e.g. spec
["0","1","3","4"]currently running with verbatim host names3/4and STSchi-…-3/chi-…-4) will see those hosts renamed to0-3/0-4(STS/PVC recreated) on upgrade. Specs with bare-number names at their natural positions, canonical{shard}-{replica}names, or custom names are normalized exactly as before.Detecting whether a deployment is affected — before upgrading, list the CHI's StatefulSets and compare against the canonical scheme: a healthy host is named
chi-{chi}-{cluster}-{shard}-{replica}(e.g.chi-x-c1-0-3), a footgun host has a bare suffix (e.g.chi-x-c1-3). If every STS follows the{shard}-{replica}form, the upgrade is a no-op name-wise and none of the below applies.Migration paths for affected deployments (the right one depends on whether every shard keeps at least one data-bearing, correctly named replica):
"3"→"0-3"), waiting for the replacement host to be recreated and to catch up (SYSTEM SYNC REPLICA, check counts /system.replicas) before doing the next. Each rename is a regular delete+add: the new host starts empty and backfills through replication, so done sequentially redundancy is never lost."0-3"is stable under both the old and the new operator. Afterwards, clean the stale Keeper entries left for the old names (SYSTEM DROP REPLICA 'chi-x-c1-3'on a surviving host — they are leftover anyway, that is Operator dropping wrong replicas when removing older replicas from cluster layout #1545 itself). Once the spec is canonical, upgrading renames nothing.["2","3","4"]after head removals): that shard would be recreated empty — total data loss.reclaimPolicytoRetain, suspend the CHI reconcile, delete the old-name STS and PVC, clear the PV'sclaimRef, pre-create a PVC with the new canonical name (data-volume-chi-x-c1-0-3-0) bound to the same PV, then upgrade: the renamed STS adopts the existing disk. Caveat: on-disk table metadata keeps the oldreplica_namein Keeper (frozen at table creation), so the replica keeps registering under its old name until tables are recreated, and the operator's post-upgradeSYSTEM DROP REPLICAfor the old name fails safely (active replica).If maintainers prefer, the canonicalization could also be made opt-out behind a chop config flag (e.g.
host.name.canonicalizeNumeric, defaulttrue) to let affected deployments migrate at their own pace — left out of this PR to avoid adding config surface, happy to add it if desired.Also note: host statuses (
WalkAdded/WalkModified) still follow the positional diff, so a head-removal still causes surviving STS to be re-created (same STS/PVC names — end state and data are correct). The "adding a replica in the middle skips schema migration" symptom mentioned in #1545 belongs to that same add-side and would be a follow-up.Reproduction playbook
Any cluster with explicitly named replicas works; no special templates are needed.
The operator normalizes these to hosts
0-0…0-3(STSchi-repro-c1-0-0…).Remove the first entry (
- name: "0") from the replicas list and re-apply.Observe, before this fix:
"2"/"3"shift positions, stop matching the auto-name patterns and are re-created as brand-new STSchi-repro-c1-2/chi-repro-c1-3with empty PVCs, whilechi-repro-c1-0-2/0-3(with the data) are purged;SYSTEM DROP REPLICAfor the wrong replica names, executed atchi-repro-c1-0-0(the deleted pod), failing withno such host; on a surviving replica,SELECT total_replicas, replica_is_active FROM system.replicas WHERE table = 't'keeps listing the removed replica andtotal_replicasnever decreases.After this fix: hosts
0-1…0-3keep their names, STS and PVCs; the operator runsSYSTEM DROP REPLICA 'chi-repro-c1-0-0'onchi-repro-c1-0-1andsystem.replicasshowstotal_replicas= 3 with no stale entry.Tests
action_plan_test.go: head/middle/tail removal, in-place rename, whole-shard removal, add-only and no-op cases.normalizer-host_test.go: bare-number name at shifted vs natural position, canonical and custom names, replicas-defined layout, ambiguous dual layout, leading-zero names.go test ./pkg/...: 609 passed; the only failures are two pre-existingTestEnforceVerifiedLegacyTLScases that also fail on a clean tree (environment-dependent TLS error message).🤖 Generated with Claude Code