Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions pkg/internal/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,9 @@ func (c *Controller[request]) reconcileHandler(ctx context.Context, req request,
// resource to be synced.
log.V(5).Info("Reconciling")
result, err := c.Reconcile(ctx, req)
if result.Priority != nil {
priority = *result.Priority
}
switch {
case err != nil:
if errors.Is(err, reconcile.TerminalError(nil)) {
Expand All @@ -468,8 +471,8 @@ func (c *Controller[request]) reconcileHandler(ctx context.Context, req request,
}
ctrlmetrics.ReconcileErrors.WithLabelValues(c.Name).Inc()
ctrlmetrics.ReconcileTotal.WithLabelValues(c.Name, labelError).Inc()
if !result.IsZero() {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If Priority is set result is not zero anymore. Not ideal, but I don't have a better idea.

Independent of that I think it's not especially bad to get rid of this warning.
I've written code specifically to set ctrl.Result{} to zero to avoid this warning which would have been absolutely not necessary otherwise

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could explicitly check for Requeue or RequeAfter being set. It is a bit awkward that we now respect a non-error return when an error is returned, that is very uncommon

Copy link
Member Author

@sbueringer sbueringer Oct 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could explicitly check for Requeue or RequeAfter being set.

I'll do that

It is a bit awkward that we now respect a non-error return when an error is returned, that is very uncommon

I think we don't "respect" it (in the sense of using it), we just don't write a warning on log level 0.

We have a bunch of controllers where the main reconcile logic returns either an error or non-zero ctrl.Result{}, but then we patch the reconciled object with defer. If that patch fails we want to return the error.

Because of this code here in controller-runtime we have to make sure that in these situations we set Result to zero to avoid this warning log: https://github.com/kubernetes-sigs/cluster-api/blob/main/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller.go#L274-L276

Not sure I see the value of this warning over CR just silently prioritizing error over ctrl.Result{} if both is returned

But this discussion is orthogonal to adding the Priority parameter. So let's continue the discussion (I'll re-add the warning for this PR)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we don't "respect" it (in the sense of using it), we just don't write a warning on log level 0.

What I meant is the Priority field, we respect it even in the error case

log.Info("Warning: Reconciler returned both a non-zero result and a non-nil error. The result will always be ignored if the error is non-nil and the non-nil error causes requeuing with exponential backoff. For more details, see: https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/reconcile#Reconciler")
if result.RequeueAfter > 0 || result.Requeue { //nolint: staticcheck // We have to handle Requeue until it is removed
log.Info("Warning: Reconciler returned both a result with either RequeueAfter or Requeue set and a non-nil error. RequeueAfter and Requeue will always be ignored if the error is non-nil. For more details, see: https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/reconcile#Reconciler")
}
log.Error(err, "Reconciler error")
case result.RequeueAfter > 0:
Expand Down
91 changes: 89 additions & 2 deletions pkg/internal/controller/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -745,7 +745,36 @@ var _ = Describe("controller", func() {
}}))
})

