From 7ccebe3acb0262701b32097b8e8cfc0ac11cfea7 Mon Sep 17 00:00:00 2001 From: OlegErshov Date: Wed, 19 Nov 2025 18:26:51 +0100 Subject: [PATCH 1/9] feat: introduced custom rate limiter based on options pattern On-behalf-of: SAP aleh.yarshou@sap.com --- .../lifecycle/controllerruntime/lifecycle.go | 15 ++++- .../controllerruntime/lifecycle_test.go | 23 +++++++ .../lifecycle/multicluster/lifecycle.go | 10 +++ controller/lifecycle/ratelimiter/config.go | 55 ++++++++++++++++ .../ratelimiter/static_exponential.go | 65 +++++++++++++++++++ .../ratelimiter/static_exponential_test.go | 33 ++++++++++ 6 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 controller/lifecycle/ratelimiter/config.go create mode 100644 controller/lifecycle/ratelimiter/static_exponential.go create mode 100644 controller/lifecycle/ratelimiter/static_exponential_test.go diff --git a/controller/lifecycle/controllerruntime/lifecycle.go b/controller/lifecycle/controllerruntime/lifecycle.go index 22261a8..96ea02a 100644 --- a/controller/lifecycle/controllerruntime/lifecycle.go +++ b/controller/lifecycle/controllerruntime/lifecycle.go @@ -15,6 +15,7 @@ import ( "github.com/platform-mesh/golang-commons/controller/lifecycle" "github.com/platform-mesh/golang-commons/controller/lifecycle/api" "github.com/platform-mesh/golang-commons/controller/lifecycle/conditions" + "github.com/platform-mesh/golang-commons/controller/lifecycle/ratelimiter" "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" "github.com/platform-mesh/golang-commons/controller/lifecycle/spread" "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" @@ -29,6 +30,7 @@ type LifecycleManager struct { spreader *spread.Spreader conditionsManager *conditions.ConditionManager prepareContextFunc api.PrepareContextFunc + rateLimiterConfig ratelimiter.Config } func NewLifecycleManager(subroutines []subroutine.Subroutine, operatorName string, controllerName string, client client.Client, log *logger.Logger) *LifecycleManager { @@ -83,10 +85,15 @@ func (l *LifecycleManager) SetupWithManagerBuilder(mgr ctrl.Manager, maxReconcil } eventPredicates = append([]predicate.Predicate{filter.DebugResourcesBehaviourPredicate(debugLabelValue)}, eventPredicates...) + opts := controller.Options{ + MaxConcurrentReconciles: maxReconciles, + RateLimiter: ratelimiter.NewStaticThenExponentialRateLimiter[reconcile.Request](l.rateLimiterConfig), + } + return ctrl.NewControllerManagedBy(mgr). Named(reconcilerName). For(instance). - WithOptions(controller.Options{MaxConcurrentReconciles: maxReconciles}). + WithOptions(opts). WithEventFilter(predicate.And(eventPredicates...)), nil } func (l *LifecycleManager) SetupWithManager(mgr ctrl.Manager, maxReconciles int, reconcilerName string, instance runtimeobject.RuntimeObject, debugLabelValue string, r reconcile.Reconciler, log *logger.Logger, eventPredicates ...predicate.Predicate) error { @@ -123,3 +130,9 @@ func (l *LifecycleManager) WithConditionManagement() *LifecycleManager { l.conditionsManager = conditions.NewConditionManager() return l } + +func (l *LifecycleManager) WithRateLimiter(opts ...ratelimiter.Option) *LifecycleManager { + cfg := ratelimiter.NewConfig(opts...) + l.rateLimiterConfig = cfg + return l +} diff --git a/controller/lifecycle/controllerruntime/lifecycle_test.go b/controller/lifecycle/controllerruntime/lifecycle_test.go index a974b5b..3728ab8 100644 --- a/controller/lifecycle/controllerruntime/lifecycle_test.go +++ b/controller/lifecycle/controllerruntime/lifecycle_test.go @@ -4,6 +4,7 @@ import ( "context" goerrors "errors" "testing" + "time" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -13,6 +14,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" + "github.com/platform-mesh/golang-commons/controller/lifecycle/ratelimiter" "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" pmtesting "github.com/platform-mesh/golang-commons/controller/testSupport" @@ -152,6 +154,27 @@ func TestLifecycle(t *testing.T) { assert.True(t, true, l.ConditionsManager() != nil) }) + t.Run("WithRateLimiter", func(t *testing.T) { + fakeClient := pmtesting.CreateFakeClient(t, &pmtesting.TestApiObject{}) + _, log := createLifecycleManager([]subroutine.Subroutine{}, fakeClient) + + l := NewLifecycleManager([]subroutine.Subroutine{}, "test-operator", "test-controller", fakeClient, log.Logger) + expectedCfg := ratelimiter.Config{ + StaticRequeueDelay: 5 * time.Second, + StaticWindow: 10 * time.Second, + ExponentialInitialBackoff: 5 * time.Second, + ExponentialMaxBackoff: time.Minute, + } + l.WithRateLimiter( + ratelimiter.WithRequeueDelay(expectedCfg.StaticRequeueDelay), + ratelimiter.WithStaticWindow(expectedCfg.StaticWindow), + ratelimiter.WithExponentialInitialBackoff(expectedCfg.ExponentialInitialBackoff), + ratelimiter.WithExponentialMaxBackoff(expectedCfg.ExponentialMaxBackoff), + ) + + assert.Equal(t, expectedCfg, l.rateLimiterConfig) + }) + } type testReconciler struct { diff --git a/controller/lifecycle/multicluster/lifecycle.go b/controller/lifecycle/multicluster/lifecycle.go index 2b31445..13a3378 100644 --- a/controller/lifecycle/multicluster/lifecycle.go +++ b/controller/lifecycle/multicluster/lifecycle.go @@ -17,6 +17,7 @@ import ( "github.com/platform-mesh/golang-commons/controller/lifecycle" "github.com/platform-mesh/golang-commons/controller/lifecycle/api" "github.com/platform-mesh/golang-commons/controller/lifecycle/conditions" + "github.com/platform-mesh/golang-commons/controller/lifecycle/ratelimiter" "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" "github.com/platform-mesh/golang-commons/controller/lifecycle/spread" "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" @@ -35,6 +36,7 @@ type LifecycleManager struct { spreader *spread.Spreader conditionsManager *conditions.ConditionManager prepareContextFunc api.PrepareContextFunc + rateLimiterConfig ratelimiter.Config } func NewLifecycleManager(subroutines []subroutine.Subroutine, operatorName string, controllerName string, mgr ClusterGetter, log *logger.Logger) *LifecycleManager { @@ -95,7 +97,9 @@ func (l *LifecycleManager) SetupWithManagerBuilder(mgr mcmanager.Manager, maxRec eventPredicates = append([]predicate.Predicate{filter.DebugResourcesBehaviourPredicate(debugLabelValue)}, eventPredicates...) opts := controller.TypedOptions[mcreconcile.Request]{ MaxConcurrentReconciles: maxReconciles, + RateLimiter: ratelimiter.NewStaticThenExponentialRateLimiter[mcreconcile.Request](l.rateLimiterConfig), } + return mcbuilder.ControllerManagedBy(mgr). Named(reconcilerName). For(instance). @@ -136,3 +140,9 @@ func (l *LifecycleManager) WithConditionManagement() api.Lifecycle { l.conditionsManager = conditions.NewConditionManager() return l } + +func (l *LifecycleManager) WithRateLimiter(opts ...ratelimiter.Option) *LifecycleManager { + cfg := ratelimiter.NewConfig(opts...) + l.rateLimiterConfig = cfg + return l +} diff --git a/controller/lifecycle/ratelimiter/config.go b/controller/lifecycle/ratelimiter/config.go new file mode 100644 index 0000000..7d720f2 --- /dev/null +++ b/controller/lifecycle/ratelimiter/config.go @@ -0,0 +1,55 @@ +package ratelimiter + +import ( + "time" +) + +type Config struct { + StaticRequeueDelay time.Duration + StaticWindow time.Duration + ExponentialInitialBackoff time.Duration + ExponentialMaxBackoff time.Duration +} + +var defaultConfig = Config{ + StaticRequeueDelay: 5 * time.Second, + StaticWindow: 1 * time.Minute, + ExponentialInitialBackoff: 5 * time.Second, + ExponentialMaxBackoff: 2 * time.Minute, +} + +type Option func(*Config) + +func WithStaticWindow(d time.Duration) Option { + return func(c *Config) { + c.StaticWindow = d + } +} + +func WithRequeueDelay(d time.Duration) Option { + return func(c *Config) { + c.StaticRequeueDelay = d + } +} + +func WithExponentialInitialBackoff(d time.Duration) Option { + return func(c *Config) { + c.ExponentialInitialBackoff = d + } +} + +func WithExponentialMaxBackoff(d time.Duration) Option { + return func(c *Config) { + c.ExponentialMaxBackoff = d + } +} + +func NewConfig(options ...Option) Config { + cfg := defaultConfig + + for _, option := range options { + option(&cfg) + } + + return cfg +} \ No newline at end of file diff --git a/controller/lifecycle/ratelimiter/static_exponential.go b/controller/lifecycle/ratelimiter/static_exponential.go new file mode 100644 index 0000000..8759fd8 --- /dev/null +++ b/controller/lifecycle/ratelimiter/static_exponential.go @@ -0,0 +1,65 @@ +package ratelimiter + +import ( + "sync" + "time" + + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/clock" +) + +type StaticThenExponentialRateLimiter[T comparable] struct { + failuresLock sync.Mutex + firstAttempt map[T]time.Time + + staticDelay time.Duration + staticWindow time.Duration + + exponential workqueue.TypedRateLimiter[T] + clock clock.Clock +} + +func NewStaticThenExponentialRateLimiter[T comparable](cfg Config) *StaticThenExponentialRateLimiter[T] { + return &StaticThenExponentialRateLimiter[T]{ + staticDelay: cfg.StaticRequeueDelay, + staticWindow: cfg.StaticWindow, + exponential: workqueue.NewTypedItemExponentialFailureRateLimiter[T]( + cfg.ExponentialInitialBackoff, + cfg.ExponentialMaxBackoff, + ), + firstAttempt: make(map[T]time.Time), + clock: clock.RealClock{}, + } +} + +func (r *StaticThenExponentialRateLimiter[T]) When(item T) time.Duration { + r.failuresLock.Lock() + defer r.failuresLock.Unlock() + + now := r.clock.Now() + + first, exists := r.firstAttempt[item] + if !exists { + first = now + r.firstAttempt[item] = first + } + + timeSinceFirst := now.Sub(first) + if timeSinceFirst <= r.staticWindow { + return r.staticDelay + } + + return r.exponential.When(item) +} + +func (r *StaticThenExponentialRateLimiter[T]) Forget(item T) { + r.failuresLock.Lock() + defer r.failuresLock.Unlock() + + delete(r.firstAttempt, item) + r.exponential.Forget(item) +} + +func (r *StaticThenExponentialRateLimiter[T]) NumRequeues(item T) int { + return r.exponential.NumRequeues(item) +} diff --git a/controller/lifecycle/ratelimiter/static_exponential_test.go b/controller/lifecycle/ratelimiter/static_exponential_test.go new file mode 100644 index 0000000..a82d377 --- /dev/null +++ b/controller/lifecycle/ratelimiter/static_exponential_test.go @@ -0,0 +1,33 @@ +package ratelimiter + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/types" + clocktesting "k8s.io/utils/clock/testing" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +func TestStaticThenExponentialRateLimiter_Forget(t *testing.T) { + cfg := Config{ + StaticRequeueDelay: 1 * time.Second, + StaticWindow: 5 * time.Second, + ExponentialInitialBackoff: 2 * time.Second, + ExponentialMaxBackoff: 1 * time.Minute, + } + limiter := NewStaticThenExponentialRateLimiter[reconcile.Request](cfg) + fakeClock := clocktesting.NewFakeClock(time.Now()) + limiter.clock = fakeClock + + item := reconcile.Request{NamespacedName: types.NamespacedName{Name: "name", Namespace: "namespace"}} + require.Equal(t, cfg.StaticRequeueDelay, limiter.When(item)) + fakeClock.Step(10 * time.Second) + _ = limiter.When(item) + + limiter.Forget(item) + require.Equal(t, 0, limiter.NumRequeues(item)) + delay := limiter.When(item) + require.Equal(t, cfg.StaticRequeueDelay, delay) +} From 8db6d981b6dc58c85ce6def50e9d1c3dbb63f62c Mon Sep 17 00:00:00 2001 From: OlegErshov Date: Thu, 20 Nov 2025 12:32:15 +0100 Subject: [PATCH 2/9] feat: added validation and rw mutex On-behalf-of: SAP aleh.yarshou@sap.com --- .../lifecycle/controllerruntime/lifecycle.go | 7 ++++++- .../lifecycle/multicluster/lifecycle.go | 7 ++++++- controller/lifecycle/ratelimiter/config.go | 19 +++++++++++++++++- .../ratelimiter/static_exponential.go | 20 +++++++++++-------- .../ratelimiter/static_exponential_test.go | 3 ++- 5 files changed, 44 insertions(+), 12 deletions(-) diff --git a/controller/lifecycle/controllerruntime/lifecycle.go b/controller/lifecycle/controllerruntime/lifecycle.go index 96ea02a..57b8989 100644 --- a/controller/lifecycle/controllerruntime/lifecycle.go +++ b/controller/lifecycle/controllerruntime/lifecycle.go @@ -84,10 +84,15 @@ func (l *LifecycleManager) SetupWithManagerBuilder(mgr ctrl.Manager, maxReconcil return nil, fmt.Errorf("cannot use conditions or spread reconciles in read-only mode") } + rateLimiter, err := ratelimiter.NewStaticThenExponentialRateLimiter[reconcile.Request](l.rateLimiterConfig) + if err != nil { + return nil, err + } + eventPredicates = append([]predicate.Predicate{filter.DebugResourcesBehaviourPredicate(debugLabelValue)}, eventPredicates...) opts := controller.Options{ MaxConcurrentReconciles: maxReconciles, - RateLimiter: ratelimiter.NewStaticThenExponentialRateLimiter[reconcile.Request](l.rateLimiterConfig), + RateLimiter: rateLimiter, } return ctrl.NewControllerManagedBy(mgr). diff --git a/controller/lifecycle/multicluster/lifecycle.go b/controller/lifecycle/multicluster/lifecycle.go index 13a3378..c988046 100644 --- a/controller/lifecycle/multicluster/lifecycle.go +++ b/controller/lifecycle/multicluster/lifecycle.go @@ -94,10 +94,15 @@ func (l *LifecycleManager) SetupWithManagerBuilder(mgr mcmanager.Manager, maxRec return nil, fmt.Errorf("cannot use conditions or spread reconciles in read-only mode") } + rateLimiter, err := ratelimiter.NewStaticThenExponentialRateLimiter[mcreconcile.Request](l.rateLimiterConfig) + if err != nil{ + return nil, err + } + eventPredicates = append([]predicate.Predicate{filter.DebugResourcesBehaviourPredicate(debugLabelValue)}, eventPredicates...) opts := controller.TypedOptions[mcreconcile.Request]{ MaxConcurrentReconciles: maxReconciles, - RateLimiter: ratelimiter.NewStaticThenExponentialRateLimiter[mcreconcile.Request](l.rateLimiterConfig), + RateLimiter: rateLimiter, } return mcbuilder.ControllerManagedBy(mgr). diff --git a/controller/lifecycle/ratelimiter/config.go b/controller/lifecycle/ratelimiter/config.go index 7d720f2..1fb8cb6 100644 --- a/controller/lifecycle/ratelimiter/config.go +++ b/controller/lifecycle/ratelimiter/config.go @@ -1,6 +1,7 @@ package ratelimiter import ( + "fmt" "time" ) @@ -18,6 +19,22 @@ var defaultConfig = Config{ ExponentialMaxBackoff: 2 * time.Minute, } +func (c Config) validate() error { + if c.StaticRequeueDelay < 0 { + return fmt.Errorf("the static requeue delay shouldn't be negative") + } + if c.ExponentialInitialBackoff < 0 { + return fmt.Errorf("the initial exponential backoff shouldn't be negative") + } + if c.StaticRequeueDelay > c.ExponentialInitialBackoff { + return fmt.Errorf("the initial exponential backoff should be equal to or greater than the static requeue delay") + } + if c.StaticWindow < c.StaticRequeueDelay { + return fmt.Errorf("the static window duration should be equal to or greater than the static requeue delay") + } + return nil +} + type Option func(*Config) func WithStaticWindow(d time.Duration) Option { @@ -52,4 +69,4 @@ func NewConfig(options ...Option) Config { } return cfg -} \ No newline at end of file +} diff --git a/controller/lifecycle/ratelimiter/static_exponential.go b/controller/lifecycle/ratelimiter/static_exponential.go index 8759fd8..116d2c6 100644 --- a/controller/lifecycle/ratelimiter/static_exponential.go +++ b/controller/lifecycle/ratelimiter/static_exponential.go @@ -9,7 +9,7 @@ import ( ) type StaticThenExponentialRateLimiter[T comparable] struct { - failuresLock sync.Mutex + failuresLock sync.RWMutex firstAttempt map[T]time.Time staticDelay time.Duration @@ -19,7 +19,10 @@ type StaticThenExponentialRateLimiter[T comparable] struct { clock clock.Clock } -func NewStaticThenExponentialRateLimiter[T comparable](cfg Config) *StaticThenExponentialRateLimiter[T] { +func NewStaticThenExponentialRateLimiter[T comparable](cfg Config) (*StaticThenExponentialRateLimiter[T], error) { + if err := cfg.validate(); err != nil { + return nil, err + } return &StaticThenExponentialRateLimiter[T]{ staticDelay: cfg.StaticRequeueDelay, staticWindow: cfg.StaticWindow, @@ -29,19 +32,20 @@ func NewStaticThenExponentialRateLimiter[T comparable](cfg Config) *StaticThenEx ), firstAttempt: make(map[T]time.Time), clock: clock.RealClock{}, - } + },nil } func (r *StaticThenExponentialRateLimiter[T]) When(item T) time.Duration { - r.failuresLock.Lock() - defer r.failuresLock.Unlock() - now := r.clock.Now() + r.failuresLock.RLock() first, exists := r.firstAttempt[item] + r.failuresLock.RUnlock() if !exists { - first = now - r.firstAttempt[item] = first + r.failuresLock.Lock() + r.firstAttempt[item] = now + r.failuresLock.Unlock() + return r.staticDelay } timeSinceFirst := now.Sub(first) diff --git a/controller/lifecycle/ratelimiter/static_exponential_test.go b/controller/lifecycle/ratelimiter/static_exponential_test.go index a82d377..24e9ff5 100644 --- a/controller/lifecycle/ratelimiter/static_exponential_test.go +++ b/controller/lifecycle/ratelimiter/static_exponential_test.go @@ -17,7 +17,8 @@ func TestStaticThenExponentialRateLimiter_Forget(t *testing.T) { ExponentialInitialBackoff: 2 * time.Second, ExponentialMaxBackoff: 1 * time.Minute, } - limiter := NewStaticThenExponentialRateLimiter[reconcile.Request](cfg) + limiter, err := NewStaticThenExponentialRateLimiter[reconcile.Request](cfg) + require.Nil(t, err) fakeClock := clocktesting.NewFakeClock(time.Now()) limiter.clock = fakeClock From 51004a80a8ebdbabcbaf864b30855f05bbe477c1 Mon Sep 17 00:00:00 2001 From: OlegErshov Date: Thu, 20 Nov 2025 12:57:27 +0100 Subject: [PATCH 3/9] chore: updated naming On-behalf-of: SAP aleh.yarshou@sap.com --- controller/lifecycle/ratelimiter/static_exponential.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/controller/lifecycle/ratelimiter/static_exponential.go b/controller/lifecycle/ratelimiter/static_exponential.go index 116d2c6..f83a6e9 100644 --- a/controller/lifecycle/ratelimiter/static_exponential.go +++ b/controller/lifecycle/ratelimiter/static_exponential.go @@ -10,7 +10,7 @@ import ( type StaticThenExponentialRateLimiter[T comparable] struct { failuresLock sync.RWMutex - firstAttempt map[T]time.Time + staticAttempts map[T]time.Time staticDelay time.Duration staticWindow time.Duration @@ -30,7 +30,7 @@ func NewStaticThenExponentialRateLimiter[T comparable](cfg Config) (*StaticThenE cfg.ExponentialInitialBackoff, cfg.ExponentialMaxBackoff, ), - firstAttempt: make(map[T]time.Time), + staticAttempts: make(map[T]time.Time), clock: clock.RealClock{}, },nil } @@ -39,11 +39,11 @@ func (r *StaticThenExponentialRateLimiter[T]) When(item T) time.Duration { now := r.clock.Now() r.failuresLock.RLock() - first, exists := r.firstAttempt[item] + first, exists := r.staticAttempts[item] r.failuresLock.RUnlock() if !exists { r.failuresLock.Lock() - r.firstAttempt[item] = now + r.staticAttempts[item] = now r.failuresLock.Unlock() return r.staticDelay } @@ -60,7 +60,7 @@ func (r *StaticThenExponentialRateLimiter[T]) Forget(item T) { r.failuresLock.Lock() defer r.failuresLock.Unlock() - delete(r.firstAttempt, item) + delete(r.staticAttempts, item) r.exponential.Forget(item) } From 6d2f988ba149f917e7ba579979b08e2ece7c2469 Mon Sep 17 00:00:00 2001 From: OlegErshov Date: Wed, 19 Nov 2025 18:26:51 +0100 Subject: [PATCH 4/9] feat: introduced custom rate limiter based on options pattern On-behalf-of: SAP aleh.yarshou@sap.com --- .../lifecycle/controllerruntime/lifecycle.go | 15 ++++- .../controllerruntime/lifecycle_test.go | 23 +++++++ .../lifecycle/multicluster/lifecycle.go | 10 +++ controller/lifecycle/ratelimiter/config.go | 55 ++++++++++++++++ .../ratelimiter/static_exponential.go | 65 +++++++++++++++++++ .../ratelimiter/static_exponential_test.go | 33 ++++++++++ 6 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 controller/lifecycle/ratelimiter/config.go create mode 100644 controller/lifecycle/ratelimiter/static_exponential.go create mode 100644 controller/lifecycle/ratelimiter/static_exponential_test.go diff --git a/controller/lifecycle/controllerruntime/lifecycle.go b/controller/lifecycle/controllerruntime/lifecycle.go index 22261a8..96ea02a 100644 --- a/controller/lifecycle/controllerruntime/lifecycle.go +++ b/controller/lifecycle/controllerruntime/lifecycle.go @@ -15,6 +15,7 @@ import ( "github.com/platform-mesh/golang-commons/controller/lifecycle" "github.com/platform-mesh/golang-commons/controller/lifecycle/api" "github.com/platform-mesh/golang-commons/controller/lifecycle/conditions" + "github.com/platform-mesh/golang-commons/controller/lifecycle/ratelimiter" "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" "github.com/platform-mesh/golang-commons/controller/lifecycle/spread" "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" @@ -29,6 +30,7 @@ type LifecycleManager struct { spreader *spread.Spreader conditionsManager *conditions.ConditionManager prepareContextFunc api.PrepareContextFunc + rateLimiterConfig ratelimiter.Config } func NewLifecycleManager(subroutines []subroutine.Subroutine, operatorName string, controllerName string, client client.Client, log *logger.Logger) *LifecycleManager { @@ -83,10 +85,15 @@ func (l *LifecycleManager) SetupWithManagerBuilder(mgr ctrl.Manager, maxReconcil } eventPredicates = append([]predicate.Predicate{filter.DebugResourcesBehaviourPredicate(debugLabelValue)}, eventPredicates...) + opts := controller.Options{ + MaxConcurrentReconciles: maxReconciles, + RateLimiter: ratelimiter.NewStaticThenExponentialRateLimiter[reconcile.Request](l.rateLimiterConfig), + } + return ctrl.NewControllerManagedBy(mgr). Named(reconcilerName). For(instance). - WithOptions(controller.Options{MaxConcurrentReconciles: maxReconciles}). + WithOptions(opts). WithEventFilter(predicate.And(eventPredicates...)), nil } func (l *LifecycleManager) SetupWithManager(mgr ctrl.Manager, maxReconciles int, reconcilerName string, instance runtimeobject.RuntimeObject, debugLabelValue string, r reconcile.Reconciler, log *logger.Logger, eventPredicates ...predicate.Predicate) error { @@ -123,3 +130,9 @@ func (l *LifecycleManager) WithConditionManagement() *LifecycleManager { l.conditionsManager = conditions.NewConditionManager() return l } + +func (l *LifecycleManager) WithRateLimiter(opts ...ratelimiter.Option) *LifecycleManager { + cfg := ratelimiter.NewConfig(opts...) + l.rateLimiterConfig = cfg + return l +} diff --git a/controller/lifecycle/controllerruntime/lifecycle_test.go b/controller/lifecycle/controllerruntime/lifecycle_test.go index a974b5b..3728ab8 100644 --- a/controller/lifecycle/controllerruntime/lifecycle_test.go +++ b/controller/lifecycle/controllerruntime/lifecycle_test.go @@ -4,6 +4,7 @@ import ( "context" goerrors "errors" "testing" + "time" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -13,6 +14,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" + "github.com/platform-mesh/golang-commons/controller/lifecycle/ratelimiter" "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" pmtesting "github.com/platform-mesh/golang-commons/controller/testSupport" @@ -152,6 +154,27 @@ func TestLifecycle(t *testing.T) { assert.True(t, true, l.ConditionsManager() != nil) }) + t.Run("WithRateLimiter", func(t *testing.T) { + fakeClient := pmtesting.CreateFakeClient(t, &pmtesting.TestApiObject{}) + _, log := createLifecycleManager([]subroutine.Subroutine{}, fakeClient) + + l := NewLifecycleManager([]subroutine.Subroutine{}, "test-operator", "test-controller", fakeClient, log.Logger) + expectedCfg := ratelimiter.Config{ + StaticRequeueDelay: 5 * time.Second, + StaticWindow: 10 * time.Second, + ExponentialInitialBackoff: 5 * time.Second, + ExponentialMaxBackoff: time.Minute, + } + l.WithRateLimiter( + ratelimiter.WithRequeueDelay(expectedCfg.StaticRequeueDelay), + ratelimiter.WithStaticWindow(expectedCfg.StaticWindow), + ratelimiter.WithExponentialInitialBackoff(expectedCfg.ExponentialInitialBackoff), + ratelimiter.WithExponentialMaxBackoff(expectedCfg.ExponentialMaxBackoff), + ) + + assert.Equal(t, expectedCfg, l.rateLimiterConfig) + }) + } type testReconciler struct { diff --git a/controller/lifecycle/multicluster/lifecycle.go b/controller/lifecycle/multicluster/lifecycle.go index 2b31445..13a3378 100644 --- a/controller/lifecycle/multicluster/lifecycle.go +++ b/controller/lifecycle/multicluster/lifecycle.go @@ -17,6 +17,7 @@ import ( "github.com/platform-mesh/golang-commons/controller/lifecycle" "github.com/platform-mesh/golang-commons/controller/lifecycle/api" "github.com/platform-mesh/golang-commons/controller/lifecycle/conditions" + "github.com/platform-mesh/golang-commons/controller/lifecycle/ratelimiter" "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" "github.com/platform-mesh/golang-commons/controller/lifecycle/spread" "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" @@ -35,6 +36,7 @@ type LifecycleManager struct { spreader *spread.Spreader conditionsManager *conditions.ConditionManager prepareContextFunc api.PrepareContextFunc + rateLimiterConfig ratelimiter.Config } func NewLifecycleManager(subroutines []subroutine.Subroutine, operatorName string, controllerName string, mgr ClusterGetter, log *logger.Logger) *LifecycleManager { @@ -95,7 +97,9 @@ func (l *LifecycleManager) SetupWithManagerBuilder(mgr mcmanager.Manager, maxRec eventPredicates = append([]predicate.Predicate{filter.DebugResourcesBehaviourPredicate(debugLabelValue)}, eventPredicates...) opts := controller.TypedOptions[mcreconcile.Request]{ MaxConcurrentReconciles: maxReconciles, + RateLimiter: ratelimiter.NewStaticThenExponentialRateLimiter[mcreconcile.Request](l.rateLimiterConfig), } + return mcbuilder.ControllerManagedBy(mgr). Named(reconcilerName). For(instance). @@ -136,3 +140,9 @@ func (l *LifecycleManager) WithConditionManagement() api.Lifecycle { l.conditionsManager = conditions.NewConditionManager() return l } + +func (l *LifecycleManager) WithRateLimiter(opts ...ratelimiter.Option) *LifecycleManager { + cfg := ratelimiter.NewConfig(opts...) + l.rateLimiterConfig = cfg + return l +} diff --git a/controller/lifecycle/ratelimiter/config.go b/controller/lifecycle/ratelimiter/config.go new file mode 100644 index 0000000..7d720f2 --- /dev/null +++ b/controller/lifecycle/ratelimiter/config.go @@ -0,0 +1,55 @@ +package ratelimiter + +import ( + "time" +) + +type Config struct { + StaticRequeueDelay time.Duration + StaticWindow time.Duration + ExponentialInitialBackoff time.Duration + ExponentialMaxBackoff time.Duration +} + +var defaultConfig = Config{ + StaticRequeueDelay: 5 * time.Second, + StaticWindow: 1 * time.Minute, + ExponentialInitialBackoff: 5 * time.Second, + ExponentialMaxBackoff: 2 * time.Minute, +} + +type Option func(*Config) + +func WithStaticWindow(d time.Duration) Option { + return func(c *Config) { + c.StaticWindow = d + } +} + +func WithRequeueDelay(d time.Duration) Option { + return func(c *Config) { + c.StaticRequeueDelay = d + } +} + +func WithExponentialInitialBackoff(d time.Duration) Option { + return func(c *Config) { + c.ExponentialInitialBackoff = d + } +} + +func WithExponentialMaxBackoff(d time.Duration) Option { + return func(c *Config) { + c.ExponentialMaxBackoff = d + } +} + +func NewConfig(options ...Option) Config { + cfg := defaultConfig + + for _, option := range options { + option(&cfg) + } + + return cfg +} \ No newline at end of file diff --git a/controller/lifecycle/ratelimiter/static_exponential.go b/controller/lifecycle/ratelimiter/static_exponential.go new file mode 100644 index 0000000..8759fd8 --- /dev/null +++ b/controller/lifecycle/ratelimiter/static_exponential.go @@ -0,0 +1,65 @@ +package ratelimiter + +import ( + "sync" + "time" + + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/clock" +) + +type StaticThenExponentialRateLimiter[T comparable] struct { + failuresLock sync.Mutex + firstAttempt map[T]time.Time + + staticDelay time.Duration + staticWindow time.Duration + + exponential workqueue.TypedRateLimiter[T] + clock clock.Clock +} + +func NewStaticThenExponentialRateLimiter[T comparable](cfg Config) *StaticThenExponentialRateLimiter[T] { + return &StaticThenExponentialRateLimiter[T]{ + staticDelay: cfg.StaticRequeueDelay, + staticWindow: cfg.StaticWindow, + exponential: workqueue.NewTypedItemExponentialFailureRateLimiter[T]( + cfg.ExponentialInitialBackoff, + cfg.ExponentialMaxBackoff, + ), + firstAttempt: make(map[T]time.Time), + clock: clock.RealClock{}, + } +} + +func (r *StaticThenExponentialRateLimiter[T]) When(item T) time.Duration { + r.failuresLock.Lock() + defer r.failuresLock.Unlock() + + now := r.clock.Now() + + first, exists := r.firstAttempt[item] + if !exists { + first = now + r.firstAttempt[item] = first + } + + timeSinceFirst := now.Sub(first) + if timeSinceFirst <= r.staticWindow { + return r.staticDelay + } + + return r.exponential.When(item) +} + +func (r *StaticThenExponentialRateLimiter[T]) Forget(item T) { + r.failuresLock.Lock() + defer r.failuresLock.Unlock() + + delete(r.firstAttempt, item) + r.exponential.Forget(item) +} + +func (r *StaticThenExponentialRateLimiter[T]) NumRequeues(item T) int { + return r.exponential.NumRequeues(item) +} diff --git a/controller/lifecycle/ratelimiter/static_exponential_test.go b/controller/lifecycle/ratelimiter/static_exponential_test.go new file mode 100644 index 0000000..a82d377 --- /dev/null +++ b/controller/lifecycle/ratelimiter/static_exponential_test.go @@ -0,0 +1,33 @@ +package ratelimiter + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/types" + clocktesting "k8s.io/utils/clock/testing" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +func TestStaticThenExponentialRateLimiter_Forget(t *testing.T) { + cfg := Config{ + StaticRequeueDelay: 1 * time.Second, + StaticWindow: 5 * time.Second, + ExponentialInitialBackoff: 2 * time.Second, + ExponentialMaxBackoff: 1 * time.Minute, + } + limiter := NewStaticThenExponentialRateLimiter[reconcile.Request](cfg) + fakeClock := clocktesting.NewFakeClock(time.Now()) + limiter.clock = fakeClock + + item := reconcile.Request{NamespacedName: types.NamespacedName{Name: "name", Namespace: "namespace"}} + require.Equal(t, cfg.StaticRequeueDelay, limiter.When(item)) + fakeClock.Step(10 * time.Second) + _ = limiter.When(item) + + limiter.Forget(item) + require.Equal(t, 0, limiter.NumRequeues(item)) + delay := limiter.When(item) + require.Equal(t, cfg.StaticRequeueDelay, delay) +} From dd782746e773ddd124c3f17ed51ef91b2b3c7db6 Mon Sep 17 00:00:00 2001 From: OlegErshov Date: Thu, 20 Nov 2025 12:32:15 +0100 Subject: [PATCH 5/9] feat: added validation and rw mutex On-behalf-of: SAP aleh.yarshou@sap.com --- .../lifecycle/controllerruntime/lifecycle.go | 7 ++++++- .../lifecycle/multicluster/lifecycle.go | 7 ++++++- controller/lifecycle/ratelimiter/config.go | 19 +++++++++++++++++- .../ratelimiter/static_exponential.go | 20 +++++++++++-------- .../ratelimiter/static_exponential_test.go | 3 ++- 5 files changed, 44 insertions(+), 12 deletions(-) diff --git a/controller/lifecycle/controllerruntime/lifecycle.go b/controller/lifecycle/controllerruntime/lifecycle.go index 96ea02a..57b8989 100644 --- a/controller/lifecycle/controllerruntime/lifecycle.go +++ b/controller/lifecycle/controllerruntime/lifecycle.go @@ -84,10 +84,15 @@ func (l *LifecycleManager) SetupWithManagerBuilder(mgr ctrl.Manager, maxReconcil return nil, fmt.Errorf("cannot use conditions or spread reconciles in read-only mode") } + rateLimiter, err := ratelimiter.NewStaticThenExponentialRateLimiter[reconcile.Request](l.rateLimiterConfig) + if err != nil { + return nil, err + } + eventPredicates = append([]predicate.Predicate{filter.DebugResourcesBehaviourPredicate(debugLabelValue)}, eventPredicates...) opts := controller.Options{ MaxConcurrentReconciles: maxReconciles, - RateLimiter: ratelimiter.NewStaticThenExponentialRateLimiter[reconcile.Request](l.rateLimiterConfig), + RateLimiter: rateLimiter, } return ctrl.NewControllerManagedBy(mgr). diff --git a/controller/lifecycle/multicluster/lifecycle.go b/controller/lifecycle/multicluster/lifecycle.go index 13a3378..c988046 100644 --- a/controller/lifecycle/multicluster/lifecycle.go +++ b/controller/lifecycle/multicluster/lifecycle.go @@ -94,10 +94,15 @@ func (l *LifecycleManager) SetupWithManagerBuilder(mgr mcmanager.Manager, maxRec return nil, fmt.Errorf("cannot use conditions or spread reconciles in read-only mode") } + rateLimiter, err := ratelimiter.NewStaticThenExponentialRateLimiter[mcreconcile.Request](l.rateLimiterConfig) + if err != nil{ + return nil, err + } + eventPredicates = append([]predicate.Predicate{filter.DebugResourcesBehaviourPredicate(debugLabelValue)}, eventPredicates...) opts := controller.TypedOptions[mcreconcile.Request]{ MaxConcurrentReconciles: maxReconciles, - RateLimiter: ratelimiter.NewStaticThenExponentialRateLimiter[mcreconcile.Request](l.rateLimiterConfig), + RateLimiter: rateLimiter, } return mcbuilder.ControllerManagedBy(mgr). diff --git a/controller/lifecycle/ratelimiter/config.go b/controller/lifecycle/ratelimiter/config.go index 7d720f2..1fb8cb6 100644 --- a/controller/lifecycle/ratelimiter/config.go +++ b/controller/lifecycle/ratelimiter/config.go @@ -1,6 +1,7 @@ package ratelimiter import ( + "fmt" "time" ) @@ -18,6 +19,22 @@ var defaultConfig = Config{ ExponentialMaxBackoff: 2 * time.Minute, } +func (c Config) validate() error { + if c.StaticRequeueDelay < 0 { + return fmt.Errorf("the static requeue delay shouldn't be negative") + } + if c.ExponentialInitialBackoff < 0 { + return fmt.Errorf("the initial exponential backoff shouldn't be negative") + } + if c.StaticRequeueDelay > c.ExponentialInitialBackoff { + return fmt.Errorf("the initial exponential backoff should be equal to or greater than the static requeue delay") + } + if c.StaticWindow < c.StaticRequeueDelay { + return fmt.Errorf("the static window duration should be equal to or greater than the static requeue delay") + } + return nil +} + type Option func(*Config) func WithStaticWindow(d time.Duration) Option { @@ -52,4 +69,4 @@ func NewConfig(options ...Option) Config { } return cfg -} \ No newline at end of file +} diff --git a/controller/lifecycle/ratelimiter/static_exponential.go b/controller/lifecycle/ratelimiter/static_exponential.go index 8759fd8..116d2c6 100644 --- a/controller/lifecycle/ratelimiter/static_exponential.go +++ b/controller/lifecycle/ratelimiter/static_exponential.go @@ -9,7 +9,7 @@ import ( ) type StaticThenExponentialRateLimiter[T comparable] struct { - failuresLock sync.Mutex + failuresLock sync.RWMutex firstAttempt map[T]time.Time staticDelay time.Duration @@ -19,7 +19,10 @@ type StaticThenExponentialRateLimiter[T comparable] struct { clock clock.Clock } -func NewStaticThenExponentialRateLimiter[T comparable](cfg Config) *StaticThenExponentialRateLimiter[T] { +func NewStaticThenExponentialRateLimiter[T comparable](cfg Config) (*StaticThenExponentialRateLimiter[T], error) { + if err := cfg.validate(); err != nil { + return nil, err + } return &StaticThenExponentialRateLimiter[T]{ staticDelay: cfg.StaticRequeueDelay, staticWindow: cfg.StaticWindow, @@ -29,19 +32,20 @@ func NewStaticThenExponentialRateLimiter[T comparable](cfg Config) *StaticThenEx ), firstAttempt: make(map[T]time.Time), clock: clock.RealClock{}, - } + },nil } func (r *StaticThenExponentialRateLimiter[T]) When(item T) time.Duration { - r.failuresLock.Lock() - defer r.failuresLock.Unlock() - now := r.clock.Now() + r.failuresLock.RLock() first, exists := r.firstAttempt[item] + r.failuresLock.RUnlock() if !exists { - first = now - r.firstAttempt[item] = first + r.failuresLock.Lock() + r.firstAttempt[item] = now + r.failuresLock.Unlock() + return r.staticDelay } timeSinceFirst := now.Sub(first) diff --git a/controller/lifecycle/ratelimiter/static_exponential_test.go b/controller/lifecycle/ratelimiter/static_exponential_test.go index a82d377..24e9ff5 100644 --- a/controller/lifecycle/ratelimiter/static_exponential_test.go +++ b/controller/lifecycle/ratelimiter/static_exponential_test.go @@ -17,7 +17,8 @@ func TestStaticThenExponentialRateLimiter_Forget(t *testing.T) { ExponentialInitialBackoff: 2 * time.Second, ExponentialMaxBackoff: 1 * time.Minute, } - limiter := NewStaticThenExponentialRateLimiter[reconcile.Request](cfg) + limiter, err := NewStaticThenExponentialRateLimiter[reconcile.Request](cfg) + require.Nil(t, err) fakeClock := clocktesting.NewFakeClock(time.Now()) limiter.clock = fakeClock From aba6e30162004640c7502e0eaf5348f320c25672 Mon Sep 17 00:00:00 2001 From: OlegErshov Date: Mon, 24 Nov 2025 12:10:51 +0100 Subject: [PATCH 6/9] used builder pattern for rate limiter set up On-behalf-of: SAP aleh.yarshou@sap.com --- controller/lifecycle/builder/builder.go | 13 +++++ controller/lifecycle/builder/builder_test.go | 55 +++++++++++++++++++ .../lifecycle/controllerruntime/lifecycle.go | 20 ++++--- .../controllerruntime/lifecycle_test.go | 5 +- .../lifecycle/multicluster/lifecycle.go | 20 ++++--- controller/lifecycle/ratelimiter/config.go | 8 +-- .../ratelimiter/static_exponential.go | 6 +- 7 files changed, 100 insertions(+), 27 deletions(-) diff --git a/controller/lifecycle/builder/builder.go b/controller/lifecycle/builder/builder.go index 53e574e..270bd9a 100644 --- a/controller/lifecycle/builder/builder.go +++ b/controller/lifecycle/builder/builder.go @@ -6,6 +6,7 @@ import ( "github.com/platform-mesh/golang-commons/controller/lifecycle/controllerruntime" "github.com/platform-mesh/golang-commons/controller/lifecycle/multicluster" + "github.com/platform-mesh/golang-commons/controller/lifecycle/ratelimiter" "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" "github.com/platform-mesh/golang-commons/logger" ) @@ -16,6 +17,7 @@ type Builder struct { withConditionManagement bool withSpreadingReconciles bool withReadOnly bool + rateLimiterOptions *[]ratelimiter.Option subroutines []subroutine.Subroutine log *logger.Logger } @@ -45,6 +47,11 @@ func (b *Builder) WithReadOnly() *Builder { return b } +func (b *Builder) WithStaticThenExponentialRateLimiter(opts ...ratelimiter.Option) *Builder { + b.rateLimiterOptions = &opts + return b +} + func (b *Builder) BuildControllerRuntime(cl client.Client) *controllerruntime.LifecycleManager { lm := controllerruntime.NewLifecycleManager(b.subroutines, b.operatorName, b.controllerName, cl, b.log) if b.withConditionManagement { @@ -56,6 +63,9 @@ func (b *Builder) BuildControllerRuntime(cl client.Client) *controllerruntime.Li if b.withReadOnly { lm.WithReadOnly() } + if b.rateLimiterOptions != nil { + lm.WithStaticThenExponentialRateLimiter((*b.rateLimiterOptions)...) + } return lm } @@ -70,5 +80,8 @@ func (b *Builder) BuildMultiCluster(mgr mcmanager.Manager) *multicluster.Lifecyc if b.withReadOnly { lm.WithReadOnly() } + if b.rateLimiterOptions != nil { + lm.WithStaticThenExponentialRateLimiter((*b.rateLimiterOptions)...) + } return lm } diff --git a/controller/lifecycle/builder/builder_test.go b/controller/lifecycle/builder/builder_test.go index 8de061e..b9bf7e3 100644 --- a/controller/lifecycle/builder/builder_test.go +++ b/controller/lifecycle/builder/builder_test.go @@ -2,11 +2,13 @@ package builder import ( "testing" + "time" "github.com/stretchr/testify/assert" "k8s.io/client-go/rest" mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + "github.com/platform-mesh/golang-commons/controller/lifecycle/ratelimiter" pmtesting "github.com/platform-mesh/golang-commons/controller/testSupport" "github.com/platform-mesh/golang-commons/logger" ) @@ -58,6 +60,38 @@ func TestBuilder_WithReadOnly(t *testing.T) { } } +func TestBuilder_WithCustomRateLimiter(t *testing.T) { + t.Run("With options", func(t *testing.T) { + b := NewBuilder("op", "ctrl", nil, &logger.Logger{}) + opts := []ratelimiter.Option{ + ratelimiter.WithRequeueDelay(5 * time.Second), + ratelimiter.WithStaticWindow(1 * time.Minute), + } + b.WithStaticThenExponentialRateLimiter(opts...) + if b.rateLimiterOptions == nil { + t.Error("expected rateLimiterOptions to be non-nil") + } + if got := len(*b.rateLimiterOptions); got != 2 { + t.Errorf("expected 2 rate limiter options, got %d", got) + } + }) + t.Run("Without options", func(t *testing.T) { + b := NewBuilder("op", "ctrl", nil, &logger.Logger{}) + b.WithStaticThenExponentialRateLimiter() + if b.rateLimiterOptions == nil { + t.Error("expected rateLimiterOptions to be non-nil even with no options") + } + if got := len(*b.rateLimiterOptions); got != 0 { + t.Errorf("expected 0 rate limiter options, got %d", got) + } + }) + + t.Run("Without custom rate limiter", func(t *testing.T) { + b := NewBuilder("op", "ctrl", nil, &logger.Logger{}) + assert.Nil(t, b.rateLimiterOptions) + }) +} + func TestControllerRuntimeBuilder(t *testing.T) { t.Run("Minimal setup", func(t *testing.T) { b := NewBuilder("op", "ctrl", nil, &logger.Logger{}) @@ -77,6 +111,15 @@ func TestControllerRuntimeBuilder(t *testing.T) { lm := b.BuildControllerRuntime(fakeClient) assert.NotNil(t, lm) }) + t.Run("WithCustomRateLimiter", func(t *testing.T) { + b := NewBuilder("op", "ctrl", nil, &logger.Logger{}).WithStaticThenExponentialRateLimiter( + ratelimiter.WithRequeueDelay(5*time.Second), + ratelimiter.WithStaticWindow(1*time.Minute), + ) + fakeClient := pmtesting.CreateFakeClient(t, &pmtesting.TestApiObject{}) + lm := b.BuildControllerRuntime(fakeClient) + assert.NotNil(t, lm) + }) } func TestMulticontrollerRuntimeBuilder(t *testing.T) { @@ -107,4 +150,16 @@ func TestMulticontrollerRuntimeBuilder(t *testing.T) { lm := b.BuildMultiCluster(mgr) assert.NotNil(t, lm) }) + t.Run("WithCustomRateLimiter", func(t *testing.T) { + b := NewBuilder("op", "ctrl", nil, &logger.Logger{}).WithStaticThenExponentialRateLimiter( + ratelimiter.WithRequeueDelay(5*time.Second), + ratelimiter.WithStaticWindow(1*time.Minute), + ) + cfg := &rest.Config{} + provider := pmtesting.NewFakeProvider(cfg) + mgr, err := mcmanager.New(cfg, provider, mcmanager.Options{}) + assert.NoError(t, err) + lm := b.BuildMultiCluster(mgr) + assert.NotNil(t, lm) + }) } diff --git a/controller/lifecycle/controllerruntime/lifecycle.go b/controller/lifecycle/controllerruntime/lifecycle.go index 57b8989..254ab44 100644 --- a/controller/lifecycle/controllerruntime/lifecycle.go +++ b/controller/lifecycle/controllerruntime/lifecycle.go @@ -30,7 +30,7 @@ type LifecycleManager struct { spreader *spread.Spreader conditionsManager *conditions.ConditionManager prepareContextFunc api.PrepareContextFunc - rateLimiterConfig ratelimiter.Config + rateLimiterConfig *ratelimiter.Config } func NewLifecycleManager(subroutines []subroutine.Subroutine, operatorName string, controllerName string, client client.Client, log *logger.Logger) *LifecycleManager { @@ -84,15 +84,17 @@ func (l *LifecycleManager) SetupWithManagerBuilder(mgr ctrl.Manager, maxReconcil return nil, fmt.Errorf("cannot use conditions or spread reconciles in read-only mode") } - rateLimiter, err := ratelimiter.NewStaticThenExponentialRateLimiter[reconcile.Request](l.rateLimiterConfig) - if err != nil { - return nil, err - } - eventPredicates = append([]predicate.Predicate{filter.DebugResourcesBehaviourPredicate(debugLabelValue)}, eventPredicates...) opts := controller.Options{ MaxConcurrentReconciles: maxReconciles, - RateLimiter: rateLimiter, + } + + if l.rateLimiterConfig != nil { + rateLimiter, err := ratelimiter.NewStaticThenExponentialRateLimiter[reconcile.Request](*l.rateLimiterConfig) + if err != nil { + return nil, err + } + opts.RateLimiter = rateLimiter } return ctrl.NewControllerManagedBy(mgr). @@ -136,8 +138,8 @@ func (l *LifecycleManager) WithConditionManagement() *LifecycleManager { return l } -func (l *LifecycleManager) WithRateLimiter(opts ...ratelimiter.Option) *LifecycleManager { +func (l *LifecycleManager) WithStaticThenExponentialRateLimiter(opts ...ratelimiter.Option) *LifecycleManager { cfg := ratelimiter.NewConfig(opts...) - l.rateLimiterConfig = cfg + l.rateLimiterConfig = &cfg return l } diff --git a/controller/lifecycle/controllerruntime/lifecycle_test.go b/controller/lifecycle/controllerruntime/lifecycle_test.go index 3728ab8..af5e809 100644 --- a/controller/lifecycle/controllerruntime/lifecycle_test.go +++ b/controller/lifecycle/controllerruntime/lifecycle_test.go @@ -165,14 +165,15 @@ func TestLifecycle(t *testing.T) { ExponentialInitialBackoff: 5 * time.Second, ExponentialMaxBackoff: time.Minute, } - l.WithRateLimiter( + l.WithStaticThenExponentialRateLimiter( ratelimiter.WithRequeueDelay(expectedCfg.StaticRequeueDelay), ratelimiter.WithStaticWindow(expectedCfg.StaticWindow), ratelimiter.WithExponentialInitialBackoff(expectedCfg.ExponentialInitialBackoff), ratelimiter.WithExponentialMaxBackoff(expectedCfg.ExponentialMaxBackoff), ) - assert.Equal(t, expectedCfg, l.rateLimiterConfig) + assert.NotNil(t, l.rateLimiterConfig) + assert.Equal(t, expectedCfg, *l.rateLimiterConfig) }) } diff --git a/controller/lifecycle/multicluster/lifecycle.go b/controller/lifecycle/multicluster/lifecycle.go index c988046..06554a5 100644 --- a/controller/lifecycle/multicluster/lifecycle.go +++ b/controller/lifecycle/multicluster/lifecycle.go @@ -36,7 +36,7 @@ type LifecycleManager struct { spreader *spread.Spreader conditionsManager *conditions.ConditionManager prepareContextFunc api.PrepareContextFunc - rateLimiterConfig ratelimiter.Config + rateLimiterConfig *ratelimiter.Config } func NewLifecycleManager(subroutines []subroutine.Subroutine, operatorName string, controllerName string, mgr ClusterGetter, log *logger.Logger) *LifecycleManager { @@ -94,15 +94,17 @@ func (l *LifecycleManager) SetupWithManagerBuilder(mgr mcmanager.Manager, maxRec return nil, fmt.Errorf("cannot use conditions or spread reconciles in read-only mode") } - rateLimiter, err := ratelimiter.NewStaticThenExponentialRateLimiter[mcreconcile.Request](l.rateLimiterConfig) - if err != nil{ - return nil, err - } - eventPredicates = append([]predicate.Predicate{filter.DebugResourcesBehaviourPredicate(debugLabelValue)}, eventPredicates...) opts := controller.TypedOptions[mcreconcile.Request]{ MaxConcurrentReconciles: maxReconciles, - RateLimiter: rateLimiter, + } + + if l.rateLimiterConfig != nil { + rateLimiter, err := ratelimiter.NewStaticThenExponentialRateLimiter[mcreconcile.Request](*l.rateLimiterConfig) + if err != nil { + return nil, err + } + opts.RateLimiter = rateLimiter } return mcbuilder.ControllerManagedBy(mgr). @@ -146,8 +148,8 @@ func (l *LifecycleManager) WithConditionManagement() api.Lifecycle { return l } -func (l *LifecycleManager) WithRateLimiter(opts ...ratelimiter.Option) *LifecycleManager { +func (l *LifecycleManager) WithStaticThenExponentialRateLimiter(opts ...ratelimiter.Option) *LifecycleManager { cfg := ratelimiter.NewConfig(opts...) - l.rateLimiterConfig = cfg + l.rateLimiterConfig = &cfg return l } diff --git a/controller/lifecycle/ratelimiter/config.go b/controller/lifecycle/ratelimiter/config.go index 1fb8cb6..e00d36d 100644 --- a/controller/lifecycle/ratelimiter/config.go +++ b/controller/lifecycle/ratelimiter/config.go @@ -13,10 +13,10 @@ type Config struct { } var defaultConfig = Config{ - StaticRequeueDelay: 5 * time.Second, - StaticWindow: 1 * time.Minute, - ExponentialInitialBackoff: 5 * time.Second, - ExponentialMaxBackoff: 2 * time.Minute, + StaticRequeueDelay: 2 * time.Second, + StaticWindow: 60 * time.Second, + ExponentialInitialBackoff: 2 * time.Second, + ExponentialMaxBackoff: 1000 * time.Second, } func (c Config) validate() error { diff --git a/controller/lifecycle/ratelimiter/static_exponential.go b/controller/lifecycle/ratelimiter/static_exponential.go index f83a6e9..ed00ea5 100644 --- a/controller/lifecycle/ratelimiter/static_exponential.go +++ b/controller/lifecycle/ratelimiter/static_exponential.go @@ -9,7 +9,7 @@ import ( ) type StaticThenExponentialRateLimiter[T comparable] struct { - failuresLock sync.RWMutex + failuresLock sync.RWMutex staticAttempts map[T]time.Time staticDelay time.Duration @@ -31,8 +31,8 @@ func NewStaticThenExponentialRateLimiter[T comparable](cfg Config) (*StaticThenE cfg.ExponentialMaxBackoff, ), staticAttempts: make(map[T]time.Time), - clock: clock.RealClock{}, - },nil + clock: clock.RealClock{}, + }, nil } func (r *StaticThenExponentialRateLimiter[T]) When(item T) time.Duration { From b7970efbb1ce87be67c3511e408abd676445b46e Mon Sep 17 00:00:00 2001 From: OlegErshov Date: Mon, 24 Nov 2025 15:06:46 +0100 Subject: [PATCH 7/9] refactored rate limiter set up On-behalf-of: SAP aleh.yarshou@sap.com --- .../lifecycle/controllerruntime/lifecycle.go | 15 ++++++--------- .../controllerruntime/lifecycle_test.go | 7 +++++-- controller/lifecycle/multicluster/lifecycle.go | 15 ++++++--------- controller/lifecycle/ratelimiter/config.go | 17 ----------------- .../lifecycle/ratelimiter/static_exponential.go | 7 ++----- .../ratelimiter/static_exponential_test.go | 3 +-- 6 files changed, 20 insertions(+), 44 deletions(-) diff --git a/controller/lifecycle/controllerruntime/lifecycle.go b/controller/lifecycle/controllerruntime/lifecycle.go index 254ab44..6415f2a 100644 --- a/controller/lifecycle/controllerruntime/lifecycle.go +++ b/controller/lifecycle/controllerruntime/lifecycle.go @@ -11,6 +11,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" + "k8s.io/client-go/util/workqueue" + "github.com/platform-mesh/golang-commons/controller/filter" "github.com/platform-mesh/golang-commons/controller/lifecycle" "github.com/platform-mesh/golang-commons/controller/lifecycle/api" @@ -30,7 +32,7 @@ type LifecycleManager struct { spreader *spread.Spreader conditionsManager *conditions.ConditionManager prepareContextFunc api.PrepareContextFunc - rateLimiterConfig *ratelimiter.Config + rateLimiter workqueue.TypedRateLimiter[reconcile.Request] } func NewLifecycleManager(subroutines []subroutine.Subroutine, operatorName string, controllerName string, client client.Client, log *logger.Logger) *LifecycleManager { @@ -89,12 +91,8 @@ func (l *LifecycleManager) SetupWithManagerBuilder(mgr ctrl.Manager, maxReconcil MaxConcurrentReconciles: maxReconciles, } - if l.rateLimiterConfig != nil { - rateLimiter, err := ratelimiter.NewStaticThenExponentialRateLimiter[reconcile.Request](*l.rateLimiterConfig) - if err != nil { - return nil, err - } - opts.RateLimiter = rateLimiter + if l.rateLimiter != nil { + opts.RateLimiter = l.rateLimiter } return ctrl.NewControllerManagedBy(mgr). @@ -139,7 +137,6 @@ func (l *LifecycleManager) WithConditionManagement() *LifecycleManager { } func (l *LifecycleManager) WithStaticThenExponentialRateLimiter(opts ...ratelimiter.Option) *LifecycleManager { - cfg := ratelimiter.NewConfig(opts...) - l.rateLimiterConfig = &cfg + l.rateLimiter = ratelimiter.NewStaticThenExponentialRateLimiter[reconcile.Request](ratelimiter.NewConfig(opts...)) return l } diff --git a/controller/lifecycle/controllerruntime/lifecycle_test.go b/controller/lifecycle/controllerruntime/lifecycle_test.go index af5e809..f056ba1 100644 --- a/controller/lifecycle/controllerruntime/lifecycle_test.go +++ b/controller/lifecycle/controllerruntime/lifecycle_test.go @@ -172,8 +172,11 @@ func TestLifecycle(t *testing.T) { ratelimiter.WithExponentialMaxBackoff(expectedCfg.ExponentialMaxBackoff), ) - assert.NotNil(t, l.rateLimiterConfig) - assert.Equal(t, expectedCfg, *l.rateLimiterConfig) + assert.NotNil(t, l.rateLimiter, "rate limiter should be configured") + + req := controllerruntime.Request{} + delay := l.rateLimiter.When(req) + assert.Equal(t, expectedCfg.StaticRequeueDelay, delay) }) } diff --git a/controller/lifecycle/multicluster/lifecycle.go b/controller/lifecycle/multicluster/lifecycle.go index 06554a5..17e90b9 100644 --- a/controller/lifecycle/multicluster/lifecycle.go +++ b/controller/lifecycle/multicluster/lifecycle.go @@ -13,6 +13,8 @@ import ( mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile" + "k8s.io/client-go/util/workqueue" + "github.com/platform-mesh/golang-commons/controller/filter" "github.com/platform-mesh/golang-commons/controller/lifecycle" "github.com/platform-mesh/golang-commons/controller/lifecycle/api" @@ -36,7 +38,7 @@ type LifecycleManager struct { spreader *spread.Spreader conditionsManager *conditions.ConditionManager prepareContextFunc api.PrepareContextFunc - rateLimiterConfig *ratelimiter.Config + rateLimiter workqueue.TypedRateLimiter[mcreconcile.Request] } func NewLifecycleManager(subroutines []subroutine.Subroutine, operatorName string, controllerName string, mgr ClusterGetter, log *logger.Logger) *LifecycleManager { @@ -99,12 +101,8 @@ func (l *LifecycleManager) SetupWithManagerBuilder(mgr mcmanager.Manager, maxRec MaxConcurrentReconciles: maxReconciles, } - if l.rateLimiterConfig != nil { - rateLimiter, err := ratelimiter.NewStaticThenExponentialRateLimiter[mcreconcile.Request](*l.rateLimiterConfig) - if err != nil { - return nil, err - } - opts.RateLimiter = rateLimiter + if l.rateLimiter != nil { + opts.RateLimiter = l.rateLimiter } return mcbuilder.ControllerManagedBy(mgr). @@ -149,7 +147,6 @@ func (l *LifecycleManager) WithConditionManagement() api.Lifecycle { } func (l *LifecycleManager) WithStaticThenExponentialRateLimiter(opts ...ratelimiter.Option) *LifecycleManager { - cfg := ratelimiter.NewConfig(opts...) - l.rateLimiterConfig = &cfg + l.rateLimiter = ratelimiter.NewStaticThenExponentialRateLimiter[mcreconcile.Request](ratelimiter.NewConfig(opts...)) return l } diff --git a/controller/lifecycle/ratelimiter/config.go b/controller/lifecycle/ratelimiter/config.go index e00d36d..ccdd252 100644 --- a/controller/lifecycle/ratelimiter/config.go +++ b/controller/lifecycle/ratelimiter/config.go @@ -1,7 +1,6 @@ package ratelimiter import ( - "fmt" "time" ) @@ -19,22 +18,6 @@ var defaultConfig = Config{ ExponentialMaxBackoff: 1000 * time.Second, } -func (c Config) validate() error { - if c.StaticRequeueDelay < 0 { - return fmt.Errorf("the static requeue delay shouldn't be negative") - } - if c.ExponentialInitialBackoff < 0 { - return fmt.Errorf("the initial exponential backoff shouldn't be negative") - } - if c.StaticRequeueDelay > c.ExponentialInitialBackoff { - return fmt.Errorf("the initial exponential backoff should be equal to or greater than the static requeue delay") - } - if c.StaticWindow < c.StaticRequeueDelay { - return fmt.Errorf("the static window duration should be equal to or greater than the static requeue delay") - } - return nil -} - type Option func(*Config) func WithStaticWindow(d time.Duration) Option { diff --git a/controller/lifecycle/ratelimiter/static_exponential.go b/controller/lifecycle/ratelimiter/static_exponential.go index ed00ea5..37b0ba9 100644 --- a/controller/lifecycle/ratelimiter/static_exponential.go +++ b/controller/lifecycle/ratelimiter/static_exponential.go @@ -19,10 +19,7 @@ type StaticThenExponentialRateLimiter[T comparable] struct { clock clock.Clock } -func NewStaticThenExponentialRateLimiter[T comparable](cfg Config) (*StaticThenExponentialRateLimiter[T], error) { - if err := cfg.validate(); err != nil { - return nil, err - } +func NewStaticThenExponentialRateLimiter[T comparable](cfg Config) *StaticThenExponentialRateLimiter[T] { return &StaticThenExponentialRateLimiter[T]{ staticDelay: cfg.StaticRequeueDelay, staticWindow: cfg.StaticWindow, @@ -32,7 +29,7 @@ func NewStaticThenExponentialRateLimiter[T comparable](cfg Config) (*StaticThenE ), staticAttempts: make(map[T]time.Time), clock: clock.RealClock{}, - }, nil + } } func (r *StaticThenExponentialRateLimiter[T]) When(item T) time.Duration { diff --git a/controller/lifecycle/ratelimiter/static_exponential_test.go b/controller/lifecycle/ratelimiter/static_exponential_test.go index 24e9ff5..a82d377 100644 --- a/controller/lifecycle/ratelimiter/static_exponential_test.go +++ b/controller/lifecycle/ratelimiter/static_exponential_test.go @@ -17,8 +17,7 @@ func TestStaticThenExponentialRateLimiter_Forget(t *testing.T) { ExponentialInitialBackoff: 2 * time.Second, ExponentialMaxBackoff: 1 * time.Minute, } - limiter, err := NewStaticThenExponentialRateLimiter[reconcile.Request](cfg) - require.Nil(t, err) + limiter := NewStaticThenExponentialRateLimiter[reconcile.Request](cfg) fakeClock := clocktesting.NewFakeClock(time.Now()) limiter.clock = fakeClock From e6f5d43327f7a7660230cdfb1a39e4e544000e08 Mon Sep 17 00:00:00 2001 From: OlegErshov Date: Mon, 24 Nov 2025 17:08:26 +0100 Subject: [PATCH 8/9] added rate limiter config error handling On-behalf-of: SAP aleh.yarshou@sap.com --- .../lifecycle/controllerruntime/lifecycle.go | 7 ++- .../lifecycle/multicluster/lifecycle.go | 7 ++- controller/lifecycle/ratelimiter/config.go | 17 ++++++ .../ratelimiter/static_exponential.go | 7 ++- .../ratelimiter/static_exponential_test.go | 53 ++++++++++++++++++- 5 files changed, 86 insertions(+), 5 deletions(-) diff --git a/controller/lifecycle/controllerruntime/lifecycle.go b/controller/lifecycle/controllerruntime/lifecycle.go index 6415f2a..23cbf0e 100644 --- a/controller/lifecycle/controllerruntime/lifecycle.go +++ b/controller/lifecycle/controllerruntime/lifecycle.go @@ -3,6 +3,7 @@ package controllerruntime import ( "context" "fmt" + "log" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" @@ -137,6 +138,10 @@ func (l *LifecycleManager) WithConditionManagement() *LifecycleManager { } func (l *LifecycleManager) WithStaticThenExponentialRateLimiter(opts ...ratelimiter.Option) *LifecycleManager { - l.rateLimiter = ratelimiter.NewStaticThenExponentialRateLimiter[reconcile.Request](ratelimiter.NewConfig(opts...)) + rateLimiter, err := ratelimiter.NewStaticThenExponentialRateLimiter[reconcile.Request](ratelimiter.NewConfig(opts...)) + if err != nil { + log.Fatalf("rate limiter config error: %s",err) + } + l.rateLimiter = rateLimiter return l } diff --git a/controller/lifecycle/multicluster/lifecycle.go b/controller/lifecycle/multicluster/lifecycle.go index 17e90b9..b4ccb7e 100644 --- a/controller/lifecycle/multicluster/lifecycle.go +++ b/controller/lifecycle/multicluster/lifecycle.go @@ -3,6 +3,7 @@ package multicluster import ( "context" "fmt" + "log" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/cluster" @@ -147,6 +148,10 @@ func (l *LifecycleManager) WithConditionManagement() api.Lifecycle { } func (l *LifecycleManager) WithStaticThenExponentialRateLimiter(opts ...ratelimiter.Option) *LifecycleManager { - l.rateLimiter = ratelimiter.NewStaticThenExponentialRateLimiter[mcreconcile.Request](ratelimiter.NewConfig(opts...)) + rateLimiter, err := ratelimiter.NewStaticThenExponentialRateLimiter[mcreconcile.Request](ratelimiter.NewConfig(opts...)) + if err != nil { + log.Fatalf("rate limiter config error: %s",err) + } + l.rateLimiter = rateLimiter return l } diff --git a/controller/lifecycle/ratelimiter/config.go b/controller/lifecycle/ratelimiter/config.go index ccdd252..e00d36d 100644 --- a/controller/lifecycle/ratelimiter/config.go +++ b/controller/lifecycle/ratelimiter/config.go @@ -1,6 +1,7 @@ package ratelimiter import ( + "fmt" "time" ) @@ -18,6 +19,22 @@ var defaultConfig = Config{ ExponentialMaxBackoff: 1000 * time.Second, } +func (c Config) validate() error { + if c.StaticRequeueDelay < 0 { + return fmt.Errorf("the static requeue delay shouldn't be negative") + } + if c.ExponentialInitialBackoff < 0 { + return fmt.Errorf("the initial exponential backoff shouldn't be negative") + } + if c.StaticRequeueDelay > c.ExponentialInitialBackoff { + return fmt.Errorf("the initial exponential backoff should be equal to or greater than the static requeue delay") + } + if c.StaticWindow < c.StaticRequeueDelay { + return fmt.Errorf("the static window duration should be equal to or greater than the static requeue delay") + } + return nil +} + type Option func(*Config) func WithStaticWindow(d time.Duration) Option { diff --git a/controller/lifecycle/ratelimiter/static_exponential.go b/controller/lifecycle/ratelimiter/static_exponential.go index 37b0ba9..ed00ea5 100644 --- a/controller/lifecycle/ratelimiter/static_exponential.go +++ b/controller/lifecycle/ratelimiter/static_exponential.go @@ -19,7 +19,10 @@ type StaticThenExponentialRateLimiter[T comparable] struct { clock clock.Clock } -func NewStaticThenExponentialRateLimiter[T comparable](cfg Config) *StaticThenExponentialRateLimiter[T] { +func NewStaticThenExponentialRateLimiter[T comparable](cfg Config) (*StaticThenExponentialRateLimiter[T], error) { + if err := cfg.validate(); err != nil { + return nil, err + } return &StaticThenExponentialRateLimiter[T]{ staticDelay: cfg.StaticRequeueDelay, staticWindow: cfg.StaticWindow, @@ -29,7 +32,7 @@ func NewStaticThenExponentialRateLimiter[T comparable](cfg Config) *StaticThenEx ), staticAttempts: make(map[T]time.Time), clock: clock.RealClock{}, - } + }, nil } func (r *StaticThenExponentialRateLimiter[T]) When(item T) time.Duration { diff --git a/controller/lifecycle/ratelimiter/static_exponential_test.go b/controller/lifecycle/ratelimiter/static_exponential_test.go index a82d377..68efbb7 100644 --- a/controller/lifecycle/ratelimiter/static_exponential_test.go +++ b/controller/lifecycle/ratelimiter/static_exponential_test.go @@ -17,7 +17,8 @@ func TestStaticThenExponentialRateLimiter_Forget(t *testing.T) { ExponentialInitialBackoff: 2 * time.Second, ExponentialMaxBackoff: 1 * time.Minute, } - limiter := NewStaticThenExponentialRateLimiter[reconcile.Request](cfg) + limiter, err := NewStaticThenExponentialRateLimiter[reconcile.Request](cfg) + require.NoError(t, err) fakeClock := clocktesting.NewFakeClock(time.Now()) limiter.clock = fakeClock @@ -31,3 +32,53 @@ func TestStaticThenExponentialRateLimiter_Forget(t *testing.T) { delay := limiter.When(item) require.Equal(t, cfg.StaticRequeueDelay, delay) } + +func TestStaticThenExponentialRateLimiter_InvalidConfig(t *testing.T) { + t.Run("negative static requeue delay", func(t *testing.T) { + cfg := Config{ + StaticRequeueDelay: -1 * time.Second, + StaticWindow: 5 * time.Second, + ExponentialInitialBackoff: 2 * time.Second, + ExponentialMaxBackoff: 1 * time.Minute, + } + _, err := NewStaticThenExponentialRateLimiter[reconcile.Request](cfg) + require.Error(t, err) + require.Contains(t, err.Error(), "static requeue delay shouldn't be negative") + }) + + t.Run("negative exponential initial backoff", func(t *testing.T) { + cfg := Config{ + StaticRequeueDelay: 1 * time.Second, + StaticWindow: 5 * time.Second, + ExponentialInitialBackoff: -1 * time.Second, + ExponentialMaxBackoff: 1 * time.Minute, + } + _, err := NewStaticThenExponentialRateLimiter[reconcile.Request](cfg) + require.Error(t, err) + require.Contains(t, err.Error(), "initial exponential backoff shouldn't be negative") + }) + + t.Run("static requeue delay greater than exponential initial backoff", func(t *testing.T) { + cfg := Config{ + StaticRequeueDelay: 5 * time.Second, + StaticWindow: 10 * time.Second, + ExponentialInitialBackoff: 2 * time.Second, + ExponentialMaxBackoff: 1 * time.Minute, + } + _, err := NewStaticThenExponentialRateLimiter[reconcile.Request](cfg) + require.Error(t, err) + require.Contains(t, err.Error(), "initial exponential backoff should be equal to or greater than the static requeue delay") + }) + + t.Run("static window less than static requeue delay", func(t *testing.T) { + cfg := Config{ + StaticRequeueDelay: 5 * time.Second, + StaticWindow: 2 * time.Second, + ExponentialInitialBackoff: 5 * time.Second, + ExponentialMaxBackoff: 1 * time.Minute, + } + _, err := NewStaticThenExponentialRateLimiter[reconcile.Request](cfg) + require.Error(t, err) + require.Contains(t, err.Error(), "static window duration should be equal to or greater than the static requeue delay") + }) +} From 42541e457ed14871c0f00fadfd10bb5ec9272d66 Mon Sep 17 00:00:00 2001 From: OlegErshov Date: Mon, 24 Nov 2025 17:13:41 +0100 Subject: [PATCH 9/9] fixed tests On-behalf-of: SAP aleh.yarshou@sap.com --- controller/lifecycle/builder/builder_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/controller/lifecycle/builder/builder_test.go b/controller/lifecycle/builder/builder_test.go index b9bf7e3..cd3dc67 100644 --- a/controller/lifecycle/builder/builder_test.go +++ b/controller/lifecycle/builder/builder_test.go @@ -115,6 +115,7 @@ func TestControllerRuntimeBuilder(t *testing.T) { b := NewBuilder("op", "ctrl", nil, &logger.Logger{}).WithStaticThenExponentialRateLimiter( ratelimiter.WithRequeueDelay(5*time.Second), ratelimiter.WithStaticWindow(1*time.Minute), + ratelimiter.WithExponentialInitialBackoff(5*time.Second), ) fakeClient := pmtesting.CreateFakeClient(t, &pmtesting.TestApiObject{}) lm := b.BuildControllerRuntime(fakeClient) @@ -154,6 +155,7 @@ func TestMulticontrollerRuntimeBuilder(t *testing.T) { b := NewBuilder("op", "ctrl", nil, &logger.Logger{}).WithStaticThenExponentialRateLimiter( ratelimiter.WithRequeueDelay(5*time.Second), ratelimiter.WithStaticWindow(1*time.Minute), + ratelimiter.WithExponentialInitialBackoff(5*time.Second), ) cfg := &rest.Config{} provider := pmtesting.NewFakeProvider(cfg)