From 8de78e5d584263a1844ca13725b951d5552c66f6 Mon Sep 17 00:00:00 2001 From: June Han Date: Sat, 8 Jun 2024 16:52:50 +0900 Subject: [PATCH 1/7] feat: add eager mode Signed-off-by: June Han --- pkg/scaling/executor/scale_jobs.go | 28 +++++++++----- pkg/scaling/executor/scale_jobs_test.go | 51 +++++++++++++++++++------ 2 files changed, 58 insertions(+), 21 deletions(-) diff --git a/pkg/scaling/executor/scale_jobs.go b/pkg/scaling/executor/scale_jobs.go index bb671afbdf1..9da2f683448 100644 --- a/pkg/scaling/executor/scale_jobs.go +++ b/pkg/scaling/executor/scale_jobs.go @@ -95,7 +95,7 @@ func (e *scaleExecutor) getScalingDecision(scaledJob *kedav1alpha1.ScaledJob, ru scaleTo = scaleToMinReplica effectiveMaxScale = scaleToMinReplica } else { - effectiveMaxScale = NewScalingStrategy(logger, scaledJob).GetEffectiveMaxScale(maxScale, runningJobCount-minReplicaCount, pendingJobCount, scaledJob.MaxReplicaCount()) + effectiveMaxScale, scaleTo = NewScalingStrategy(logger, scaledJob).GetEffectiveMaxScale(maxScale, runningJobCount-minReplicaCount, pendingJobCount, scaledJob.MaxReplicaCount(), scaleTo) } return effectiveMaxScale, scaleTo } @@ -391,6 +391,9 @@ func NewScalingStrategy(logger logr.Logger, scaledJob *kedav1alpha1.ScaledJob) S case "accurate": logger.V(1).Info("Selecting Scale Strategy", "specified", scaledJob.Spec.ScalingStrategy.Strategy, "selected", "accurate") return accurateScalingStrategy{} + case "eager": + logger.V(1).Info("Selecting Scale Strategy", "specified", scaledJob.Spec.ScalingStrategy.Strategy, "selected", "eager") + return eagerScalingStrategy{} default: logger.V(1).Info("Selecting Scale Strategy", "specified", scaledJob.Spec.ScalingStrategy.Strategy, "selected", "default") return defaultScalingStrategy{} @@ -399,14 +402,14 @@ func NewScalingStrategy(logger logr.Logger, scaledJob *kedav1alpha1.ScaledJob) S // ScalingStrategy is an interface for switching scaling algorithm type ScalingStrategy interface { - GetEffectiveMaxScale(maxScale, runningJobCount, pendingJobCount, maxReplicaCount int64) int64 + GetEffectiveMaxScale(maxScale, runningJobCount, pendingJobCount, maxReplicaCount, scaleTo int64) (int64, int64) } type defaultScalingStrategy struct { } -func (s defaultScalingStrategy) GetEffectiveMaxScale(maxScale, runningJobCount, _, _ int64) int64 { - return maxScale - runningJobCount +func (s defaultScalingStrategy) GetEffectiveMaxScale(maxScale, runningJobCount, _, _, scaleTo int64) (int64, int64) { + return maxScale - runningJobCount, scaleTo } type customScalingStrategy struct { @@ -414,18 +417,25 @@ type customScalingStrategy struct { CustomScalingRunningJobPercentage *float64 } -func (s customScalingStrategy) GetEffectiveMaxScale(maxScale, runningJobCount, _, maxReplicaCount int64) int64 { - return min(maxScale-int64(*s.CustomScalingQueueLengthDeduction)-int64(float64(runningJobCount)*(*s.CustomScalingRunningJobPercentage)), maxReplicaCount) +func (s customScalingStrategy) GetEffectiveMaxScale(maxScale, runningJobCount, _, maxReplicaCount, scaleTo int64) (int64, int64) { + return min(maxScale-int64(*s.CustomScalingQueueLengthDeduction)-int64(float64(runningJobCount)*(*s.CustomScalingRunningJobPercentage)), maxReplicaCount), scaleTo } type accurateScalingStrategy struct { } -func (s accurateScalingStrategy) GetEffectiveMaxScale(maxScale, runningJobCount, pendingJobCount, maxReplicaCount int64) int64 { +func (s accurateScalingStrategy) GetEffectiveMaxScale(maxScale, runningJobCount, pendingJobCount, maxReplicaCount, scaleTo int64) (int64, int64) { if (maxScale + runningJobCount) > maxReplicaCount { - return maxReplicaCount - runningJobCount + return maxReplicaCount - runningJobCount, scaleTo } - return maxScale - pendingJobCount + return maxScale - pendingJobCount, scaleTo +} + +type eagerScalingStrategy struct { +} + +func (s eagerScalingStrategy) GetEffectiveMaxScale(maxScale, runningJobCount, pendingJobCount, maxReplicaCount, scaleTo int64) (int64, int64) { + return min(maxReplicaCount-runningJobCount-pendingJobCount, maxScale), maxReplicaCount } func min(x, y int64) int64 { diff --git a/pkg/scaling/executor/scale_jobs_test.go b/pkg/scaling/executor/scale_jobs_test.go index 7542c1acb58..c3f625a016b 100644 --- a/pkg/scaling/executor/scale_jobs_test.go +++ b/pkg/scaling/executor/scale_jobs_test.go @@ -81,13 +81,17 @@ func TestNewNewScalingStrategy(t *testing.T) { assert.Equal(t, "executor.defaultScalingStrategy", fmt.Sprintf("%T", strategy)) } +func maxScaleValue(maxValue, _ int64) int64 { + return maxValue +} + func TestDefaultScalingStrategy(t *testing.T) { logger := logf.Log.WithName("ScaledJobTest") strategy := NewScalingStrategy(logger, getMockScaledJobWithDefaultStrategy("default")) // maxScale doesn't exceed MaxReplicaCount. You can ignore on this sceanrio // pendingJobCount isn't relevant on this scenario - assert.Equal(t, int64(1), strategy.GetEffectiveMaxScale(3, 2, 0, 5)) - assert.Equal(t, int64(2), strategy.GetEffectiveMaxScale(2, 0, 0, 5)) + assert.Equal(t, int64(1), maxScaleValue(strategy.GetEffectiveMaxScale(3, 2, 0, 5, 1))) + assert.Equal(t, int64(2), maxScaleValue(strategy.GetEffectiveMaxScale(2, 0, 0, 5, 1))) } func TestCustomScalingStrategy(t *testing.T) { @@ -97,13 +101,13 @@ func TestCustomScalingStrategy(t *testing.T) { strategy := NewScalingStrategy(logger, getMockScaledJobWithStrategy("custom", "custom", customScalingQueueLengthDeduction, customScalingRunningJobPercentage)) // maxScale doesn't exceed MaxReplicaCount. You can ignore on this sceanrio // pendingJobCount isn't relevant on this scenario - assert.Equal(t, int64(1), strategy.GetEffectiveMaxScale(3, 2, 0, 5)) - assert.Equal(t, int64(9), strategy.GetEffectiveMaxScale(10, 0, 0, 10)) + assert.Equal(t, int64(1), maxScaleValue(strategy.GetEffectiveMaxScale(3, 2, 0, 5, 1))) + assert.Equal(t, int64(9), maxScaleValue(strategy.GetEffectiveMaxScale(10, 0, 0, 10, 1))) strategy = NewScalingStrategy(logger, getMockScaledJobWithCustomStrategyWithNilParameter("custom", "custom")) // If you don't set the two parameters is the same behavior as DefaultStrategy - assert.Equal(t, int64(1), strategy.GetEffectiveMaxScale(3, 2, 0, 5)) - assert.Equal(t, int64(2), strategy.GetEffectiveMaxScale(2, 0, 0, 5)) + assert.Equal(t, int64(1), maxScaleValue(strategy.GetEffectiveMaxScale(3, 2, 0, 5, 1))) + assert.Equal(t, int64(2), maxScaleValue(strategy.GetEffectiveMaxScale(2, 0, 0, 5, 1))) // Empty String will be DefaultStrategy customScalingQueueLengthDeduction = int32(1) @@ -115,25 +119,48 @@ func TestCustomScalingStrategy(t *testing.T) { customScalingQueueLengthDeduction = int32(2) customScalingRunningJobPercentage = "0" strategy = NewScalingStrategy(logger, getMockScaledJobWithStrategy("custom", "custom", customScalingQueueLengthDeduction, customScalingRunningJobPercentage)) - assert.Equal(t, int64(1), strategy.GetEffectiveMaxScale(3, 2, 0, 5)) + assert.Equal(t, int64(1), maxScaleValue(strategy.GetEffectiveMaxScale(3, 2, 0, 5, 1))) // Exceed the MaxReplicaCount customScalingQueueLengthDeduction = int32(-2) customScalingRunningJobPercentage = "0" strategy = NewScalingStrategy(logger, getMockScaledJobWithStrategy("custom", "custom", customScalingQueueLengthDeduction, customScalingRunningJobPercentage)) - assert.Equal(t, int64(4), strategy.GetEffectiveMaxScale(3, 2, 0, 4)) + assert.Equal(t, int64(4), maxScaleValue(strategy.GetEffectiveMaxScale(3, 2, 0, 4, 1))) } func TestAccurateScalingStrategy(t *testing.T) { logger := logf.Log.WithName("ScaledJobTest") strategy := NewScalingStrategy(logger, getMockScaledJobWithStrategy("accurate", "accurate", 0, "0")) // maxScale doesn't exceed MaxReplicaCount. You can ignore on this sceanrio - assert.Equal(t, int64(3), strategy.GetEffectiveMaxScale(3, 2, 0, 5)) - assert.Equal(t, int64(3), strategy.GetEffectiveMaxScale(5, 2, 0, 5)) + assert.Equal(t, int64(3), maxScaleValue(strategy.GetEffectiveMaxScale(3, 2, 0, 5, 1))) + assert.Equal(t, int64(3), maxScaleValue(strategy.GetEffectiveMaxScale(5, 2, 0, 5, 1))) // Test with 2 pending jobs - assert.Equal(t, int64(1), strategy.GetEffectiveMaxScale(3, 4, 2, 10)) - assert.Equal(t, int64(1), strategy.GetEffectiveMaxScale(5, 4, 2, 5)) + assert.Equal(t, int64(1), maxScaleValue(strategy.GetEffectiveMaxScale(3, 4, 2, 10, 1))) + assert.Equal(t, int64(1), maxScaleValue(strategy.GetEffectiveMaxScale(5, 4, 2, 5, 1))) +} + +func TestEagerScalingStrategy(t *testing.T) { + logger := logf.Log.WithName("ScaledJobTest") + strategy := NewScalingStrategy(logger, getMockScaledJobWithStrategy("eager", "eager", 0, "0")) + + maxScale, scaleTo := strategy.GetEffectiveMaxScale(4, 3, 0, 10, 1) + assert.Equal(t, int64(4), maxScale) + assert.Equal(t, int64(10), scaleTo) + maxScale, scaleTo = strategy.GetEffectiveMaxScale(4, 0, 3, 10, 1) + assert.Equal(t, int64(4), maxScale) + assert.Equal(t, int64(10), scaleTo) + + maxScale, scaleTo = strategy.GetEffectiveMaxScale(4, 7, 0, 10, 1) + assert.Equal(t, int64(3), maxScale) + assert.Equal(t, int64(10), scaleTo) + maxScale, scaleTo = strategy.GetEffectiveMaxScale(4, 1, 6, 10, 1) + assert.Equal(t, int64(3), maxScale) + assert.Equal(t, int64(10), scaleTo) + + maxScale, scaleTo = strategy.GetEffectiveMaxScale(15, 0, 0, 10, 1) + assert.Equal(t, int64(10), maxScale) + assert.Equal(t, int64(10), scaleTo) } func TestCleanUpMixedCaseWithSortByTime(t *testing.T) { From f0614e3015aed58d3872ab9285491419a4a857c3 Mon Sep 17 00:00:00 2001 From: June Han Date: Sat, 8 Jun 2024 17:02:49 +0900 Subject: [PATCH 2/7] chore: add changelog Signed-off-by: June Han --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7d67ee9127..d9b203748b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,7 @@ Here is an overview of all new **experimental** features: ### Improvements - **GCP Scalers**: Added custom time horizon in GCP scalers ([#5778](https://github.com/kedacore/keda/issues/5778)) +- **General**: Added `eagerScalingStrategy` for `ScaledJob` ([#5114](https://github.com/kedacore/keda/issues/5114)) - **GitHub Scaler**: Fixed pagination, fetching repository list ([#5738](https://github.com/kedacore/keda/issues/5738)) - **Kafka**: Fix logic to scale to zero on invalid offset even with earliest offsetResetPolicy ([#5689](https://github.com/kedacore/keda/issues/5689)) From bb7c5020b0ad55c9aff2766e6b42a149fd2fc472 Mon Sep 17 00:00:00 2001 From: June Han Date: Sat, 8 Jun 2024 18:12:41 +0900 Subject: [PATCH 3/7] fix: rename unused parameter Signed-off-by: June Han --- pkg/scaling/executor/scale_jobs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/scaling/executor/scale_jobs.go b/pkg/scaling/executor/scale_jobs.go index 9da2f683448..3c1b629ce18 100644 --- a/pkg/scaling/executor/scale_jobs.go +++ b/pkg/scaling/executor/scale_jobs.go @@ -434,7 +434,7 @@ func (s accurateScalingStrategy) GetEffectiveMaxScale(maxScale, runningJobCount, type eagerScalingStrategy struct { } -func (s eagerScalingStrategy) GetEffectiveMaxScale(maxScale, runningJobCount, pendingJobCount, maxReplicaCount, scaleTo int64) (int64, int64) { +func (s eagerScalingStrategy) GetEffectiveMaxScale(maxScale, runningJobCount, pendingJobCount, maxReplicaCount, _ int64) (int64, int64) { return min(maxReplicaCount-runningJobCount-pendingJobCount, maxScale), maxReplicaCount } From 6113790609100b5acfd4a5151a51e8bf21498fcf Mon Sep 17 00:00:00 2001 From: June Han Date: Sat, 8 Jun 2024 18:27:46 +0900 Subject: [PATCH 4/7] chore: correct changelog order Signed-off-by: June Han --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9b203748b0..245298e8156 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,8 +70,8 @@ Here is an overview of all new **experimental** features: ### Improvements -- **GCP Scalers**: Added custom time horizon in GCP scalers ([#5778](https://github.com/kedacore/keda/issues/5778)) - **General**: Added `eagerScalingStrategy` for `ScaledJob` ([#5114](https://github.com/kedacore/keda/issues/5114)) +- **GCP Scalers**: Added custom time horizon in GCP scalers ([#5778](https://github.com/kedacore/keda/issues/5778)) - **GitHub Scaler**: Fixed pagination, fetching repository list ([#5738](https://github.com/kedacore/keda/issues/5738)) - **Kafka**: Fix logic to scale to zero on invalid offset even with earliest offsetResetPolicy ([#5689](https://github.com/kedacore/keda/issues/5689)) From e911f6d9e2bd7464a3db156eac933b5037e85cae Mon Sep 17 00:00:00 2001 From: June Han Date: Thu, 13 Jun 2024 18:16:10 +0900 Subject: [PATCH 5/7] add e2e test for eagerScalingStrategy Signed-off-by: June Han --- .../eager_scaling_strategy_test.go | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 tests/internals/scaling_strategies/eager_scaling_strategy_test.go diff --git a/tests/internals/scaling_strategies/eager_scaling_strategy_test.go b/tests/internals/scaling_strategies/eager_scaling_strategy_test.go new file mode 100644 index 00000000000..fba4512a70b --- /dev/null +++ b/tests/internals/scaling_strategies/eager_scaling_strategy_test.go @@ -0,0 +1,116 @@ +//go:build e2e +// +build e2e + +package eager_scaling_strategy_test + +import ( + "fmt" + "testing" + + "github.com/joho/godotenv" + "github.com/stretchr/testify/assert" + "k8s.io/client-go/kubernetes" + + . "github.com/kedacore/keda/v2/tests/helper" // For helper methods + . "github.com/kedacore/keda/v2/tests/scalers/rabbitmq" +) + +var _ = godotenv.Load("../../.env") // For loading env variables from .env + +const ( + testName = "eager-scaling-strategy-test" +) + +var ( + testNamespace = fmt.Sprintf("%s-ns", testName) + rmqNamespace = fmt.Sprintf("%s-rmq", testName) + scaledJobName = fmt.Sprintf("%s-sj", testName) + queueName = "hello" + user = fmt.Sprintf("%s-user", testName) + password = fmt.Sprintf("%s-password", testName) + vhost = "/" + connectionString = fmt.Sprintf("amqp://%s:%s@rabbitmq.%s.svc.cluster.local", user, password, rmqNamespace) +) + +// YAML templates for your Kubernetes resources +const ( + scaledJobTemplate = ` +apiVersion: keda.sh/v1alpha1 +kind: ScaledJob +metadata: + name: {{.ScaledJobName}} + namespace: {{.TestNamespace}} + labels: + app: {{.ScaledJobName}} +spec: + jobTargetRef: + template: + spec: + containers: + - name: sleeper + image: busybox + command: + - sleep + - "300" + imagePullPolicy: IfNotPresent + restartPolicy: Never + backoffLimit: 1 + pollingInterval: 5 + maxReplicaCount: 10 + scalingStrategy: + strategy: "eager" + triggers: + - type: rabbitmq + metadata: + queueName: {{.QueueName}} + hostFromEnv: RabbitApiHost + mode: QueueLength + value: '1' +` +) + +type templateData struct { + ScaledJobName string + TestNamespace string + QueueName string +} + +func TestScalingStrategy(t *testing.T) { + kc := GetKubernetesClient(t) + data, templates := getTemplateData() + t.Cleanup(func() { + DeleteKubernetesResources(t, testNamespace, data, templates) + RMQUninstall(t, rmqNamespace, user, password, vhost, WithoutOAuth()) + }) + + RMQInstall(t, kc, rmqNamespace, user, password, vhost, WithoutOAuth()) + CreateKubernetesResources(t, kc, testNamespace, data, templates) + + testEagerScaling(t, kc) +} + +func getTemplateData() (templateData, []Template) { + return templateData{ + // Populate fields required in YAML templates + ScaledJobName: scaledJobName, + TestNamespace: testNamespace, + QueueName: queueName, + }, []Template{ + {Name: "scaledJobTemplate", Config: scaledJobTemplate}, + } +} + +func testEagerScaling(t *testing.T, kc *kubernetes.Clientset) { + iterationCount := 20 + RMQPublishMessages(t, rmqNamespace, connectionString, queueName, 4) + assert.True(t, WaitForScaledJobCount(t, kc, scaledJobName, testNamespace, 4, iterationCount, 1), + "job count should be %d after %d iterations", 4, iterationCount) + + RMQPublishMessages(t, rmqNamespace, connectionString, queueName, 4) + assert.True(t, WaitForScaledJobCount(t, kc, scaledJobName, testNamespace, 8, iterationCount, 1), + "job count should be %d after %d iterations", 8, iterationCount) + + RMQPublishMessages(t, rmqNamespace, connectionString, queueName, 4) + assert.True(t, WaitForScaledJobCount(t, kc, scaledJobName, testNamespace, 10, iterationCount, 1), + "job count should be %d after %d iterations", 10, iterationCount) +} From 754105ef83e5d860dca502e2effed24b70612f14 Mon Sep 17 00:00:00 2001 From: June Han Date: Fri, 28 Jun 2024 14:15:27 +0900 Subject: [PATCH 6/7] fix: correct the e2e test case with the missing RMQ connection configuration Signed-off-by: June Han --- .../eager_scaling_strategy_test.go | 46 +++++++++++++------ 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/tests/internals/scaling_strategies/eager_scaling_strategy_test.go b/tests/internals/scaling_strategies/eager_scaling_strategy_test.go index fba4512a70b..e05c84c30a0 100644 --- a/tests/internals/scaling_strategies/eager_scaling_strategy_test.go +++ b/tests/internals/scaling_strategies/eager_scaling_strategy_test.go @@ -4,6 +4,7 @@ package eager_scaling_strategy_test import ( + "encoding/base64" "fmt" "testing" @@ -22,19 +23,29 @@ const ( ) var ( - testNamespace = fmt.Sprintf("%s-ns", testName) - rmqNamespace = fmt.Sprintf("%s-rmq", testName) - scaledJobName = fmt.Sprintf("%s-sj", testName) - queueName = "hello" - user = fmt.Sprintf("%s-user", testName) - password = fmt.Sprintf("%s-password", testName) - vhost = "/" - connectionString = fmt.Sprintf("amqp://%s:%s@rabbitmq.%s.svc.cluster.local", user, password, rmqNamespace) + testNamespace = fmt.Sprintf("%s-ns", testName) + rmqNamespace = fmt.Sprintf("%s-rmq", testName) + scaledJobName = fmt.Sprintf("%s-sj", testName) + queueName = "hello" + user = fmt.Sprintf("%s-user", testName) + password = fmt.Sprintf("%s-password", testName) + vhost = "/" + connectionString = fmt.Sprintf("amqp://%s:%s@rabbitmq.%s.svc.cluster.local/", user, password, rmqNamespace) + httpConnectionString = fmt.Sprintf("http://%s:%s@rabbitmq.%s.svc.cluster.local/", user, password, rmqNamespace) + secretName = fmt.Sprintf("%s-secret", testName) ) // YAML templates for your Kubernetes resources const ( scaledJobTemplate = ` +apiVersion: v1 +kind: Secret +metadata: + name: {{.SecretName}} + namespace: {{.TestNamespace}} +data: + RabbitApiHost: {{.Base64Connection}} +--- apiVersion: keda.sh/v1alpha1 kind: ScaledJob metadata: @@ -53,6 +64,9 @@ spec: - sleep - "300" imagePullPolicy: IfNotPresent + envFrom: + - secretRef: + name: {{.SecretName}} restartPolicy: Never backoffLimit: 1 pollingInterval: 5 @@ -70,9 +84,11 @@ spec: ) type templateData struct { - ScaledJobName string - TestNamespace string - QueueName string + ScaledJobName string + TestNamespace string + QueueName string + SecretName string + Base64Connection string } func TestScalingStrategy(t *testing.T) { @@ -92,9 +108,11 @@ func TestScalingStrategy(t *testing.T) { func getTemplateData() (templateData, []Template) { return templateData{ // Populate fields required in YAML templates - ScaledJobName: scaledJobName, - TestNamespace: testNamespace, - QueueName: queueName, + ScaledJobName: scaledJobName, + TestNamespace: testNamespace, + QueueName: queueName, + Base64Connection: base64.StdEncoding.EncodeToString([]byte(httpConnectionString)), + SecretName: secretName, }, []Template{ {Name: "scaledJobTemplate", Config: scaledJobTemplate}, } From 497848e09a48122da49ab3b15df5d15a51f17231 Mon Sep 17 00:00:00 2001 From: June Han Date: Fri, 5 Jul 2024 10:08:11 +0900 Subject: [PATCH 7/7] chore: fix changelog order Signed-off-by: June Han --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1734f5ad1a..57b225b08d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,8 +72,8 @@ Here is an overview of all new **experimental** features: ### Improvements -- **Cassandra Scaler**: Add TLS support for cassandra scaler ([#5802](https://github.com/kedacore/keda/issues/5802)) - **General**: Added `eagerScalingStrategy` for `ScaledJob` ([#5114](https://github.com/kedacore/keda/issues/5114)) +- **Cassandra Scaler**: Add TLS support for cassandra scaler ([#5802](https://github.com/kedacore/keda/issues/5802)) - **GCP Scalers**: Added custom time horizon in GCP scalers ([#5778](https://github.com/kedacore/keda/issues/5778)) - **GitHub Scaler**: Fixed pagination, fetching repository list ([#5738](https://github.com/kedacore/keda/issues/5738)) - **Kafka**: Fix logic to scale to zero on invalid offset even with earliest offsetResetPolicy ([#5689](https://github.com/kedacore/keda/issues/5689))