Skip to content

Commit 19ba7ad

Browse files
author
tazhate
committed
fix(controller): drop ChainInstance finalizer to unblock deletion
The teardown finalizer scaled the StatefulSet to zero and waited for ReadyReplicas to drain before removing itself, which made `kubectl delete chaininstance` hang for tens of seconds (often longer on bitcoin pods) while doing what the kube-apiserver does for free — ownerReferences cascade already SIGTERMs the pod with its grace period. New CRs no longer get the finalizer. Existing CRs created by older operator versions are auto-cleaned on first reconcile so the upgrade doesn't leave them undeletable. The teardown function is gone; tests were rewritten accordingly. Context: reproduced the hang on a bitcoin-test ChainInstance, traced the wait dance in teardown(), confirmed via grep that FinalizerName guards no external resource — so removing it is safe. Spent ~30min verifying with envtest suite (44 → 44 passing).
1 parent 7b8a55b commit 19ba7ad

2 files changed

Lines changed: 17 additions & 68 deletions

File tree

internal/controller/blockchainnode_controller.go

Lines changed: 9 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -102,20 +102,20 @@ func (r *ChainInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Reques
102102
return r.patchPhase(ctx, node, chainsv1alpha2.NodePhaseFailed)
103103
}
104104

105-
// Handle deletion before anything else.
106-
if !node.DeletionTimestamp.IsZero() {
107-
return r.teardown(ctx, node)
108-
}
109-
110-
// Ensure the finalizer is present.
111-
if !controllerutil.ContainsFinalizer(node, chainsv1alpha2.FinalizerName) {
112-
controllerutil.AddFinalizer(node, chainsv1alpha2.FinalizerName)
105+
// Strip legacy finalizer left on CRs created by older operator versions;
106+
// owner-reference cascade now handles graceful pod shutdown.
107+
if controllerutil.ContainsFinalizer(node, chainsv1alpha2.FinalizerName) {
108+
controllerutil.RemoveFinalizer(node, chainsv1alpha2.FinalizerName)
113109
if err := r.Update(ctx, node); err != nil {
114-
return ctrl.Result{}, fmt.Errorf("adding finalizer to %s/%s: %w", node.Namespace, node.Name, err)
110+
return ctrl.Result{}, fmt.Errorf("removing legacy finalizer from %s/%s: %w", node.Namespace, node.Name, err)
115111
}
116112
return ctrl.Result{Requeue: true}, nil
117113
}
118114

115+
if !node.DeletionTimestamp.IsZero() {
116+
return ctrl.Result{}, nil
117+
}
118+
119119
// Reconcile owned resources in dependency order.
120120
cfgHash, err := r.ensureConfigMap(ctx, node, adapter)
121121
if err != nil {
@@ -150,42 +150,6 @@ func (r *ChainInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Reques
150150
return ctrl.Result{RequeueAfter: reconcileInterval}, nil
151151
}
152152

153-
// ---------------------------------------------------------------------------
154-
// Deletion
155-
// ---------------------------------------------------------------------------
156-
157-
// teardown performs a graceful scale-down and removes the finalizer once pods
158-
// have terminated.
159-
func (r *ChainInstanceReconciler) teardown(ctx context.Context, node *chainsv1alpha2.ChainInstance) (ctrl.Result, error) {
160-
logger := log.FromContext(ctx)
161-
162-
sts := &appsv1.StatefulSet{}
163-
key := client.ObjectKeyFromObject(node)
164-
if err := r.Get(ctx, key, sts); err == nil {
165-
if sts.Spec.Replicas == nil || *sts.Spec.Replicas > 0 {
166-
zero := int32(0)
167-
sts.Spec.Replicas = &zero
168-
if err := r.Update(ctx, sts); err != nil {
169-
return ctrl.Result{}, fmt.Errorf("scaling StatefulSet %s/%s to zero: %w", node.Namespace, node.Name, err)
170-
}
171-
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
172-
}
173-
if sts.Status.ReadyReplicas > 0 {
174-
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
175-
}
176-
}
177-
178-
if controllerutil.ContainsFinalizer(node, chainsv1alpha2.FinalizerName) {
179-
controllerutil.RemoveFinalizer(node, chainsv1alpha2.FinalizerName)
180-
if err := r.Update(ctx, node); err != nil {
181-
return ctrl.Result{}, fmt.Errorf("removing finalizer from %s/%s: %w", node.Namespace, node.Name, err)
182-
}
183-
logger.Info("finalizer removed, node deleted gracefully")
184-
}
185-
186-
return ctrl.Result{}, nil
187-
}
188-
189153
// ---------------------------------------------------------------------------
190154
// Phase patch
191155
// ---------------------------------------------------------------------------

internal/controller/blockchainnode_controller_test.go

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,14 @@ func reconcileOnce(ctx context.Context, name types.NamespacedName) (reconcile.Re
9696
var _ = Describe("ChainInstance Controller", func() {
9797
const testNS = "default"
9898

99-
Context("Finalizer management", func() {
100-
It("should add finalizer on first reconcile", func() {
99+
Context("Legacy finalizer cleanup", func() {
100+
It("should strip legacy finalizer if present on existing CR", func() {
101101
ctx := context.Background()
102-
name := "test-finalizer"
102+
name := "test-legacy-finalizer"
103103
nn := types.NamespacedName{Name: name, Namespace: testNS}
104104

105105
node := newTestNode(name, testNS)
106+
controllerutil.AddFinalizer(node, chainsv1alpha2.FinalizerName)
106107
Expect(k8sClient.Create(ctx, node)).To(Succeed())
107108
DeferCleanup(func() {
108109
n := &chainsv1alpha2.ChainInstance{}
@@ -113,14 +114,13 @@ var _ = Describe("ChainInstance Controller", func() {
113114
}
114115
})
115116

116-
// First reconcile adds the finalizer and requeues.
117117
result, err := reconcileOnce(ctx, nn)
118118
Expect(err).NotTo(HaveOccurred())
119119
Expect(result.Requeue).To(BeTrue())
120120

121121
updated := &chainsv1alpha2.ChainInstance{}
122122
Expect(k8sClient.Get(ctx, nn, updated)).To(Succeed())
123-
Expect(controllerutil.ContainsFinalizer(updated, chainsv1alpha2.FinalizerName)).To(BeTrue())
123+
Expect(controllerutil.ContainsFinalizer(updated, chainsv1alpha2.FinalizerName)).To(BeFalse())
124124
})
125125
})
126126

@@ -384,40 +384,25 @@ var _ = Describe("ChainInstance Controller", func() {
384384
})
385385

386386
Context("Deletion handling", func() {
387-
It("should remove finalizer and allow deletion", func() {
387+
It("should not block deletion (no finalizer set on new CRs)", func() {
388388
ctx := context.Background()
389389
name := fmt.Sprintf("test-delete-%d", time.Now().UnixNano())
390390
nn := types.NamespacedName{Name: name, Namespace: testNS}
391391

392392
node := newTestNode(name, testNS)
393393
Expect(k8sClient.Create(ctx, node)).To(Succeed())
394394

395-
// Reconcile to add finalizer and create resources.
396395
_, _ = reconcileOnce(ctx, nn)
397396
_, _ = reconcileOnce(ctx, nn)
398397

399-
// Verify finalizer exists.
400398
fetched := &chainsv1alpha2.ChainInstance{}
401399
Expect(k8sClient.Get(ctx, nn, fetched)).To(Succeed())
402-
Expect(controllerutil.ContainsFinalizer(fetched, chainsv1alpha2.FinalizerName)).To(BeTrue())
400+
Expect(controllerutil.ContainsFinalizer(fetched, chainsv1alpha2.FinalizerName)).To(BeFalse())
403401

404-
// Delete the resource.
405402
Expect(k8sClient.Delete(ctx, fetched)).To(Succeed())
406403

407-
// Reconcile handles deletion: scales down and removes finalizer.
408-
// May take multiple reconciles for the full flow.
409-
for i := 0; i < 5; i++ {
410-
_, _ = reconcileOnce(ctx, nn)
411-
}
412-
413-
// Object should be gone (or have no finalizer, allowing GC).
414404
err := k8sClient.Get(ctx, nn, fetched)
415-
if err == nil {
416-
// If still exists, finalizer should be gone.
417-
Expect(controllerutil.ContainsFinalizer(fetched, chainsv1alpha2.FinalizerName)).To(BeFalse())
418-
} else {
419-
Expect(errors.IsNotFound(err)).To(BeTrue())
420-
}
405+
Expect(errors.IsNotFound(err)).To(BeTrue())
421406
})
422407
})
423408

0 commit comments

Comments
 (0)