Skip to content

Commit

Permalink
chore: stop scale loop for pause Scaledbject kedacore#4253
Browse files Browse the repository at this point in the history
Signed-off-by: Tobo Atchou <[email protected]>
  • Loading branch information
tobotg committed Apr 23, 2023
1 parent 8961618 commit 4b2bb99
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ To learn more about active deprecations, we recommend checking [GitHub Discussio

- **General**: Drop a transitive dependency on bou.ke/monkey ([#4364](https://github.com/kedacore/keda/issues/4364))
- **General**: Fix odd number of arguments passed as key-value pairs for logging ([#4368](https://github.com/kedacore/keda/issues/4368))
- **General**: Stop logging errors for paused ScaledObject (with "autoscaling.keda.sh/paused-replicas" annotation) by skipping reconciliation loop for the object (stop the scale loop and delete the HPA) ([#4253](https://github.com/kedacore/keda/pull/4253))

## v2.10.0

Expand Down
12 changes: 10 additions & 2 deletions controllers/keda/hpa.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,13 +182,21 @@ func (r *ScaledObjectReconciler) updateHPAIfNeeded(ctx context.Context, logger l

// deleteAndCreateHpa delete old HPA and create new one
func (r *ScaledObjectReconciler) renameHPA(ctx context.Context, logger logr.Logger, scaledObject *kedav1alpha1.ScaledObject, foundHpa *autoscalingv2.HorizontalPodAutoscaler, gvkr *kedav1alpha1.GroupVersionKindResource) error {
logger.Info("Deleting old HPA", "HPA.Namespace", scaledObject.Namespace, "HPA.Name", foundHpa.Name)
if err := r.deleteHPA(ctx, logger, scaledObject, foundHpa); err != nil {
return err
}
return r.createAndDeployNewHPA(ctx, logger, scaledObject, gvkr)
}

// deleteHpa delete existing HPA
func (r *ScaledObjectReconciler) deleteHPA(ctx context.Context, logger logr.Logger, scaledObject *kedav1alpha1.ScaledObject, foundHpa *autoscalingv2.HorizontalPodAutoscaler) error {
logger.Info("Deleting existing HPA", "HPA.Namespace", scaledObject.Namespace, "HPA.Name", foundHpa.Name)
if err := r.Client.Delete(ctx, foundHpa); err != nil {
logger.Error(err, "Failed to delete old HPA", "HPA.Namespace", foundHpa.Namespace, "HPA.Name", foundHpa.Name)
return err
}

return r.createAndDeployNewHPA(ctx, logger, scaledObject, gvkr)
return nil
}

// getScaledObjectMetricSpecs returns MetricSpec for HPA, generater from Triggers defitinion in ScaledObject
Expand Down
39 changes: 39 additions & 0 deletions controllers/keda/scaledobject_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,20 @@ func (r *ScaledObjectReconciler) Reconcile(ctx context.Context, req ctrl.Request

// reconcileScaledObject implements reconciler logic for ScaledObject
func (r *ScaledObjectReconciler) reconcileScaledObject(ctx context.Context, logger logr.Logger, scaledObject *kedav1alpha1.ScaledObject) (string, error) {
// Check the presence of "autoscaling.keda.sh/paused-replicas" annotation on the scaledObject (since the presence of this annotation will pause
// autoscaling no matter what number of replicas is provided), and if so, stop the scale loop and delete the HPA on the scaled object.
_, paused := scaledObject.GetAnnotations()[kedacontrollerutil.PausedReplicasAnnotation]
if paused {
logger.Info("ScaledObject is paused, so skipping the request.")
if err := r.stopScaleLoop(ctx, logger, scaledObject); err != nil {
return "Failed to stop the scale loop for paused ScaledObject", err
}
if deleted, err := r.ensureHPAForScaledObjectIsDeleted(ctx, logger, scaledObject); !deleted {
return "Failed to delete HPA for paused ScaledObject", err
}
return kedav1alpha1.ScaledObjectConditionReadySuccessMessage, nil
}

// Check scale target Name is specified
if scaledObject.Spec.ScaleTargetRef.Name == "" {
err := fmt.Errorf("ScaledObject.spec.scaleTargetRef.name is missing")
Expand Down Expand Up @@ -437,6 +451,31 @@ func (r *ScaledObjectReconciler) ensureHPAForScaledObjectExists(ctx context.Cont
return false, nil
}

// ensureHPAForScaledObjectIsDeleted ensures that in cluster any HPA for specified ScaledObject is deleted, returns true if no HPA exists
func (r *ScaledObjectReconciler) ensureHPAForScaledObjectIsDeleted(ctx context.Context, logger logr.Logger, scaledObject *kedav1alpha1.ScaledObject) (bool, error) {
var hpaName string
if scaledObject.Status.HpaName != "" {
hpaName = scaledObject.Status.HpaName
} else {
hpaName = getHPAName(scaledObject)
}
foundHpa := &autoscalingv2.HorizontalPodAutoscaler{}
// Check if HPA for this ScaledObject already exists
err := r.Client.Get(ctx, types.NamespacedName{Name: hpaName, Namespace: scaledObject.Namespace}, foundHpa)
if err != nil && errors.IsNotFound(err) {
return true, nil
} else if err != nil {
logger.Error(err, "Failed to get HPA from cluster")
return false, err
}

if err := r.deleteHPA(ctx, logger, scaledObject, foundHpa); err != nil {
logger.Error(err, "Failed to delete HPA from cluster")
return false, err
}
return true, nil
}

func isHpaRenamed(scaledObject *kedav1alpha1.ScaledObject, foundHpa *autoscalingv2.HorizontalPodAutoscaler) bool {
// if HPA name defined in SO -> check if equals to the found HPA
if scaledObject.Spec.Advanced != nil && scaledObject.Spec.Advanced.HorizontalPodAutoscalerConfig != nil && scaledObject.Spec.Advanced.HorizontalPodAutoscalerConfig.Name != "" {
Expand Down
96 changes: 96 additions & 0 deletions controllers/keda/scaledobject_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ import (
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
clientcache "k8s.io/client-go/tools/cache"
"sigs.k8s.io/controller-runtime/pkg/log/zap"

kedav1alpha1 "github.com/kedacore/keda/v2/apis/keda/v1alpha1"
kedacontrollerutil "github.com/kedacore/keda/v2/controllers/keda/util"
"github.com/kedacore/keda/v2/pkg/mock/mock_client"
"github.com/kedacore/keda/v2/pkg/mock/mock_scaling"
"github.com/kedacore/keda/v2/pkg/scalers"
Expand Down Expand Up @@ -841,6 +843,100 @@ var _ = Describe("ScaledObjectController", func() {
return len(hpa.Spec.Metrics)
}, 5*time.Second).Should(Equal(2))
})

// Fix issue 4253
It("stops scale loop and delete existing hpa when scaledobject is updated to have pause annotation", func() {
// Create the scaling target.
deploymentName := "to-be-paused-name"
soName := "so-" + deploymentName
err := k8sClient.Create(context.Background(), generateDeployment(deploymentName))
Expect(err).ToNot(HaveOccurred())

// Create the ScaledObject without specifying name.
so := &kedav1alpha1.ScaledObject{
ObjectMeta: metav1.ObjectMeta{Name: soName, Namespace: "default"},
Spec: kedav1alpha1.ScaledObjectSpec{
ScaleTargetRef: &kedav1alpha1.ScaleTarget{
Name: deploymentName,
},
Advanced: &kedav1alpha1.AdvancedConfig{
HorizontalPodAutoscalerConfig: &kedav1alpha1.HorizontalPodAutoscalerConfig{},
},
Triggers: []kedav1alpha1.ScaleTriggers{
{
Type: "cron",
Metadata: map[string]string{
"timezone": "UTC",
"start": "0 * * * *",
"end": "1 * * * *",
"desiredReplicas": "1",
},
},
},
},
}
err = k8sClient.Create(context.Background(), so)
Expect(err).ToNot(HaveOccurred())

// Get and confirm the HPA.
hpa := &autoscalingv2.HorizontalPodAutoscaler{}
Eventually(func() error {
return k8sClient.Get(context.Background(), types.NamespacedName{Name: fmt.Sprintf("keda-hpa-%s", soName), Namespace: "default"}, hpa)
}).ShouldNot(HaveOccurred())
Expect(hpa.Name).To(Equal(fmt.Sprintf("keda-hpa-%s", soName)))

// Pause scaledObject (add annotation)
Eventually(func() error {
err = k8sClient.Get(context.Background(), types.NamespacedName{Name: soName, Namespace: "default"}, so)
Expect(err).ToNot(HaveOccurred())
so.Annotations[kedacontrollerutil.PausedReplicasAnnotation] = "0"
return k8sClient.Update(context.Background(), so)
}).ShouldNot(HaveOccurred())

// wait so's ready condition Ready
Eventually(func() metav1.ConditionStatus {
err := k8sClient.Get(context.Background(), types.NamespacedName{Name: soName, Namespace: "default"}, so)
if err != nil {
return metav1.ConditionUnknown
}
return so.Status.Conditions.GetReadyCondition().Status
}).Should(Equal(metav1.ConditionTrue))

// And validate that old hpa is deleted.
err = k8sClient.Get(context.Background(), types.NamespacedName{Name: fmt.Sprintf("keda-hpa-%s", soName), Namespace: "default"}, hpa)
Expect(err).Should(HaveOccurred())
Expect(errors.IsNotFound(err)).To(Equal(true))

//
key, err := clientcache.MetaNamespaceKeyFunc(so)
Expect(err).Should(HaveOccurred())
Expect(key).To(Equal(nil))

// Resume scaledObject (remove annotation)
Eventually(func() error {
err = k8sClient.Get(context.Background(), types.NamespacedName{Name: soName, Namespace: "default"}, so)
Expect(err).ToNot(HaveOccurred())
delete(so.Annotations, kedacontrollerutil.PausedReplicasAnnotation)
return k8sClient.Update(context.Background(), so)
}).ShouldNot(HaveOccurred())

// wait so's ready condition Ready
Eventually(func() metav1.ConditionStatus {
err := k8sClient.Get(context.Background(), types.NamespacedName{Name: soName, Namespace: "default"}, so)
if err != nil {
return metav1.ConditionUnknown
}
return so.Status.Conditions.GetReadyCondition().Status
}).Should(Equal(metav1.ConditionTrue))

// And validate that hpa is created.
err = k8sClient.Get(context.Background(), types.NamespacedName{Name: fmt.Sprintf("keda-hpa-%s", soName), Namespace: "default"}, hpa)
Expect(err).ShouldNot(HaveOccurred())

//
_, err = clientcache.MetaNamespaceKeyFunc(so)
Expect(err).ShouldNot(HaveOccurred())
})
})

func generateDeployment(name string) *appsv1.Deployment {
Expand Down

0 comments on commit 4b2bb99

Please sign in to comment.