generated from amazon-archives/__template_Apache-2.0
-
Notifications
You must be signed in to change notification settings - Fork 22
feat: add AsReconciler wrapper to rate limit and replace controller-runtime's requeue: true behavior #160
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
feat: add AsReconciler wrapper to rate limit and replace controller-runtime's requeue: true behavior #160
Changes from 14 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
790cf1c
feat: add default singleton rate limiter to replace requeue
rschalo 65907aa
feat: Add requeue adapter for rate-limiting
rschalo b44f368
clean up
rschalo 956678b
fix presubmit
rschalo 4c6d4c3
Merge branch 'main' into default-singleton-rate-limiter
rschalo ab74bfc
pr responses
rschalo d40e4b2
reduce diff
rschalo 393dc54
further reduce diff
rschalo 4cdda31
remove describe
rschalo d408d23
formatting
rschalo c7b120d
pr responses
rschalo 3ab7250
pr responses
rschalo 4ccfb14
fix comments
rschalo 6129d0b
Merge branch 'main' into default-singleton-rate-limiter
rschalo 2ef6efd
add forget to requeueAfter and move test to reconciler
rschalo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| package reconciler | ||
|
|
||
| import ( | ||
| "context" | ||
| "time" | ||
|
|
||
| "k8s.io/client-go/util/workqueue" | ||
| "sigs.k8s.io/controller-runtime/pkg/reconcile" | ||
| ) | ||
|
|
||
| // Result adds Requeue functionality back to reconcile results. | ||
| type Result struct { | ||
| RequeueAfter time.Duration | ||
| Requeue bool | ||
| } | ||
|
|
||
| // Reconciler defines the interface for standard reconcilers | ||
| type Reconciler interface { | ||
| Reconcile(ctx context.Context, req reconcile.Request) (Result, error) | ||
| } | ||
|
|
||
| // AsReconciler creates a reconciler with a default rate-limiter | ||
| func AsReconciler(reconciler Reconciler) reconcile.Reconciler { | ||
| return AsReconcilerWithRateLimiter( | ||
| reconciler, | ||
| workqueue.DefaultTypedControllerRateLimiter[reconcile.Request](), | ||
| ) | ||
| } | ||
|
|
||
| // AsReconcilerWithRateLimiter creates a reconciler with a custom rate-limiter | ||
| func AsReconcilerWithRateLimiter( | ||
| reconciler Reconciler, | ||
| rateLimiter workqueue.TypedRateLimiter[reconcile.Request], | ||
| ) reconcile.Reconciler { | ||
| return reconcile.Func(func(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { | ||
| result, err := reconciler.Reconcile(ctx, req) | ||
| if err != nil { | ||
| return reconcile.Result{}, err | ||
| } | ||
| if result.RequeueAfter > 0 { | ||
| return reconcile.Result{RequeueAfter: result.RequeueAfter}, nil | ||
rschalo marked this conversation as resolved.
Show resolved
Hide resolved
rschalo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| if result.Requeue { | ||
| return reconcile.Result{RequeueAfter: rateLimiter.When(req)}, nil | ||
| } | ||
| rateLimiter.Forget(req) | ||
| return reconcile.Result{}, nil | ||
| }) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,260 @@ | ||
| package reconciler_test | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/awslabs/operatorpkg/reconciler" | ||
| . "github.com/onsi/ginkgo/v2" | ||
| . "github.com/onsi/gomega" | ||
| "k8s.io/apimachinery/pkg/types" | ||
| "sigs.k8s.io/controller-runtime/pkg/reconcile" | ||
| ) | ||
|
|
||
| func Test(t *testing.T) { | ||
| RegisterFailHandler(Fail) | ||
| RunSpecs(t, "Reconciler") | ||
| } | ||
|
|
||
| // MockRateLimiter is a mock implementation of workqueue.TypedRateLimiter for testing | ||
| type MockRateLimiter[K comparable] struct { | ||
| whenFunc func(K) time.Duration | ||
| numRequeues map[K]int | ||
| backoffDuration time.Duration | ||
| } | ||
|
|
||
| func (m *MockRateLimiter[K]) When(key K) time.Duration { | ||
| if m.whenFunc != nil { | ||
| return m.whenFunc(key) | ||
| } | ||
| // Default implementation | ||
| if m.numRequeues == nil { | ||
| m.numRequeues = make(map[K]int) | ||
| } | ||
| m.numRequeues[key] += 1 | ||
| return m.backoffDuration | ||
| } | ||
|
|
||
| func (m *MockRateLimiter[K]) NumRequeues(key K) int { | ||
| return m.numRequeues[key] | ||
| } | ||
|
|
||
| func (m *MockRateLimiter[K]) Forget(key K) { | ||
| delete(m.numRequeues, key) | ||
| } | ||
|
|
||
| // MockReconciler is a mock implementation of Reconciler for testing | ||
| type MockReconciler struct { | ||
| reconcileFunc func(context.Context, reconcile.Request) (reconciler.Result, error) | ||
| result reconciler.Result | ||
| err error | ||
| } | ||
|
|
||
| func (m *MockReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconciler.Result, error) { | ||
| if m.reconcileFunc != nil { | ||
| return m.reconcileFunc(ctx, req) | ||
| } | ||
| return m.result, m.err | ||
| } | ||
|
|
||
| var _ = Describe("Reconciler", func() { | ||
rschalo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| It("should return the result without backoff", func() { | ||
| mockReconciler := &MockReconciler{ | ||
| result: reconciler.Result{}, | ||
| } | ||
|
|
||
| reconciler := reconciler.AsReconciler(mockReconciler) | ||
|
|
||
| result, err := reconciler.Reconcile(context.Background(), reconcile.Request{}) | ||
|
|
||
| Expect(err).NotTo(HaveOccurred()) | ||
| Expect(result.RequeueAfter).To(Equal(0 * time.Second)) | ||
| }) | ||
| It("should return the result with backoff when Requeue is set", func() { | ||
| mockReconciler := &MockReconciler{ | ||
| result: reconciler.Result{ | ||
| Requeue: true, | ||
| }, | ||
| } | ||
|
|
||
| reconciler := reconciler.AsReconciler(mockReconciler) | ||
|
|
||
| result, err := reconciler.Reconcile(context.Background(), reconcile.Request{}) | ||
|
|
||
| Expect(err).NotTo(HaveOccurred()) | ||
| Expect(result.RequeueAfter).To(Equal(5 * time.Millisecond)) | ||
| }) | ||
| It("should return the result with backoff when both RequeueAfter and Requeue are set", func() { | ||
| mockReconciler := &MockReconciler{ | ||
| result: reconciler.Result{ | ||
| RequeueAfter: 10 * time.Second, | ||
| Requeue: true, | ||
| }, | ||
| } | ||
|
|
||
| reconciler := reconciler.AsReconciler(mockReconciler) | ||
|
|
||
| result, err := reconciler.Reconcile(context.Background(), reconcile.Request{}) | ||
|
|
||
| Expect(err).NotTo(HaveOccurred()) | ||
| Expect(result.RequeueAfter).To(Equal(10 * time.Second)) | ||
| }) | ||
| It("should return the result with backoff when RequeueAfter is set and Requeue is false", func() { | ||
| mockReconciler := &MockReconciler{ | ||
| result: reconciler.Result{ | ||
| RequeueAfter: 10 * time.Second, | ||
| Requeue: false, | ||
| }, | ||
| } | ||
|
|
||
| reconciler := reconciler.AsReconciler(mockReconciler) | ||
|
|
||
| result, err := reconciler.Reconcile(context.Background(), reconcile.Request{}) | ||
|
|
||
| Expect(err).NotTo(HaveOccurred()) | ||
| Expect(result.RequeueAfter).To(Equal(10 * time.Second)) | ||
| }) | ||
| It("should return the result with backoff when RequeueAfter is set to zero and Requeue is true", func() { | ||
| mockReconciler := &MockReconciler{ | ||
| result: reconciler.Result{ | ||
| RequeueAfter: 0 * time.Second, | ||
| Requeue: true, | ||
| }, | ||
| } | ||
|
|
||
| reconciler := reconciler.AsReconciler(mockReconciler) | ||
|
|
||
| result, err := reconciler.Reconcile(context.Background(), reconcile.Request{}) | ||
|
|
||
| Expect(err).NotTo(HaveOccurred()) | ||
| Expect(result.RequeueAfter).To(Equal(5 * time.Millisecond)) | ||
| }) | ||
| It("should return the result without backoff when RequeueAfter is set to zero and Requeue is false", func() { | ||
| mockReconciler := &MockReconciler{ | ||
| result: reconciler.Result{ | ||
| RequeueAfter: 0 * time.Second, | ||
| Requeue: false, | ||
| }, | ||
| } | ||
|
|
||
| reconciler := reconciler.AsReconciler(mockReconciler) | ||
|
|
||
| result, err := reconciler.Reconcile(context.Background(), reconcile.Request{}) | ||
|
|
||
| Expect(err).NotTo(HaveOccurred()) | ||
| Expect(result.RequeueAfter).To(Equal(0 * time.Millisecond)) | ||
| }) | ||
| It("should return the error without processing backoff", func() { | ||
| expectedErr := errors.New("test error") | ||
| mockReconciler := &MockReconciler{ | ||
| result: reconciler.Result{Requeue: true}, | ||
| err: expectedErr, | ||
| } | ||
|
|
||
| reconciler := reconciler.AsReconciler(mockReconciler) | ||
|
|
||
| result, err := reconciler.Reconcile(context.Background(), reconcile.Request{}) | ||
|
|
||
| Expect(err).To(HaveOccurred()) | ||
| Expect(err).To(Equal(expectedErr)) | ||
| Expect(result.RequeueAfter).To(BeZero()) | ||
| }) | ||
| It("should use custom rate limiter for backoff", func() { | ||
| mockRateLimiter := &MockRateLimiter[reconcile.Request]{ | ||
| backoffDuration: 10 * time.Second, | ||
| } | ||
|
|
||
| mockReconciler := &MockReconciler{ | ||
| result: reconciler.Result{ | ||
| Requeue: true, | ||
| }, | ||
| } | ||
|
|
||
| reconciler := reconciler.AsReconcilerWithRateLimiter(mockReconciler, mockRateLimiter) | ||
|
|
||
| req := reconcile.Request{} | ||
| result, err := reconciler.Reconcile(context.Background(), req) | ||
|
|
||
| Expect(err).NotTo(HaveOccurred()) | ||
| Expect(result.RequeueAfter).To(Equal(10 * time.Second)) | ||
| Expect(mockRateLimiter.NumRequeues(req)).To(Equal(1)) | ||
| }) | ||
| It("should rate limit distinct items", func() { | ||
| mockRateLimiter := &MockRateLimiter[reconcile.Request]{ | ||
| backoffDuration: 10 * time.Second, | ||
| } | ||
|
|
||
| mockReconciler := &MockReconciler{ | ||
| result: reconciler.Result{ | ||
| Requeue: true, | ||
| }, | ||
| } | ||
|
|
||
| reconciler := reconciler.AsReconcilerWithRateLimiter(mockReconciler, mockRateLimiter) | ||
|
|
||
| req1 := reconcile.Request{ | ||
| NamespacedName: types.NamespacedName{ | ||
| Name: "req1", | ||
| Namespace: "", | ||
| }, | ||
| } | ||
| result1, err1 := reconciler.Reconcile(context.Background(), req1) | ||
| req2 := reconcile.Request{ | ||
| NamespacedName: types.NamespacedName{ | ||
| Name: "req2", | ||
| Namespace: "", | ||
| }, | ||
| } | ||
| result2, err2 := reconciler.Reconcile(context.Background(), req2) | ||
|
|
||
| Expect(err1).NotTo(HaveOccurred()) | ||
| Expect(result1.RequeueAfter).To(Equal(10 * time.Second)) | ||
| Expect(err2).NotTo(HaveOccurred()) | ||
| Expect(result2.RequeueAfter).To(Equal(10 * time.Second)) | ||
| Expect(mockRateLimiter.NumRequeues(req1)).To(Equal(1)) | ||
| Expect(mockRateLimiter.NumRequeues(req2)).To(Equal(1)) | ||
| }) | ||
| It("should implement exponential backoff on repeated calls", func() { | ||
| mockReconciler := &MockReconciler{ | ||
| result: reconciler.Result{ | ||
| Requeue: true, | ||
| }, | ||
| } | ||
| // Multiple calls to the same controller should show increasing delays | ||
| delays := make([]time.Duration, 5) | ||
| reconciler := reconciler.AsReconciler(mockReconciler) | ||
|
|
||
| for i := range 5 { | ||
| result, err := reconciler.Reconcile(context.Background(), reconcile.Request{}) | ||
| Expect(err).NotTo(HaveOccurred()) | ||
| delays[i] = result.RequeueAfter | ||
| } | ||
|
|
||
| initialDelay := 5 * time.Millisecond | ||
| Expect(delays[0]).To(BeNumerically("==", initialDelay)) | ||
| for i := 1; i < len(delays); i++ { | ||
| initialDelay *= 2 | ||
| Expect(delays[i]).To(BeNumerically("==", initialDelay)) | ||
| Expect(delays[i]).To(BeNumerically(">", delays[i-1]), | ||
| "Delay at index %d (%v) should be >= delay at index %d (%v)", | ||
| i, delays[i], i-1, delays[i-1]) | ||
| } | ||
| }) | ||
| It("should forget an item when reconcile succeeds", func() { | ||
| mockReconciler := &MockReconciler{ | ||
| result: reconciler.Result{ | ||
| Requeue: false, | ||
| }, | ||
| } | ||
| // Multiple calls to the same controller should show zero requeues | ||
| reconciler := reconciler.AsReconciler(mockReconciler) | ||
|
|
||
| for i := 0; i < 5; i++ { | ||
| result, err := reconciler.Reconcile(context.Background(), reconcile.Request{}) | ||
| Expect(err).NotTo(HaveOccurred()) | ||
| Expect(result.RequeueAfter).To(BeZero()) | ||
| } | ||
| }) | ||
| }) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.