Skip to content

Commit b2d5090

Browse files
committed
feat: exponential backoff for flagd-proxy reconciliation
Signed-off-by: Matthias Riegler <[email protected]>
1 parent dce4f17 commit b2d5090

File tree

6 files changed

+100
-12
lines changed

6 files changed

+100
-12
lines changed

common/utils/utils.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package utils
33
import (
44
"fmt"
55
"strings"
6+
"sync/atomic"
7+
"time"
68
)
79

810
func TrueVal() *bool {
@@ -41,3 +43,23 @@ func FeatureFlagId(namespace, name string) string {
4143
func FeatureFlagConfigMapKey(namespace, name string) string {
4244
return fmt.Sprintf("%s.flagd.json", FeatureFlagId(namespace, name))
4345
}
46+
47+
type ExponentialBackoff struct {
48+
StartDelay time.Duration
49+
MaxDelay time.Duration
50+
counter int64
51+
}
52+
53+
func (e *ExponentialBackoff) Next() time.Duration {
54+
val := atomic.AddInt64(&e.counter, 1)
55+
56+
delay := e.StartDelay * (1 << (val - 1))
57+
if delay > e.MaxDelay {
58+
delay = e.MaxDelay
59+
}
60+
return delay
61+
}
62+
63+
func (e *ExponentialBackoff) Reset() {
64+
e.counter = 0
65+
}

common/utils/utils_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package utils
22

33
import (
44
"testing"
5+
"time"
56

67
"github.com/stretchr/testify/require"
78
)
@@ -39,3 +40,47 @@ func Test_ParseAnnotations(t *testing.T) {
3940
require.Equal(t, "default", s1)
4041
require.Equal(t, "anno", s2)
4142
}
43+
44+
func TestExponentialBackoff_Next(t *testing.T) {
45+
tests := []struct {
46+
name string
47+
startDelay time.Duration
48+
maxDelay time.Duration
49+
steps int
50+
expected time.Duration
51+
}{
52+
{name: "basic backoff", startDelay: 1 * time.Second, maxDelay: 16 * time.Second, steps: 3, expected: 4 * time.Second},
53+
{name: "max delay reached", startDelay: 1 * time.Second, maxDelay: 5 * time.Second, steps: 10, expected: 5 * time.Second},
54+
{name: "single step", startDelay: 500 * time.Millisecond, maxDelay: 10 * time.Second, steps: 1, expected: 500 * time.Millisecond},
55+
}
56+
57+
for _, tt := range tests {
58+
t.Run(tt.name, func(t *testing.T) {
59+
backoff := &ExponentialBackoff{StartDelay: tt.startDelay, MaxDelay: tt.maxDelay}
60+
var result time.Duration
61+
for i := 0; i < tt.steps; i++ {
62+
result = backoff.Next()
63+
}
64+
if result != tt.expected {
65+
t.Errorf("Expected delay after %d steps to be %v; got %v", tt.steps, tt.expected, result)
66+
}
67+
})
68+
}
69+
}
70+
71+
func TestExponentialBackoff_Reset(t *testing.T) {
72+
backoff := &ExponentialBackoff{StartDelay: 1 * time.Second, MaxDelay: 10 * time.Second}
73+
74+
// Increment the backoff a few times
75+
backoff.Next()
76+
backoff.Next()
77+
78+
// Reset and check the counter
79+
backoff.Reset()
80+
if backoff.counter != 0 {
81+
t.Errorf("Expected counter to be reset to 0; got %d", backoff.counter)
82+
}
83+
if backoff.Next() != 1*time.Second {
84+
t.Errorf("Expected delay after reset to be %v; got %v", 1*time.Second, backoff.Next())
85+
}
86+
}

controllers/core/featureflagsource/controller.go

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,27 @@ import (
2626
api "github.com/open-feature/open-feature-operator/apis/core/v1beta1"
2727
"github.com/open-feature/open-feature-operator/common"
2828
"github.com/open-feature/open-feature-operator/common/flagdproxy"
29+
"github.com/open-feature/open-feature-operator/common/utils"
2930
appsV1 "k8s.io/api/apps/v1"
3031
"k8s.io/apimachinery/pkg/api/errors"
3132
"k8s.io/apimachinery/pkg/runtime"
3233
ctrl "sigs.k8s.io/controller-runtime"
3334
"sigs.k8s.io/controller-runtime/pkg/builder"
3435
"sigs.k8s.io/controller-runtime/pkg/client"
3536
"sigs.k8s.io/controller-runtime/pkg/predicate"
37+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3638
)
3739

3840
// FeatureFlagSourceReconciler reconciles a FeatureFlagSource object
3941
type FeatureFlagSourceReconciler struct {
4042
client.Client
4143
Scheme *runtime.Scheme
4244
// ReqLogger contains the Logger of this controller
43-
Log logr.Logger
44-
FlagdProxy *flagdproxy.FlagdProxyHandler
45+
Log logr.Logger
46+
47+
// FlagdProxy is the handler for the flagd-proxy deployment
48+
FlagdProxy *flagdproxy.FlagdProxyHandler
49+
FlagdProxyBackoff *utils.ExponentialBackoff
4550
}
4651

4752
// renovate: datasource=github-tags depName=open-feature/flagd/flagd-proxy
@@ -73,13 +78,21 @@ func (r *FeatureFlagSourceReconciler) Reconcile(ctx context.Context, req ctrl.Re
7378
return r.finishReconcile(err, false)
7479
}
7580

81+
needsFlagdProxy := false
7682
for _, source := range fsConfig.Spec.Sources {
7783
if source.Provider.IsFlagdProxy() {
78-
r.Log.Info(fmt.Sprintf("featureflagsource %s uses flagd-proxy, checking deployment", req.NamespacedName))
79-
if err := r.FlagdProxy.HandleFlagdProxy(ctx); err != nil {
80-
r.Log.Error(err, "error handling the flagd-proxy deployment")
81-
}
82-
break
84+
r.Log.Info(fmt.Sprintf("featureflagsource %s requires flagd-proxy", req.NamespacedName))
85+
needsFlagdProxy = true
86+
}
87+
}
88+
89+
if needsFlagdProxy {
90+
r.Log.Info(fmt.Sprintf("featureflagsource %s uses flagd-proxy, checking deployment", req.NamespacedName))
91+
if err := r.FlagdProxy.HandleFlagdProxy(ctx); err != nil {
92+
r.Log.Error(err, "error handling the flagd-proxy deployment")
93+
return reconcile.Result{RequeueAfter: r.FlagdProxyBackoff.Next()}, err
94+
} else {
95+
r.FlagdProxyBackoff.Reset()
8396
}
8497
}
8598

controllers/core/featureflagsource/controller_test.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/open-feature/open-feature-operator/common"
1212
"github.com/open-feature/open-feature-operator/common/flagdproxy"
1313
commontypes "github.com/open-feature/open-feature-operator/common/types"
14+
"github.com/open-feature/open-feature-operator/common/utils"
1415
"github.com/stretchr/testify/require"
1516
appsv1 "k8s.io/api/apps/v1"
1617
corev1 "k8s.io/api/core/v1"
@@ -113,10 +114,11 @@ func TestFeatureFlagSourceReconciler_Reconcile(t *testing.T) {
113114
)
114115

115116
r := &FeatureFlagSourceReconciler{
116-
Client: fakeClient,
117-
Log: ctrl.Log.WithName("featureflagsource-controller"),
118-
Scheme: fakeClient.Scheme(),
119-
FlagdProxy: kph,
117+
Client: fakeClient,
118+
Log: ctrl.Log.WithName("featureflagsource-controller"),
119+
Scheme: fakeClient.Scheme(),
120+
FlagdProxy: kph,
121+
FlagdProxyBackoff: &utils.ExponentialBackoff{StartDelay: time.Duration(0), MaxDelay: time.Duration(0)},
120122
}
121123

122124
if tt.deployment != nil {

controllers/core/flagd/controller_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99

1010
"github.com/golang/mock/gomock"
1111
api "github.com/open-feature/open-feature-operator/apis/core/v1beta1"
12-
"github.com/open-feature/open-feature-operator/controllers/core/flagd/common"
12+
resources "github.com/open-feature/open-feature-operator/controllers/core/flagd/common"
1313
commonmock "github.com/open-feature/open-feature-operator/controllers/core/flagd/mock"
1414
resourcemock "github.com/open-feature/open-feature-operator/controllers/core/flagd/resources/mock"
1515
"github.com/stretchr/testify/require"

main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,15 @@ import (
2323
"log"
2424
"os"
2525
"strings"
26+
"time"
2627

2728
"github.com/kelseyhightower/envconfig"
2829
corev1beta1 "github.com/open-feature/open-feature-operator/apis/core/v1beta1"
2930
"github.com/open-feature/open-feature-operator/common"
3031
"github.com/open-feature/open-feature-operator/common/flagdinjector"
3132
"github.com/open-feature/open-feature-operator/common/flagdproxy"
3233
"github.com/open-feature/open-feature-operator/common/types"
34+
"github.com/open-feature/open-feature-operator/common/utils"
3335
"github.com/open-feature/open-feature-operator/controllers/core/featureflagsource"
3436
"github.com/open-feature/open-feature-operator/controllers/core/flagd"
3537
flagdresources "github.com/open-feature/open-feature-operator/controllers/core/flagd/resources"
@@ -228,6 +230,10 @@ func main() {
228230
Scheme: mgr.GetScheme(),
229231
Log: ctrl.Log.WithName("FeatureFlagSource Controller"),
230232
FlagdProxy: kph,
233+
FlagdProxyBackoff: &utils.ExponentialBackoff{
234+
StartDelay: time.Second,
235+
MaxDelay: time.Minute,
236+
},
231237
}
232238
if err = flagSourceController.SetupWithManager(mgr); err != nil {
233239
setupLog.Error(err, "unable to create controller", "controller", "FeatureFlagSource")

0 commit comments

Comments
 (0)