@@ -6,9 +6,11 @@ import (
6
6
"errors"
7
7
"fmt"
8
8
"github.com/harness/ff-golang-server-sdk/sdk_codes"
9
+ "golang.org/x/sync/errgroup"
9
10
"log"
10
11
"math/rand"
11
12
"net/http"
13
+ "strconv"
12
14
"strings"
13
15
"sync"
14
16
"sync/atomic"
@@ -53,6 +55,7 @@ type CfClient struct {
53
55
token string
54
56
streamConnected bool
55
57
streamConnectedLock sync.RWMutex
58
+ streamDisconnected chan struct {}
56
59
authenticated chan struct {}
57
60
postEvalChan chan evaluation.PostEvalData
58
61
initializedBool bool
@@ -78,16 +81,17 @@ func NewCfClient(sdkKey string, options ...ConfigOption) (*CfClient, error) {
78
81
analyticsService := analyticsservice .NewAnalyticsService (time .Minute , config .Logger )
79
82
80
83
client := & CfClient {
81
- sdkKey : sdkKey ,
82
- config : config ,
83
- authenticated : make (chan struct {}),
84
- analyticsService : analyticsService ,
85
- clusterIdentifier : "1" ,
86
- postEvalChan : make (chan evaluation.PostEvalData ),
87
- stop : make (chan struct {}),
88
- stopped : newAtomicBool (false ),
89
- initialized : make (chan struct {}),
90
- initializedErr : make (chan error ),
84
+ sdkKey : sdkKey ,
85
+ config : config ,
86
+ authenticated : make (chan struct {}),
87
+ analyticsService : analyticsService ,
88
+ clusterIdentifier : "1" ,
89
+ postEvalChan : make (chan evaluation.PostEvalData ),
90
+ stop : make (chan struct {}),
91
+ stopped : newAtomicBool (false ),
92
+ initialized : make (chan struct {}),
93
+ initializedErr : make (chan error ),
94
+ streamDisconnected : make (chan struct {}),
91
95
}
92
96
93
97
if sdkKey == "" {
@@ -154,6 +158,9 @@ func (c *CfClient) start() {
154
158
}()
155
159
go c .setAnalyticsServiceClient (ctx )
156
160
go c .pullCronJob (ctx )
161
+ if c .config .enableStream {
162
+ go c .stream (ctx )
163
+ }
157
164
}
158
165
159
166
// PostEvaluateProcessor push the data to the analytics service
@@ -186,47 +193,52 @@ func (c *CfClient) IsInitialized() (bool, error) {
186
193
return false , InitializeTimeoutError {}
187
194
}
188
195
189
- func (c * CfClient ) retrieve (ctx context.Context ) bool {
190
- ok := true
191
- var wg sync. WaitGroup
192
- wg . Add ( 2 )
193
- go func () {
194
- defer wg . Done ()
195
- rCtx , cancel := context . WithTimeout ( ctx , time . Minute )
196
- defer cancel ()
196
+ func (c * CfClient ) retrieve (ctx context.Context ) {
197
+ var g errgroup. Group
198
+
199
+ rCtx , cancel := context . WithTimeout ( ctx , time . Minute )
200
+ defer cancel ()
201
+
202
+ // First goroutine for retrieving flags.
203
+ g . Go ( func () error {
197
204
err := c .retrieveFlags (rCtx )
198
205
if err != nil {
199
- ok = false
200
- c . config . Logger . Errorf ( "error while retrieving flags: %v" , err . Error ())
206
+ c . config . Logger . Errorf ( "error while retrieving flags: %v" , err )
207
+ return err
201
208
}
202
- }()
209
+ return nil
210
+ })
203
211
204
- go func () {
205
- defer wg .Done ()
206
- rCtx , cancel := context .WithTimeout (ctx , time .Minute )
207
- defer cancel ()
212
+ // Second goroutine for retrieving segments.
213
+ g .Go (func () error {
208
214
err := c .retrieveSegments (rCtx )
209
215
if err != nil {
210
- ok = false
211
- c . config . Logger . Errorf ( "error while retrieving segments: %v" , err . Error ())
216
+ c . config . Logger . Errorf ( "error while retrieving segments: %v" , err )
217
+ return err
212
218
}
213
- }()
214
- wg .Wait ()
215
- if ok {
216
- c .config .Logger .Info ("Data poll finished successfully" )
219
+ return nil
220
+ })
221
+
222
+ err := g .Wait ()
223
+
224
+ if err != nil {
225
+ // We just log the error and continue. In the case of initialization, this means we mark the client as initialized
226
+ // if we can't poll for initial state, and default evaluations are likely to be returned.
227
+ c .config .Logger .Errorf ("Data poll finished with errors: %s" , err )
217
228
} else {
218
- c .config .Logger .Error ("Data poll finished with errors " )
229
+ c .config .Logger .Info ("Data poll finished successfully " )
219
230
}
220
231
221
- if ok {
222
- // This flag is used by `IsInitialized` so set to true.
223
- c .initializedBoolLock .Lock ()
224
- c .initializedBool = true
225
- c .initializedBoolLock .Unlock ()
232
+ c .initializedBoolLock .Lock ()
233
+ defer c .initializedBoolLock .Unlock ()
226
234
235
+ // This function is used to mark the client as "initialized" once flags and segments have been loaded,
236
+ // but it's also used for the polling thread, so we check if the client is already initialized before
237
+ // marking it as such.
238
+ if ! c .initializedBool {
239
+ c .initializedBool = true
227
240
close (c .initialized )
228
241
}
229
- return ok
230
242
}
231
243
232
244
func (c * CfClient ) streamConnect (ctx context.Context ) {
@@ -247,28 +259,12 @@ func (c *CfClient) streamConnect(ctx context.Context) {
247
259
// Use the SDKs http client
248
260
sseClient .Connection = c .config .httpClient
249
261
250
- streamErr := func () {
251
- c .config .Logger .Warnf ("%s Stream disconnected. Swapping to polling mode" , sdk_codes .StreamDisconnected )
252
- c .mux .RLock ()
253
- defer c .mux .RUnlock ()
254
- c .streamConnected = false
255
-
256
- // If an eventStreamListener has been passed to the Proxy lets notify it of the disconnected
257
- // to let it know something is up with the stream it has been listening to
258
- if c .config .eventStreamListener != nil {
259
- c .config .eventStreamListener .Pub (context .Background (), stream.Event {
260
- APIKey : c .sdkKey ,
261
- Environment : c .environmentID ,
262
- Err : stream .ErrStreamDisconnect ,
263
- })
264
- }
265
- }
266
- conn := stream .NewSSEClient (c .sdkKey , c .token , sseClient , c .repository , c .api , c .config .Logger , streamErr ,
267
- c .config .eventStreamListener , c .config .proxyMode )
262
+ conn := stream .NewSSEClient (c .sdkKey , c .token , sseClient , c .repository , c .api , c .config .Logger ,
263
+ c .config .eventStreamListener , c .config .proxyMode , c .streamDisconnected )
268
264
269
265
// Connect kicks off a goroutine that attempts to establish a stream connection
270
266
// while this is happening we set streamConnected to true - if any errors happen
271
- // in this process streamConnected will be set back to false by the streamErr function
267
+ // in this process streamConnected will be set back to false by the streamDisconnected function
272
268
conn .Connect (ctx , c .environmentID , c .sdkKey )
273
269
c .streamConnected = true
274
270
}
@@ -303,7 +299,14 @@ func (c *CfClient) initAuthentication(ctx context.Context) error {
303
299
jitter := time .Duration (rand .Float64 () * float64 (currentDelay ))
304
300
delayWithJitter := currentDelay + jitter
305
301
306
- c .config .Logger .Errorf ("%s Authentication failed with error: '%s'. Retrying in %v." , sdk_codes .AuthAttempt , err , delayWithJitter )
302
+ maxAttemptLog := ""
303
+ if c .config .maxAuthRetries == - 1 {
304
+ maxAttemptLog = "∞"
305
+ } else {
306
+ maxAttemptLog = strconv .Itoa (c .config .maxAuthRetries )
307
+ }
308
+
309
+ c .config .Logger .Errorf ("%s Authentication attempt %d of %s failed with error: '%s'. Retrying in %v." , sdk_codes .AuthAttempt , attempts , maxAttemptLog , err , delayWithJitter )
307
310
c .config .sleeper .Sleep (delayWithJitter )
308
311
309
312
currentDelay *= time .Duration (factor )
@@ -321,7 +324,7 @@ func (c *CfClient) authenticate(ctx context.Context) error {
321
324
defer c .mux .RUnlock ()
322
325
323
326
// dont check err just retry
324
- httpClient , err := rest .NewClientWithResponses (c .config .url , rest .WithHTTPClient (c .config .httpClient ))
327
+ httpClient , err := rest .NewClientWithResponses (c .config .url , rest .WithHTTPClient (c .config .authHttpClient ))
325
328
if err != nil {
326
329
return err
327
330
}
@@ -420,25 +423,64 @@ func (c *CfClient) makeTicker(interval uint) *time.Ticker {
420
423
return time .NewTicker (time .Second * time .Duration (interval ))
421
424
}
422
425
426
+ func (c * CfClient ) stream (ctx context.Context ) {
427
+ // wait until initialized with initial state
428
+ <- c .initialized
429
+ c .config .Logger .Infof ("%s Polling Stopped" , sdk_codes .PollStop )
430
+ c .config .Logger .Info ("Attempting to start stream" )
431
+ c .streamConnect (ctx )
432
+
433
+ const maxBackoffDuration = 2 * time .Minute
434
+ backoffDuration := 2 * time .Second
435
+ for {
436
+ select {
437
+ case <- ctx .Done ():
438
+ c .config .Logger .Infof ("%s Stream stopped" , sdk_codes .StreamStop )
439
+ return
440
+ case <- c .streamDisconnected :
441
+ c .config .Logger .Warnf ("%s Stream disconnected. Swapping to polling mode" , sdk_codes .StreamDisconnected )
442
+ c .mux .RLock ()
443
+ c .streamConnected = false
444
+ c .mux .RUnlock ()
445
+
446
+ // If an eventStreamListener has been passed to the Proxy lets notify it of the disconnected
447
+ // to let it know something is up with the stream it has been listening to
448
+ if c .config .eventStreamListener != nil {
449
+ c .config .eventStreamListener .Pub (context .Background (), stream.Event {
450
+ APIKey : c .sdkKey ,
451
+ Environment : c .environmentID ,
452
+ Err : stream .ErrStreamDisconnect ,
453
+ })
454
+ }
455
+
456
+ time .Sleep (backoffDuration )
457
+
458
+ c .config .Logger .Info ("Attempting to restart stream" )
459
+ c .streamConnect (ctx )
460
+
461
+ if backoffDuration > maxBackoffDuration {
462
+ backoffDuration = maxBackoffDuration
463
+ return
464
+ }
465
+
466
+ backoffDuration *= 2
467
+
468
+ }
469
+ }
470
+ }
471
+
423
472
func (c * CfClient ) pullCronJob (ctx context.Context ) {
424
473
poll := func () {
425
474
c .mux .RLock ()
426
- c . config . Logger . Infof ( "%s Polling started, interval: %v" , sdk_codes . PollStart , c . config . pullInterval )
475
+ defer c . mux . RUnlock ( )
427
476
if ! c .streamConnected {
428
- ok := c .retrieve (ctx )
429
- // we should only try and start the stream after the poll succeeded to make sure we get the latest changes
430
- if ok && c .config .enableStream {
431
- c .config .Logger .Infof ("%s Polling Stopped" , sdk_codes .PollStop )
432
- // here stream is enabled but not connected, so we attempt to reconnect
433
- c .config .Logger .Info ("Attempting to start stream" )
434
- c .streamConnect (ctx )
435
- }
477
+ c .retrieve (ctx )
436
478
}
437
- c .mux .RUnlock ()
438
479
}
439
480
// wait until authenticated
440
481
<- c .authenticated
441
482
483
+ c .config .Logger .Infof ("%s Polling started, interval: %v seconds" , sdk_codes .PollStart , c .config .pullInterval )
442
484
// pull initial data
443
485
poll ()
444
486
@@ -449,7 +491,6 @@ func (c *CfClient) pullCronJob(ctx context.Context) {
449
491
case <- ctx .Done ():
450
492
pullingTicker .Stop ()
451
493
c .config .Logger .Infof ("%s Polling stopped" , sdk_codes .PollStop )
452
- c .config .Logger .Infof ("%s Stream stopped" , sdk_codes .StreamStop )
453
494
return
454
495
case <- pullingTicker .C :
455
496
poll ()
@@ -471,7 +512,7 @@ func (c *CfClient) retrieveFlags(ctx context.Context) error {
471
512
}
472
513
473
514
if flags .JSON200 == nil {
474
- return nil
515
+ return fmt . Errorf ( "%w: `%v`" , FetchFlagsError , flags . HTTPResponse . Status )
475
516
}
476
517
477
518
c .repository .SetFlags (true , c .environmentID , * flags .JSON200 ... )
0 commit comments