fix: replace the blocking StatefulSet-ready wait with reconcile-loop gates#402
fix: replace the blocking StatefulSet-ready wait with reconcile-loop gates#402xrl wants to merge 2 commits into
Conversation
reconcileStatefulSet no longer sleeps up to 45s inside the worker (waitForStatefulSetReady removed); it returns the CreateOrPatch response, which is fresher than the post-write cache read it replaces. reconcileClusterState now requeues instead: an observed-generation gate before reading Spec.Replicas (own-write cache staleness), and a settled gate (ObservedGeneration + ReadyReplicas==Replicas, the old wait predicate) before membership mutations. The settled gate sits after the replica/member-count mismatch recovery so an interrupted scale-in, whose orphaned pod can never become ready, still unblocks. The Owns(StatefulSet) watch plus the 10s requeue backstop drive convergence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Xavier Lange <xrlange@gmail.com>
Review follow-ups to the gate rework: Health checks now requeue instead of erroring while the StatefulSet is unsettled: after bootstrap scales 0->1 the endpoint is unreachable until the pod starts, and the old blocking wait absorbed that window; without this every new cluster inflated reconcile error metrics on the happy path. Once settled, health failures surface as errors as before. Spec-driven scale-in is exempt from the settled gate. Gating it wedged shrinks whenever a pod was NotReady but its etcd still answered (dead kubelet, lagging readiness) -- exactly the pod an admin shrinks away from. The exemption is quorum-safe: reaching the gate means every member passed health checks, and the member removed first is the last ordinal. The settled gate now also emits a StatefulSetNotSettled warning event; with the health-check change it only fires when etcd is healthy but pod readiness lags, so a persistent stall is visible on the CR instead of only in operator logs. Remaining known delta vs origin/main for the PR body: none for genuinely dead members -- those already wedge at performHealthChecks on main; this PR now just requeues quietly (Degraded condition still set) instead of erroring while unsettled. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Xavier Lange <xrlange@gmail.com>
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: xrl The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
|
Hi @xrl. Thanks for your PR. I'm waiting for a etcd-io member to verify that this patch is reasonable to test. If it is, they should reply with Regular contributors should join the org to skip this step. Once the patch is verified, the new status will be reflected by the I understand the commands that are listed here. DetailsInstructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. |
What
reconcileStatefulSetslept up to 45s inside the reconcile worker (waitForStatefulSetReady, exponential backoff pollingReadyReplicas == Replicas). That blocks the worker for the whole rollout, and with a single worker it head-of-line blocks every other EtcdCluster. This PR removes the blocking wait and re-plumbs the same predicate into the reconcile loop as requeue gates:reconcileStatefulSetnow returns theCreateOrPatchresponse directly — fresher than the post-write cache read it replaces, which could still hold the pre-write object.reconcileClusterStaterequeues before readingSpec.Replicasif the StatefulSet controller hasn't observed the latest spec write (own-write cache staleness), so the operator never acts on a spec the workload controller hasn't seen.ObservedGenerationcaught up andReadyReplicas == Replicas— the old wait's predicate, now non-blocking. TheOwns(StatefulSet)watch plus the existing 10s requeue backstop drive convergence.Gate placement is deliberate:
performHealthChecksfailures requeue instead of erroring: after bootstrap scales 0→1 the endpoint is unreachable until the pod starts, a window the old blocking wait absorbed — without this every new cluster inflates reconcile error metrics on the happy path. Once settled, health failures surface as errors exactly as before.StatefulSetNotSettledwarning event, so it's visible on the CR instead of only in operator logs.Files
internal/controller/utils.go— deletewaitForStatefulSetReady;createOrPatchStatefulSetreturns the written object; addstatefulSetSpecObserved/isStatefulSetSettledpredicates.internal/controller/etcdcluster_controller.go— the two gates inreconcileClusterState, the unsettled health-check requeue inReconcile, and theStatefulSetNotSettledevent.Review
An adversarial review pass produced 3 findings; all blocking/important ones resolved (second commit): the settled gate wedged spec-driven scale-in whenever a pod was NotReady but its etcd still answered — exactly the pod an admin shrinks away from (blocking — scale-in exempted, quorum-safety argued above); every new cluster erred through health checks during the bootstrap 0→1 window the old wait used to absorb (important — requeue while unsettled); and a persistent gate stall was invisible outside operator logs (resolved with the warning event).
Testing
TestReconcileClusterStateGates: stale generation requeues without writes, unsettled replicas requeue before scale-out, spec-driven scale-in is exempt from the settled gate, interrupted scale-in recovery is not gated.TestReconcileClusterStateStaleGenerationEnvtest: envtest-backed own-write staleness scenario through the real cache.TestStatefulSetSpecObserved/TestIsStatefulSetSettled: predicate table tests (nil replicas, generation skew, partial readiness).internal/controller/utils_test.goreworked forcreateOrPatchStatefulSetreturning the object;waitForStatefulSetReadytests deleted with the function.make testandmake verifypass.Descoped follow-ups
(1) In-memory expectations mechanism (ECK-style per-cluster expected-Generation map recorded after each write): descoped — the ObservedGeneration gate plus
reconcileClusterState's existing replica/memberCnt cross-check (etcdcluster_controller.go:284, which turns own-write cache staleness into an idempotent re-patch) already neutralize the race once the blocking wait is gone; an expectations store adds a stateful component and restart semantics for marginal gain. Revisit with the MaxConcurrentReconciles follow-up if soak shows residual double-acts. (2)MaxConcurrentReconcilesbump: explicitly excluded from this PR; noted as follow-up (see #379). (3)UpdatedReplicas/CurrentRevision==UpdateRevisionchecks in the settled predicate: descoped — the old wait only checkedReadyReplicas==Replicas; adding rollout-revision checks would be a behavior change beyond re-plumbing. (4) GatingbootstrapStatefulSet's 0→1 scale on the gate: unnecessary (idempotent re-patch, no pods yet) and it would add bootstrap latency visible to e2e. (5) New CRD status fields / conditions for "waiting on StatefulSet": descoped — no API change keeps the PR marker-free and generated-file-free. (6) ReworkingperformHealthCheckserror noise during pod startup in general (e.g. treating connection-refused as requeue-not-error even when settled): descoped, separate concern.🤖 Generated with Claude Code
PR series — operability fixes, TLS & EtcdMirror
Small single-purpose PRs from live kind-cluster testing of the operator. Each stands alone unless an After is listed. → = this PR.
events.k8s.ioRBAC so operator Events are actually recordedmembers[]/leaderIDfrom one health snapshot (consistent leader)altNames.ipAddressesinto certificatesvalidityDuration(365d,100d12h) as documentedDegradedcondition (was empty status)--max-concurrent-reconciles)podTemplate.specscheduling fields: topologySpread, resources, priorityClass, schedulerNameconfig/surfacespec.tls.{peer,client}surfaces (breaking alpha API)TLSReadycondition + TLS lifecycle EventsPeerCANotSharedEtcdMirrorCRD: one-way cross-cluster replication API (types + CEL)pkg/mirroragentreplication engine (fenced checkpoint-in-target)mirror-agentbinary: config/TLS-reload/statusz/metrics + kind e2e/metricsendpointEtcdBackupCR → object storage (S3/GCS)🟢 ready · ⚪ draft