Skip to content

Commit 3408b8a

Browse files
authored
FFM-9485 SSE Enhancements #2 (#134)
1 parent a551d0c commit 3408b8a

File tree

8 files changed

+256
-155
lines changed

8 files changed

+256
-155
lines changed

client/client.go

Lines changed: 125 additions & 105 deletions
Large diffs are not rendered by default.

client/client_test.go

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"encoding/json"
66
"errors"
7+
"github.com/cenkalti/backoff/v4"
78
"github.com/harness/ff-golang-server-sdk/dto"
89
"github.com/harness/ff-golang-server-sdk/evaluation"
910
"github.com/harness/ff-golang-server-sdk/log"
@@ -16,6 +17,7 @@ import (
1617
"net/http"
1718
"os"
1819
"testing"
20+
"time"
1921
)
2022

2123
const (
@@ -180,7 +182,7 @@ func TestCfClient_NewClient(t *testing.T) {
180182
{
181183
name: "Synchronous client: Authentication failed with 500 and succeeds after one retry",
182184
newClientFunc: func() (*CfClient, error) {
183-
return newClient(http.DefaultClient, ValidSDKKey, WithWaitForInitialized(true), WithSleeper(test_helpers.MockSleeper{}))
185+
return newClient(http.DefaultClient, ValidSDKKey, WithWaitForInitialized(true), WithAuthRetryStrategy(getInstantRetryStrategy()), WithSleeper(test_helpers.MockSleeper{}))
184186
},
185187
mockResponder: func() {
186188
bodyString := `{
@@ -198,7 +200,7 @@ func TestCfClient_NewClient(t *testing.T) {
198200
{
199201
name: "Synchronous client: Authentication failed and succeeds just before exceeding max retries",
200202
newClientFunc: func() (*CfClient, error) {
201-
newClient, err := newClient(http.DefaultClient, ValidSDKKey, WithWaitForInitialized(true), WithMaxAuthRetries(10), WithSleeper(test_helpers.MockSleeper{}))
203+
newClient, err := newClient(http.DefaultClient, ValidSDKKey, WithWaitForInitialized(true), WithMaxAuthRetries(10), WithAuthRetryStrategy(getInstantRetryStrategy()), WithSleeper(test_helpers.MockSleeper{}))
202204
return newClient, err
203205
},
204206
mockResponder: func() {
@@ -208,7 +210,7 @@ func TestCfClient_NewClient(t *testing.T) {
208210
}`
209211
var responses []httpmock.Responder
210212
// Add a bunch of error responses
211-
for i := 0; i < 10; i++ {
213+
for i := 0; i < 9; i++ {
212214
responses = append(responses, AuthResponseDetailed(500, "internal server error", bodyString))
213215
}
214216

@@ -224,7 +226,7 @@ func TestCfClient_NewClient(t *testing.T) {
224226
{
225227
name: "Synchronous client: Authentication failed and exceeds max retries",
226228
newClientFunc: func() (*CfClient, error) {
227-
newClient, err := newClient(http.DefaultClient, ValidSDKKey, WithWaitForInitialized(true), WithMaxAuthRetries(10), WithSleeper(test_helpers.MockSleeper{}))
229+
newClient, err := newClient(http.DefaultClient, ValidSDKKey, WithWaitForInitialized(true), WithMaxAuthRetries(10), WithAuthRetryStrategy(getInstantRetryStrategy()), WithSleeper(test_helpers.MockSleeper{}))
228230
return newClient, err
229231
},
230232
mockResponder: func() {
@@ -234,7 +236,7 @@ func TestCfClient_NewClient(t *testing.T) {
234236
}`
235237
var responses []httpmock.Responder
236238
// Add a bunch of error responses
237-
for i := 0; i < 11; i++ {
239+
for i := 0; i < 10; i++ {
238240
responses = append(responses, AuthResponseDetailed(500, "internal server error", bodyString))
239241
}
240242

@@ -503,7 +505,7 @@ func TestCfClient_DefaultVariationReturned(t *testing.T) {
503505
{
504506
name: "Evaluations with Synchronous client with a server error",
505507
clientFunc: func() (*CfClient, error) {
506-
return newClient(http.DefaultClient, ValidSDKKey, WithWaitForInitialized(true), WithMaxAuthRetries(2), WithSleeper(test_helpers.MockSleeper{}))
508+
return newClient(http.DefaultClient, ValidSDKKey, WithWaitForInitialized(true), WithMaxAuthRetries(2), WithAuthRetryStrategy(getInstantRetryStrategy()), WithSleeper(test_helpers.MockSleeper{}))
507509
},
508510
mockResponder: func() {
509511
bodyString := `{
@@ -558,7 +560,7 @@ func TestCfClient_DefaultVariationReturned(t *testing.T) {
558560
{
559561
name: "Evaluations with Async client with a server error",
560562
clientFunc: func() (*CfClient, error) {
561-
return newClient(http.DefaultClient, ValidSDKKey, WithMaxAuthRetries(2), WithSleeper(test_helpers.MockSleeper{}))
563+
return newClient(http.DefaultClient, ValidSDKKey, WithMaxAuthRetries(2), WithAuthRetryStrategy(getInstantRetryStrategy()), WithSleeper(test_helpers.MockSleeper{}))
562564
},
563565
mockResponder: func() {
564566
bodyString := `{
@@ -759,3 +761,12 @@ func TestCfClient_Close(t *testing.T) {
759761
t.Log("When I close the client for the second time I should an error")
760762
assert.NotNil(t, client.Close())
761763
}
764+
765+
// getInstantRetryStrategy returns a strategy that retries every millisecond for testing purposes
766+
func getInstantRetryStrategy() *backoff.ExponentialBackOff {
767+
exponentialBackOff := backoff.NewExponentialBackOff()
768+
exponentialBackOff.InitialInterval = 1 * time.Millisecond
769+
exponentialBackOff.MaxInterval = 1 * time.Millisecond
770+
exponentialBackOff.Multiplier = 0
771+
return exponentialBackOff
772+
}

client/config.go

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package client
22

33
import (
44
"fmt"
5+
"github.com/cenkalti/backoff/v4"
56
"github.com/harness/ff-golang-server-sdk/cache"
67
"github.com/harness/ff-golang-server-sdk/evaluation"
78
"github.com/harness/ff-golang-server-sdk/logger"
@@ -15,23 +16,25 @@ import (
1516
)
1617

1718
type config struct {
18-
url string
19-
eventsURL string
20-
pullInterval uint // in seconds
21-
Cache cache.Cache
22-
Store storage.Storage
23-
Logger logger.Logger
24-
httpClient *http.Client
25-
authHttpClient *http.Client
26-
enableStream bool
27-
enableStore bool
28-
target evaluation.Target
29-
eventStreamListener stream.EventStreamListener
30-
enableAnalytics bool
31-
proxyMode bool
32-
waitForInitialized bool
33-
maxAuthRetries int
34-
sleeper types.Sleeper
19+
url string
20+
eventsURL string
21+
pullInterval uint // in seconds
22+
Cache cache.Cache
23+
Store storage.Storage
24+
Logger logger.Logger
25+
httpClient *http.Client
26+
authHttpClient *http.Client
27+
enableStream bool
28+
enableStore bool
29+
target evaluation.Target
30+
eventStreamListener stream.EventStreamListener
31+
enableAnalytics bool
32+
proxyMode bool
33+
waitForInitialized bool
34+
maxAuthRetries int
35+
authRetryStrategy *backoff.ExponentialBackOff
36+
streamingRetryStrategy *backoff.ExponentialBackOff
37+
sleeper types.Sleeper
3538
}
3639

3740
func newDefaultConfig(log logger.Logger) *config {
@@ -41,9 +44,8 @@ func newDefaultConfig(log logger.Logger) *config {
4144
defaultStore = storage.NewFileStore("defaultProject", storage.GetHarnessDir(log), log)
4245
}
4346

44-
const requestTimeout = time.Second * 30
45-
4647
// Authentication uses a default http client + timeout as we have our own custom retry logic for authentication.
48+
const requestTimeout = time.Second * 30
4749
authHttpClient := &http.Client{}
4850
authHttpClient.Timeout = requestTimeout
4951

@@ -83,7 +85,17 @@ func newDefaultConfig(log logger.Logger) *config {
8385
enableAnalytics: true,
8486
proxyMode: false,
8587
// Indicate that we should retry forever by default
86-
maxAuthRetries: -1,
87-
sleeper: &types.RealClock{},
88+
maxAuthRetries: -1,
89+
authRetryStrategy: getDefaultExpBackoff(),
90+
streamingRetryStrategy: getDefaultExpBackoff(),
91+
sleeper: &types.RealClock{},
8892
}
8993
}
94+
95+
func getDefaultExpBackoff() *backoff.ExponentialBackOff {
96+
exponentialBackOff := backoff.NewExponentialBackOff()
97+
exponentialBackOff.InitialInterval = 1 * time.Second
98+
exponentialBackOff.MaxInterval = 1 * time.Minute
99+
exponentialBackOff.Multiplier = 2.0
100+
return exponentialBackOff
101+
}

client/options.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package client
22

33
import (
4+
"github.com/cenkalti/backoff/v4"
45
"github.com/harness/ff-golang-server-sdk/cache"
56
"github.com/harness/ff-golang-server-sdk/evaluation"
67
"github.com/harness/ff-golang-server-sdk/logger"
@@ -113,18 +114,29 @@ func WithProxyMode(b bool) ConfigOption {
113114
}
114115
}
115116

117+
// WithWaitForInitialized configures the SDK to block the thread until initialization succeeds or fails
116118
func WithWaitForInitialized(b bool) ConfigOption {
117119
return func(config *config) {
118120
config.waitForInitialized = b
119121
}
120122
}
121123

124+
// WithMaxAuthRetries sets how many times the SDK will retry if authentication fails
122125
func WithMaxAuthRetries(i int) ConfigOption {
123126
return func(config *config) {
124127
config.maxAuthRetries = i
125128
}
126129
}
127130

131+
// WithAuthRetryStrategy sets the backoff and retry strategy for client authentication requests
132+
// Mainly used for testing purposes, as the SDKs default backoff strategy should be sufficient for most if not all scenarios.
133+
func WithAuthRetryStrategy(retryStrategy *backoff.ExponentialBackOff) ConfigOption {
134+
return func(config *config) {
135+
config.authRetryStrategy = retryStrategy
136+
}
137+
}
138+
139+
// WithSleeper is used to aid in testing functionality that sleeps
128140
func WithSleeper(sleeper types.Sleeper) ConfigOption {
129141
return func(config *config) {
130142
config.sleeper = sleeper

go.mod

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,31 @@ module github.com/harness/ff-golang-server-sdk
33
go 1.18
44

55
require (
6+
github.com/cenkalti/backoff/v4 v4.2.1
67
github.com/deepmap/oapi-codegen v1.11.0
78
github.com/getkin/kin-openapi v0.94.0
89
github.com/golang-jwt/jwt v3.2.2+incompatible
910
github.com/google/uuid v1.3.0
10-
github.com/hashicorp/go-retryablehttp v0.6.8
11+
github.com/harness-community/sse/v3 v3.1.0
12+
github.com/hashicorp/go-retryablehttp v0.7.4
1113
github.com/hashicorp/golang-lru v0.5.4
1214
github.com/jarcoal/httpmock v1.0.8
1315
github.com/json-iterator/go v1.1.12
1416
github.com/mitchellh/go-homedir v1.1.0
1517
github.com/mitchellh/mapstructure v1.3.3
16-
github.com/r3labs/sse v0.0.0-20201126193848-34e640891548
1718
github.com/spaolacci/murmur3 v1.1.0
1819
github.com/stretchr/testify v1.7.1
1920
go.uber.org/zap v1.16.0
2021
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
21-
gopkg.in/cenkalti/backoff.v1 v1.1.0
22+
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
2223
)
2324

2425
require (
2526
github.com/davecgh/go-spew v1.1.1 // indirect
2627
github.com/ghodss/yaml v1.0.0 // indirect
2728
github.com/go-openapi/jsonpointer v0.19.5 // indirect
2829
github.com/go-openapi/swag v0.21.1 // indirect
29-
github.com/hashicorp/go-cleanhttp v0.5.1 // indirect
30+
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
3031
github.com/josharian/intern v1.0.0 // indirect
3132
github.com/mailru/easyjson v0.7.7 // indirect
3233
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
@@ -35,6 +36,7 @@ require (
3536
go.uber.org/atomic v1.7.0 // indirect
3637
go.uber.org/multierr v1.6.0 // indirect
3738
golang.org/x/net v0.7.0 // indirect
39+
gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect
3840
gopkg.in/yaml.v2 v2.4.0 // indirect
3941
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
4042
)

go.sum

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
22
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
3+
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
4+
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
35
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
46
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
57
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -42,12 +44,14 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
4244
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
4345
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
4446
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
45-
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
46-
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
47+
github.com/harness-community/sse/v3 v3.1.0 h1:uaLxXzC9DjpWEV/qTYU3uJV3eLMTRhMY2P6qb/3QAeY=
48+
github.com/harness-community/sse/v3 v3.1.0/go.mod h1:v4ft76Eaj+kAsUcc29zIspInWgpzsMLlHLb4x/PYVX0=
49+
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
50+
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
4751
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
4852
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
49-
github.com/hashicorp/go-retryablehttp v0.6.8 h1:92lWxgpa+fF3FozM4B3UZtHZMJX8T5XT+TFdCxsPyWs=
50-
github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
53+
github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA=
54+
github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
5155
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
5256
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
5357
github.com/jarcoal/httpmock v1.0.8 h1:8kI16SoO6LQKgPE7PvQuV+YuD/inwHd7fOOe2zMbo4k=
@@ -109,8 +113,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
109113
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
110114
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
111115
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
112-
github.com/r3labs/sse v0.0.0-20201126193848-34e640891548 h1:ewzX4RiFeFXl8APBmMqXBXR5CZoF/jctB71BuLg7d3s=
113-
github.com/r3labs/sse v0.0.0-20201126193848-34e640891548/go.mod h1:S8xSOnV3CgpNrWd0GQ/OoQfMtlg2uPRSuTzcSGrzwK8=
114116
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
115117
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
116118
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
@@ -171,6 +173,7 @@ golang.org/x/net v0.0.0-20220513224357-95641704303c/go.mod h1:CfG3xpIq0wQ8r1q4Su
171173
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
172174
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
173175
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
176+
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
174177
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
175178
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
176179
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

0 commit comments

Comments
 (0)