Skip to content

Commit ace25a4

Browse files
lmicciniclaude
andcommitted
Prevent unnecessary RabbitMQ rolling restarts during scaling and cert rotation
Three changes to align with the upstream rabbitmq-cluster-operator and prevent quorum queue corruption caused by unnecessary rolling restarts: 1. Remove TLS secret data and replica count from config hash Stop hashing TLS secret content (tlsSecretData, caSecretData) and the replica count into the pod-template annotation. The secret names are still hashed so that switching to a different cert triggers a restart, but content changes (cert-manager re-issuance, CA chain updates, rotation) no longer cause rolling restarts. TLS certificates are mounted via projected volumes that kubelet updates in-place, and RabbitMQ 4.x reloads them from disk automatically. During initial deployment the playbook scales RabbitMQ from 1 to 3 replicas, which adds new DNS SANs to the Certificate CR and triggers cert-manager to re-issue. When secret data was hashed, this caused a rolling restart before quorum queue replicas had synced, leading to Raft log divergence and a permanent crash loop (ra_server assertion "PLIdx < LastApplied"). 2. Make PreStop hook replica-count-independent The PreStop command previously varied based on spec.replicas (skipping quorum checks for single-replica clusters). This meant scaling from 1 to 3 changed the pod template, triggering a restart of existing pods. Now the quorum check is conditional at runtime: it queries rabbit_nodes:list_running() and only runs await_online_quorum_plus_one when more than one node is active. Timeouts are reduced from 600s to 10s for quorum checks and 30s for drain, fitting within the 60s terminationGracePeriodSeconds. If quorum is already satisfied (normal rolling restart), the check passes instantly; in degraded scenarios it times out quickly rather than blocking. 3. Set explicit UpdateStrategy on the StatefulSet Always set RollingUpdate with Partition=0, matching the upstream cluster-operator. This prevents stale partition values (from migration or manual debugging) from silently blocking pod updates. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 517307d commit ace25a4

3 files changed

Lines changed: 30 additions & 40 deletions

File tree

internal/controller/rabbitmq/rabbitmq_controller.go

Lines changed: 6 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -388,45 +388,22 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ct
388388
}
389389

