Skip to content

Commit b97ee65

Browse files
committed
add retriable errors
There is a new error type (types.RetriableError) which can be used anywhere in the reconciler logic (in particular in hooks and generators) to wrap other occuring errors; optionally a retry delay can be set on that error object; such errors will then be caught late in the reconciler flow, immediately before returning to controller-runtime. Basically, no error (but nil) will be returned to controller-runtime, and a RequeueAfter with the desired delay will be set instead in the response. This bypasses the (sometimes unwanted) default backoff done by controller-runtime, and leads to a li:wqnear retry behaviour. In addition, components can implement two new interfaces component.RequeueConfiguration and component.RetryConfiguration, in order to override the default requeue interval (10m) and the default retry interval, which applies to retriable errors which do not specify a delay (default is the value of requeue interval).
1 parent 7619f70 commit b97ee65

File tree

5 files changed

+106
-9
lines changed

5 files changed

+106
-9
lines changed

pkg/component/component.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,28 @@ func assertImpersonationConfiguration[T Component](component T) (ImpersonationCo
6565
return nil, false
6666
}
6767

68+
// Check if given component or its spec implements RequeueConfiguration (and return it).
69+
func assertRequeueConfiguration[T Component](component T) (RequeueConfiguration, bool) {
70+
if requeueConfiguration, ok := Component(component).(RequeueConfiguration); ok {
71+
return requeueConfiguration, true
72+
}
73+
if requeueConfiguration, ok := getSpec(component).(RequeueConfiguration); ok {
74+
return requeueConfiguration, true
75+
}
76+
return nil, false
77+
}
78+
79+
// Check if given component or its spec implements RetryConfiguration (and return it).
80+
func assertRetryConfiguration[T Component](component T) (RetryConfiguration, bool) {
81+
if retryConfiguration, ok := Component(component).(RetryConfiguration); ok {
82+
return retryConfiguration, true
83+
}
84+
if retryConfiguration, ok := getSpec(component).(RetryConfiguration); ok {
85+
return retryConfiguration, true
86+
}
87+
return nil, false
88+
}
89+
6890
// Implement the PlacementConfiguration interface.
6991
func (s *PlacementSpec) GetDeploymentNamespace() string {
7092
return s.Namespace

pkg/component/reconcile.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,22 @@ func (r *Reconciler[T]) Reconcile(ctx context.Context, req ctrl.Request) (result
133133
status := component.GetStatus()
134134
savedStatus := status.DeepCopy()
135135

136+
// requeue/retry interval
137+
requeueInterval := time.Duration(0)
138+
if requeueConfiguration, ok := assertRequeueConfiguration(component); ok {
139+
requeueInterval = requeueConfiguration.GetRequeueInterval()
140+
}
141+
if requeueInterval == 0 {
142+
requeueInterval = 10 * time.Minute
143+
}
144+
retryInterval := time.Duration(0)
145+
if retryConfiguration, ok := assertRetryConfiguration(component); ok {
146+
retryInterval = retryConfiguration.GetRetryInterval()
147+
}
148+
if retryInterval == 0 {
149+
retryInterval = requeueInterval
150+
}
151+
136152
// always attempt to update the status
137153
skipStatusUpdate := false
138154
defer func() {
@@ -150,6 +166,15 @@ func (r *Reconciler[T]) Reconcile(ctx context.Context, req ctrl.Request) (result
150166
} else {
151167
r.client.EventRecorder().Event(component, corev1.EventTypeNormal, reason, message)
152168
}
169+
retriableError := &types.RetriableError{}
170+
if errors.As(err, retriableError) {
171+
retryAfter := retriableError.RetryAfter()
172+
if retryAfter == nil || *retryAfter == 0 {
173+
retryAfter = &retryInterval
174+
}
175+
result = ctrl.Result{RequeueAfter: *retryAfter}
176+
err = nil
177+
}
153178
if skipStatusUpdate {
154179
return
155180
}
@@ -219,7 +244,7 @@ func (r *Reconciler[T]) Reconcile(ctx context.Context, req ctrl.Request) (result
219244
status.SetState(StateReady, readyConditionReasonReady, "Dependent resources successfully reconciled")
220245
status.AppliedGeneration = component.GetGeneration()
221246
status.LastAppliedAt = &now
222-
return ctrl.Result{RequeueAfter: 10 * time.Minute}, nil
247+
return ctrl.Result{RequeueAfter: requeueInterval}, nil
223248
} else {
224249
log.V(1).Info("not all dependent resources successfully reconciled")
225250
status.SetState(StateProcessing, readyConditionReasonProcessing, "Reconcilation of dependent resources triggered; waiting until all dependent resources are ready")

pkg/component/reference.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"sigs.k8s.io/controller-runtime/pkg/client"
1717

1818
"github.com/sap/component-operator-runtime/internal/walk"
19+
"github.com/sap/component-operator-runtime/pkg/types"
1920
)
2021

2122
// +kubebuilder:object:generate=true
@@ -30,7 +31,7 @@ type ConfigMapReference struct {
3031
func (r *ConfigMapReference) load(ctx context.Context, client client.Client, namespace string) error {
3132
configMap := &corev1.ConfigMap{}
3233
if err := client.Get(ctx, apitypes.NamespacedName{Namespace: namespace, Name: r.Name}, configMap); err != nil {
33-
return err
34+
return types.NewRetriableError(err, nil)
3435
}
3536
r.data = configMap.Data
3637
return nil
@@ -55,14 +56,14 @@ type ConfigMapKeyReference struct {
5556
func (r *ConfigMapKeyReference) load(ctx context.Context, client client.Client, namespace string, fallbackKeys ...string) error {
5657
configMap := &corev1.ConfigMap{}
5758
if err := client.Get(ctx, apitypes.NamespacedName{Namespace: namespace, Name: r.Name}, configMap); err != nil {
58-
return err
59+
return types.NewRetriableError(err, nil)
5960
}
6061
if r.Key != "" {
6162
if value, ok := configMap.Data[r.Key]; ok {
6263
r.value = value
6364
return nil
6465
} else {
65-
return fmt.Errorf("key %s not found in configmap %s/%s", r.Key, namespace, r.Name)
66+
return types.NewRetriableError(fmt.Errorf("key %s not found in configmap %s/%s", r.Key, namespace, r.Name), nil)
6667
}
6768
} else {
6869
for _, key := range fallbackKeys {
@@ -71,7 +72,7 @@ func (r *ConfigMapKeyReference) load(ctx context.Context, client client.Client,
7172
return nil
7273
}
7374
}
74-
return fmt.Errorf("no matching key found in configmap %s/%s", namespace, r.Name)
75+
return types.NewRetriableError(fmt.Errorf("no matching key found in configmap %s/%s", namespace, r.Name), nil)
7576
}
7677
}
7778

@@ -92,7 +93,7 @@ type SecretReference struct {
9293
func (r *SecretReference) load(ctx context.Context, client client.Client, namespace string) error {
9394
secret := &corev1.Secret{}
9495
if err := client.Get(ctx, apitypes.NamespacedName{Namespace: namespace, Name: r.Name}, secret); err != nil {
95-
return err
96+
return types.NewRetriableError(err, nil)
9697
}
9798
r.data = secret.Data
9899
return nil
@@ -117,14 +118,14 @@ type SecretKeyReference struct {
117118
func (r *SecretKeyReference) load(ctx context.Context, client client.Client, namespace string, fallbackKeys ...string) error {
118119
secret := &corev1.Secret{}
119120
if err := client.Get(ctx, apitypes.NamespacedName{Namespace: namespace, Name: r.Name}, secret); err != nil {
120-
return err
121+
return types.NewRetriableError(err, nil)
121122
}
122123
if r.Key != "" {
123124
if value, ok := secret.Data[r.Key]; ok {
124125
r.value = value
125126
return nil
126127
} else {
127-
return fmt.Errorf("key %s not found in secret %s/%s", r.Key, namespace, r.Name)
128+
return types.NewRetriableError(fmt.Errorf("key %s not found in secret %s/%s", r.Key, namespace, r.Name), nil)
128129
}
129130
} else {
130131
for _, key := range fallbackKeys {
@@ -133,7 +134,7 @@ func (r *SecretKeyReference) load(ctx context.Context, client client.Client, nam
133134
return nil
134135
}
135136
}
136-
return fmt.Errorf("no matching key found in secret %s/%s", namespace, r.Name)
137+
return types.NewRetriableError(fmt.Errorf("no matching key found in secret %s/%s", namespace, r.Name), nil)
137138
}
138139
}
139140

pkg/component/types.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ SPDX-License-Identifier: Apache-2.0
66
package component
77

88
import (
9+
"time"
10+
911
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1012
"sigs.k8s.io/controller-runtime/pkg/client"
1113

@@ -58,6 +60,20 @@ type ImpersonationConfiguration interface {
5860
GetImpersonationGroups() []string
5961
}
6062

63+
// The RequeueConfiguration interface is meant to be implemented by components (or their spec) which offer
64+
// tweaking the requeue interval (by default, it would be 10 minutes).
65+
type RequeueConfiguration interface {
66+
// Get requeue interval. Should be greater than 1 minute.
67+
GetRequeueInterval() time.Duration
68+
}
69+
70+
// The RetryConfiguration interface is meant to be implemented by components (or their spec) which offer
71+
// tweaking the retry interval (by default, it would be the value of the requeue interval).
72+
type RetryConfiguration interface {
73+
// Get retry interval. Should be greater than 1 minute.
74+
GetRetryInterval() time.Duration
75+
}
76+
6177
// +kubebuilder:object:generate=true
6278

6379
// Legacy placement spec. Components may include this into their spec.

pkg/types/errors.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and component-operator-runtime contributors
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package types
7+
8+
import "time"
9+
10+
type RetriableError struct {
11+
err error
12+
retryAfter *time.Duration
13+
}
14+
15+
func NewRetriableError(err error, retryAfter *time.Duration) RetriableError {
16+
return RetriableError{err: err, retryAfter: retryAfter}
17+
}
18+
19+
func (e RetriableError) Error() string {
20+
return e.err.Error()
21+
}
22+
23+
func (e RetriableError) Unwrap() error {
24+
return e.err
25+
}
26+
27+
func (e RetriableError) Cause() error {
28+
return e.err
29+
}
30+
31+
func (e RetriableError) RetryAfter() *time.Duration {
32+
return e.retryAfter
33+
}

0 commit comments

Comments
 (0)