Skip to content

Commit 5309ed0

Browse files
authored
feat: integrate smart requeue logic into status updater (#102)
1 parent 4939d73 commit 5309ed0

File tree

3 files changed

+102
-0
lines changed

3 files changed

+102
-0
lines changed

docs/libs/status.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ You can then `Build()` the status updater and run `UpdateStatus()` to do the act
161161
- The `AllStatusFields()` function returns a list containing all status field keys, _except the one for the status field itself_, for convenience.
162162
- The `WithCustomUpdateFunc` method can be used to inject a function that performs custom logic on the resource's status. Note that while the function gets the complete object as an argument, only changes to its status will be updated by the status updater.
163163
- `WithConditionEvents` can be used to enable event recording for changed conditions. The events are automatically connected to the resource from the `ReconcileResult`'s `Object` field, no events will be recorded if that field is `nil`.
164+
- By using `WithSmartRequeue`, the [smart requeuing logic](./smartrequeue.md) can be used.
165+
- A `smartrequeue.Store` is required to be configured outside of the status updater, because it has to be persisted across multiple reconciliations.
166+
- It is also possible to use the smart requeue logic explicitly and modify the `ReconcileResult`'s `Result` field with the returned value, but the integration should be easier to use, since both, the smart requeue logic as well as the status updater, return a `reconcile.Result` and an `error`, which are intended to be directly used as return values for the `Reconcile` method.
164167

165168
### The ReconcileResult
166169

pkg/controller/status_updater.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"strings"
88

99
"github.com/openmcp-project/controller-utils/pkg/conditions"
10+
"github.com/openmcp-project/controller-utils/pkg/controller/smartrequeue"
1011
"github.com/openmcp-project/controller-utils/pkg/errors"
1112

1213
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -120,6 +121,29 @@ func (b *StatusUpdaterBuilder[Obj]) WithCustomUpdateFunc(f func(obj Obj, rr Reco
120121
return b
121122
}
122123

124+
type SmartRequeueAction string
125+
126+
const (
127+
SR_BACKOFF SmartRequeueAction = "Backoff"
128+
SR_RESET SmartRequeueAction = "Reset"
129+
SR_NO_REQUEUE SmartRequeueAction = "NoRequeue"
130+
)
131+
132+
// WithSmartRequeue integrates the smart requeue logic into the status updater.
133+
// Requires a smartrequeue.Store to be passed in (this needs to be persistent across multiple reconciliations and therefore cannot be stored in the status updater itself).
134+
// The action determines when the object should be requeued:
135+
// - "Backoff": the object is requeued with an increasing backoff, as specified in the store.
136+
// - "Reset": the object is requeued, but the backoff is reset to its minimal value, as specified in the store.
137+
// - "NoRequeue": the object is not requeued.
138+
// If the 'Result' field in the ReconcileResult has a non-zero RequeueAfter value set, that one is used if it is earlier than the one from smart requeue or if "NoRequeue" has been specified.
139+
// This function only has an effect if the Object in the ReconcileResult is not nil, the smart requeue store is not nil, and the action is one of the known values.
140+
// Also, if a reconciliation error occurred, the requeue interval will be reset, but no requeueAfter duration will be set, because controller-runtime will take care of requeuing the object anyway.
141+
func (b *StatusUpdaterBuilder[Obj]) WithSmartRequeue(store *smartrequeue.Store, action SmartRequeueAction) *StatusUpdaterBuilder[Obj] {
142+
b.internal.smartRequeueStore = store
143+
b.internal.smartRequeueAction = action
144+
return b
145+
}
146+
123147
// Build returns the status updater.
124148
func (b *StatusUpdaterBuilder[Obj]) Build() *statusUpdater[Obj] {
125149
return b.internal
@@ -158,6 +182,8 @@ type statusUpdater[Obj client.Object] struct {
158182
removeUntouchedConditions bool
159183
eventRecorder record.EventRecorder
160184
eventVerbosity conditions.EventVerbosity
185+
smartRequeueStore *smartrequeue.Store
186+
smartRequeueAction SmartRequeueAction
161187
}
162188

163189
func newStatusUpdater[Obj client.Object]() *statusUpdater[Obj] {
@@ -184,6 +210,8 @@ func defaultPhaseUpdateFunc[Obj client.Object](obj Obj, _ ReconcileResult[Obj])
184210
// UpdateStatus updates the status of the object in the given ReconcileResult, using the previously set field names and functions.
185211
// The object is expected to be a pointer to a struct with the status field.
186212
// If the 'Object' field in the ReconcileResult is nil, the status update becomes a no-op.
213+
//
214+
//nolint:gocyclo
187215
func (s *statusUpdater[Obj]) UpdateStatus(ctx context.Context, c client.Client, rr ReconcileResult[Obj]) (ctrl.Result, error) {
188216
errs := errors.NewReasonableErrorList(rr.ReconcileError)
189217
if IsNil(rr.Object) {
@@ -259,6 +287,25 @@ func (s *statusUpdater[Obj]) UpdateStatus(ctx context.Context, c client.Client,
259287
errs.Append(fmt.Errorf("error patching status: %w", err))
260288
}
261289

290+
if s.smartRequeueStore != nil {
291+
var srRes ctrl.Result
292+
if rr.ReconcileError != nil {
293+
srRes, _ = s.smartRequeueStore.For(rr.Object).Error(rr.ReconcileError)
294+
} else {
295+
switch s.smartRequeueAction {
296+
case SR_BACKOFF:
297+
srRes, _ = s.smartRequeueStore.For(rr.Object).Backoff()
298+
case SR_RESET:
299+
srRes, _ = s.smartRequeueStore.For(rr.Object).Reset()
300+
case SR_NO_REQUEUE:
301+
srRes, _ = s.smartRequeueStore.For(rr.Object).Never()
302+
}
303+
}
304+
if srRes.RequeueAfter > 0 && (rr.Result.RequeueAfter == 0 || srRes.RequeueAfter < rr.Result.RequeueAfter) {
305+
rr.Result.RequeueAfter = srRes.RequeueAfter
306+
}
307+
}
308+
262309
return rr.Result, errs.Aggregate()
263310
}
264311

pkg/controller/status_updater_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ import (
99
. "github.com/onsi/gomega"
1010

1111
"github.com/openmcp-project/controller-utils/pkg/conditions"
12+
"github.com/openmcp-project/controller-utils/pkg/controller/smartrequeue"
1213
. "github.com/openmcp-project/controller-utils/pkg/testing/matchers"
1314

1415
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1516
"k8s.io/apimachinery/pkg/runtime"
1617
"k8s.io/apimachinery/pkg/runtime/schema"
18+
ctrl "sigs.k8s.io/controller-runtime"
1719
"sigs.k8s.io/controller-runtime/pkg/client"
1820
"sigs.k8s.io/controller-runtime/pkg/scheme"
1921

@@ -194,6 +196,56 @@ var _ = Describe("Status Updater", func() {
194196
}
195197
})
196198

199+
Context("Smart Requeue", func() {
200+
201+
It("should add a requeueAfter duration if configured", func() {
202+
env := testutils.NewEnvironmentBuilder().WithFakeClient(coScheme).WithInitObjectPath("testdata", "test-02").WithDynamicObjectsWithStatus(&CustomObject{}).Build()
203+
obj := &CustomObject{}
204+
Expect(env.Client().Get(env.Ctx, controller.ObjectKey("status", "default"), obj)).To(Succeed())
205+
rr := controller.ReconcileResult[*CustomObject]{
206+
Object: obj,
207+
Conditions: dummyConditions(),
208+
}
209+
store := smartrequeue.NewStore(1*time.Second, 10*time.Second, 2.0)
210+
su := preconfiguredStatusUpdaterBuilder().WithPhaseUpdateFunc(func(obj *CustomObject, rr controller.ReconcileResult[*CustomObject]) (string, error) {
211+
return PhaseSucceeded, nil
212+
}).WithSmartRequeue(store, controller.SR_RESET).Build()
213+
res, err := su.UpdateStatus(env.Ctx, env.Client(), rr)
214+
Expect(err).ToNot(HaveOccurred())
215+
Expect(res.RequeueAfter).To(Equal(1 * time.Second))
216+
})
217+
218+
It("should keep the smaller requeueAfter duration if smart requeue and RequeueAfter in the ReconcileResult are set", func() {
219+
env := testutils.NewEnvironmentBuilder().WithFakeClient(coScheme).WithInitObjectPath("testdata", "test-02").WithDynamicObjectsWithStatus(&CustomObject{}).Build()
220+
obj := &CustomObject{}
221+
Expect(env.Client().Get(env.Ctx, controller.ObjectKey("status", "default"), obj)).To(Succeed())
222+
rr := controller.ReconcileResult[*CustomObject]{
223+
Object: obj,
224+
Conditions: dummyConditions(),
225+
Result: ctrl.Result{
226+
RequeueAfter: 30 * time.Second,
227+
},
228+
}
229+
store := smartrequeue.NewStore(1*time.Second, 10*time.Second, 2.0)
230+
su := preconfiguredStatusUpdaterBuilder().WithPhaseUpdateFunc(func(obj *CustomObject, rr controller.ReconcileResult[*CustomObject]) (string, error) {
231+
return PhaseSucceeded, nil
232+
}).WithSmartRequeue(store, controller.SR_RESET).Build()
233+
res, err := su.UpdateStatus(env.Ctx, env.Client(), rr)
234+
Expect(err).ToNot(HaveOccurred())
235+
Expect(res.RequeueAfter).To(Equal(1 * time.Second))
236+
237+
rr.Result.RequeueAfter = 30 * time.Second
238+
store = smartrequeue.NewStore(1*time.Minute, 10*time.Minute, 2.0)
239+
su = preconfiguredStatusUpdaterBuilder().WithPhaseUpdateFunc(func(obj *CustomObject, rr controller.ReconcileResult[*CustomObject]) (string, error) {
240+
return PhaseSucceeded, nil
241+
}).WithSmartRequeue(store, controller.SR_RESET).Build()
242+
res, err = su.UpdateStatus(env.Ctx, env.Client(), rr)
243+
Expect(err).ToNot(HaveOccurred())
244+
Expect(res.RequeueAfter).To(Equal(30 * time.Second))
245+
})
246+
247+
})
248+
197249
Context("GenerateCreateConditionFunc", func() {
198250

199251
It("should add the condition to the given ReconcileResult", func() {

0 commit comments

Comments
 (0)