390390
// Calculate hash for config tracking.
391-
// Include TLS secret content so that certificate rotation (e.g. by
392-
// cert-manager) triggers a rolling restart of the StatefulSet pods.
391+
// TLS cert content is NOT included: certs are mounted via projected
392+
// volumes that kubelet updates in-place, and RabbitMQ 4.x reloads
393+
// them automatically. Hashing cert data here would trigger a
394+
// rolling restart on every cert-manager update, which can corrupt
395+
// quorum queues when replicas haven't fully synced yet.
393396
hashInput := map[string]interface{}{
394397
"ipv6Enabled": IPv6Enabled,
395398
"fipsEnabled": fipsEnabled,
396399
"tlsSecret": instance.Spec.TLS.SecretName,
400+
"caSecret": instance.Spec.TLS.CaSecretName,
397401
"additionalConfig": instance.Spec.Rabbitmq.AdditionalConfig,
398402
"advancedConfig": instance.Spec.Rabbitmq.AdvancedConfig,
399403
"envConfig": instance.Spec.Rabbitmq.EnvConfig,
400404
"erlangInetConfig": instance.Spec.Rabbitmq.ErlangInetConfig,
401405
"additionalPlugins": instance.Spec.Rabbitmq.AdditionalPlugins,
402406
"containerImage": instance.Spec.ContainerImage,
403-
"replicas": instance.Spec.Replicas,
404-
}
405-
if instance.Spec.TLS.SecretName != "" {
406-
tlsSecret := &corev1.Secret{}
407-
if err := r.Get(ctx, types.NamespacedName{
408-
Name: instance.Spec.TLS.SecretName,
409-
Namespace: instance.Namespace,
410-
}, tlsSecret); err != nil {
411-
if !k8s_errors.IsNotFound(err) {
412-
return ctrl.Result{}, fmt.Errorf("failed to get TLS secret %s: %w", instance.Spec.TLS.SecretName, err)
413-
}
414-
} else {
415-
hashInput["tlsSecretData"] = tlsSecret.Data
416-
}
417-
if instance.Spec.TLS.CaSecretName != "" && instance.Spec.TLS.CaSecretName != instance.Spec.TLS.SecretName {
418-
caSecret := &corev1.Secret{}
419-
if err := r.Get(ctx, types.NamespacedName{
420-
Name: instance.Spec.TLS.CaSecretName,
421-
Namespace: instance.Namespace,
422-
}, caSecret); err != nil {
423-
if !k8s_errors.IsNotFound(err) {
424-
return ctrl.Result{}, fmt.Errorf("failed to get CA secret %s: %w", instance.Spec.TLS.CaSecretName, err)
425-
}
426-
} else {
427-
hashInput["caSecretData"] = caSecret.Data
428-
}
429-
}
430407
}
431408
configMapHash, err := util.ObjectHash(hashInput)
432409
if err != nil {

internal/rabbitmq/statefulset.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,12 @@ func StatefulSet(
143143
ServiceName: fmt.Sprintf("%s-nodes", r.Name),
144144
Replicas: replicas,
145145
PodManagementPolicy: appsv1.ParallelPodManagement,
146+
UpdateStrategy: appsv1.StatefulSetUpdateStrategy{
147+
RollingUpdate: &appsv1.RollingUpdateStatefulSetStrategy{
148+
Partition: ptr.To(int32(0)),
149+
},
150+
Type: appsv1.RollingUpdateStatefulSetStrategyType,
151+
},
146152
Selector: &metav1.LabelSelector{
147153
MatchLabels: matchls,
148154
},
@@ -391,11 +397,16 @@ func buildReadinessProbe(r *rabbitmqv1.RabbitMq) *corev1.Probe {
391397
// For multi-replica clusters, the PreStop hook waits for quorum safety
392398
// before draining. For single-replica clusters, quorum checks are
393399
// skipped since there are no other nodes to maintain quorum.
394-
func buildLifecycle(r *rabbitmqv1.RabbitMq) *corev1.Lifecycle {
395-
preStopCmd := `if [ ! -z "$(cat /etc/pod-info/skipPreStopChecks)" ]; then exit 0; fi; rabbitmq-upgrade await_online_quorum_plus_one -t 600 && rabbitmq-upgrade await_online_synchronized_mirror -t 600 || true && rabbitmq-upgrade drain -t 600`
396-
if r.Spec.Replicas != nil && *r.Spec.Replicas <= 1 {
397-
preStopCmd = `if [ ! -z "$(cat /etc/pod-info/skipPreStopChecks)" ]; then exit 0; fi; rabbitmq-upgrade drain -t 600`
398-
}
400+
func buildLifecycle(_ *rabbitmqv1.RabbitMq) *corev1.Lifecycle {
401+
// The PreStop command must be identical regardless of the replica count
402+
// so that scaling does not change the pod template and trigger a rolling
403+
// restart. The quorum check is skipped at runtime when there is only one
404+
// running node (where quorum+1 can never be satisfied and would block
405+
// until timeout).
406+
preStopCmd := `if [ ! -z "$(cat /etc/pod-info/skipPreStopChecks)" ]; then exit 0; fi; ` +
407+
`if rabbitmqctl eval 'length(rabbit_nodes:list_running()) > 1.' 2>/dev/null | grep -q true; then ` +
408+
`rabbitmq-upgrade await_online_quorum_plus_one -t 10 && rabbitmq-upgrade await_online_synchronized_mirror -t 10 || true; fi; ` +
409+
`rabbitmq-upgrade drain -t 30`
399410
return &corev1.Lifecycle{
400411
PreStop: &corev1.LifecycleHandler{
401412
Exec: &corev1.ExecAction{

test/functional/rabbitmq_controller_test.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2240,7 +2240,7 @@ var _ = Describe("RabbitMQ Controller", func() {
22402240
})
22412241
})
22422242

2243-
When("TLS secret is rotated, config hash changes to trigger rolling restart", func() {
2243+
When("TLS secret data is rotated, config hash remains stable", func() {
22442244
var certSecret *corev1.Secret
22452245

22462246
BeforeEach(func() {
@@ -2254,7 +2254,7 @@ var _ = Describe("RabbitMQ Controller", func() {
22542254
DeferCleanup(th.DeleteInstance, rabbitmq)
22552255
})
22562256

2257-
It("should update the config-hash annotation on the StatefulSet when TLS secret changes", func() {
2257+
It("should NOT update the config-hash annotation when TLS secret data changes", func() {
22582258
SimulateRabbitMQClusterReady(rabbitmqName)
22592259

22602260
// Get initial config-hash from the StatefulSet
@@ -2274,12 +2274,14 @@ var _ = Describe("RabbitMQ Controller", func() {
22742274
g.Expect(th.K8sClient.Update(th.Ctx, secret)).Should(Succeed())
22752275
}, timeout, interval).Should(Succeed())
22762276

2277-
// Verify config-hash changed on the StatefulSet (triggers rolling restart)
2278-
Eventually(func(g Gomega) {
2277+
// Verify config-hash did NOT change — cert rotation is handled
2278+
// by projected volumes and RabbitMQ 4.x automatic reload, not
2279+
// by rolling restart. Hashing cert data caused quorum queue
2280+
// corruption during initial deployment.
2281+
Consistently(func(g Gomega) {
22792282
sts := GetRabbitMQStatefulSet(rabbitmqName)
22802283
newHash := sts.Spec.Template.Annotations["config-hash"]
2281-
g.Expect(newHash).NotTo(BeEmpty())
2282-
g.Expect(newHash).NotTo(Equal(initialHash), "config-hash should change when TLS secret is rotated")
2284+
g.Expect(newHash).To(Equal(initialHash), "config-hash should not change on TLS secret data rotation")
22832285
}, timeout, interval).Should(Succeed())
22842286
})
22852287
})

0 commit comments

Comments
 (0)