It("should requeue a Request after a duration (but not rate-limitted) if the Result sets RequeueAfter (regardless of Requeue)", func(ctx SpecContext) {
It("should use the priority from Result when the reconciler requests a requeue", func(ctx SpecContext) {
q := &fakePriorityQueue{PriorityQueue: priorityqueue.New[reconcile.Request]("controller1")}
ctrl.NewQueue = func(string, workqueue.TypedRateLimiter[reconcile.Request]) workqueue.TypedRateLimitingInterface[reconcile.Request] {
return q
}

go func() {
defer GinkgoRecover()
Expect(ctrl.Start(ctx)).NotTo(HaveOccurred())
}()

q.PriorityQueue.AddWithOpts(priorityqueue.AddOpts{Priority: ptr.To(10)}, request)

By("Invoking Reconciler which will request a requeue")
fakeReconcile.AddResult(reconcile.Result{Requeue: true, Priority: ptr.To(99)}, nil)
Expect(<-reconciled).To(Equal(request))
Eventually(func() []priorityQueueAddition {
q.lock.Lock()
defer q.lock.Unlock()
return q.added
}).Should(Equal([]priorityQueueAddition{{
AddOpts: priorityqueue.AddOpts{
RateLimited: true,
Priority: ptr.To(99),
},
items: []reconcile.Request{request},
}}))
})

It("should requeue a Request after a duration (but not rate-limited) if the Result sets RequeueAfter (regardless of Requeue)", func(ctx SpecContext) {
dq := &DelegatingQueue{TypedRateLimitingInterface: ctrl.NewQueue("controller1", nil)}
ctrl.NewQueue = func(string, workqueue.TypedRateLimiter[reconcile.Request]) workqueue.TypedRateLimitingInterface[reconcile.Request] {
return dq
Expand Down Expand Up @@ -775,7 +804,7 @@ var _ = Describe("controller", func() {
Eventually(func() int { return dq.NumRequeues(request) }).Should(Equal(0))
})

It("should retain the priority with RequeAfter", func(ctx SpecContext) {
It("should retain the priority with RequeueAfter", func(ctx SpecContext) {
q := &fakePriorityQueue{PriorityQueue: priorityqueue.New[reconcile.Request]("controller1")}
ctrl.NewQueue = func(string, workqueue.TypedRateLimiter[reconcile.Request]) workqueue.TypedRateLimitingInterface[reconcile.Request] {
return q
Expand Down Expand Up @@ -804,6 +833,35 @@ var _ = Describe("controller", func() {
}}))
})

It("should use the priority from Result with RequeueAfter", func(ctx SpecContext) {
q := &fakePriorityQueue{PriorityQueue: priorityqueue.New[reconcile.Request]("controller1")}
ctrl.NewQueue = func(string, workqueue.TypedRateLimiter[reconcile.Request]) workqueue.TypedRateLimitingInterface[reconcile.Request] {
return q
}

go func() {
defer GinkgoRecover()
Expect(ctrl.Start(ctx)).NotTo(HaveOccurred())
}()

q.PriorityQueue.AddWithOpts(priorityqueue.AddOpts{Priority: ptr.To(10)}, request)

By("Invoking Reconciler which will ask for RequeueAfter")
fakeReconcile.AddResult(reconcile.Result{RequeueAfter: time.Millisecond * 100, Priority: ptr.To(99)}, nil)
Expect(<-reconciled).To(Equal(request))
Eventually(func() []priorityQueueAddition {
q.lock.Lock()
defer q.lock.Unlock()
return q.added
}).Should(Equal([]priorityQueueAddition{{
AddOpts: priorityqueue.AddOpts{
After: time.Millisecond * 100,
Priority: ptr.To(99),
},
items: []reconcile.Request{request},
}}))
})

It("should perform error behavior if error is not nil, regardless of RequeueAfter", func(ctx SpecContext) {
dq := &DelegatingQueue{TypedRateLimitingInterface: ctrl.NewQueue("controller1", nil)}
ctrl.NewQueue = func(string, workqueue.TypedRateLimiter[reconcile.Request]) workqueue.TypedRateLimitingInterface[reconcile.Request] {
Expand Down Expand Up @@ -862,6 +920,35 @@ var _ = Describe("controller", func() {
}}))
})

It("should use the priority from Result when there was an error", func(ctx SpecContext) {
q := &fakePriorityQueue{PriorityQueue: priorityqueue.New[reconcile.Request]("controller1")}
ctrl.NewQueue = func(string, workqueue.TypedRateLimiter[reconcile.Request]) workqueue.TypedRateLimitingInterface[reconcile.Request] {
return q
}

go func() {
defer GinkgoRecover()
Expect(ctrl.Start(ctx)).NotTo(HaveOccurred())
}()

q.PriorityQueue.AddWithOpts(priorityqueue.AddOpts{Priority: ptr.To(10)}, request)

By("Invoking Reconciler which will return an error")
fakeReconcile.AddResult(reconcile.Result{Priority: ptr.To(99)}, errors.New("oups, I did it again"))
Expect(<-reconciled).To(Equal(request))
Eventually(func() []priorityQueueAddition {
q.lock.Lock()
defer q.lock.Unlock()
return q.added
}).Should(Equal([]priorityQueueAddition{{
AddOpts: priorityqueue.AddOpts{
RateLimited: true,
Priority: ptr.To(99),
},
items: []reconcile.Request{request},
}}))
})

PIt("should return if the queue is shutdown", func() {
// TODO(community): write this test
})
Expand Down
5 changes: 5 additions & 0 deletions pkg/reconcile/reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ type Result struct {
// RequeueAfter if greater than 0, tells the Controller to requeue the reconcile key after the Duration.
// Implies that Requeue is true, there is no need to set Requeue to true at the same time as RequeueAfter.
RequeueAfter time.Duration

// Priority is the priority that will be used if the item gets re-enqueued (also if an error is returned).
// If Priority is not set the original Priority of the request is preserved.
// Note: Priority is only respected if the controller is using a priorityqueue.PriorityQueue.
Priority *int
}

// IsZero returns true if this result is empty.
Expand Down