16
16
import dev .openfeature .contrib .providers .gofeatureflag .events .EventsPublisher ;
17
17
import dev .openfeature .contrib .providers .gofeatureflag .exception .InvalidEndpoint ;
18
18
import dev .openfeature .contrib .providers .gofeatureflag .exception .InvalidOptions ;
19
+ import dev .openfeature .contrib .providers .gofeatureflag .exception .NotPresentInCache ;
19
20
import dev .openfeature .sdk .ErrorCode ;
20
21
import dev .openfeature .sdk .EvaluationContext ;
21
22
import dev .openfeature .sdk .FeatureProvider ;
34
35
import dev .openfeature .sdk .exceptions .TypeMismatchError ;
35
36
import lombok .AccessLevel ;
36
37
import lombok .Getter ;
37
- import lombok .SneakyThrows ;
38
38
import lombok .extern .slf4j .Slf4j ;
39
39
import okhttp3 .ConnectionPool ;
40
40
import okhttp3 .HttpUrl ;
54
54
import java .util .function .Consumer ;
55
55
import java .util .stream .Collectors ;
56
56
57
-
58
57
import static java .net .HttpURLConnection .HTTP_BAD_REQUEST ;
59
58
import static java .net .HttpURLConnection .HTTP_OK ;
60
59
import static java .net .HttpURLConnection .HTTP_UNAUTHORIZED ;
64
63
*/
65
64
@ Slf4j
66
65
public class GoFeatureFlagProvider implements FeatureProvider {
67
- private static final String NAME = "GO Feature Flag Provider" ;
68
- private static final ObjectMapper requestMapper = new ObjectMapper ();
69
- private static final ObjectMapper responseMapper = new ObjectMapper ()
70
- .configure (DeserializationFeature .FAIL_ON_UNKNOWN_PROPERTIES , false );
71
-
72
66
public static final long DEFAULT_FLUSH_INTERVAL_MS = Duration .ofMinutes (1 ).toMillis ();
73
67
public static final int DEFAULT_MAX_PENDING_EVENTS = 10000 ;
74
68
public static final long DEFAULT_CACHE_TTL_MS = 1000 ;
75
69
public static final int DEFAULT_CACHE_CONCURRENCY_LEVEL = 1 ;
76
70
public static final int DEFAULT_CACHE_INITIAL_CAPACITY = 100 ;
77
71
public static final int DEFAULT_CACHE_MAXIMUM_SIZE = 10000 ;
78
72
protected static final String CACHED_REASON = Reason .CACHED .name ();
73
+ private static final String NAME = "GO Feature Flag Provider" ;
74
+ private static final ObjectMapper requestMapper = new ObjectMapper ();
75
+ private static final ObjectMapper responseMapper = new ObjectMapper ()
76
+ .configure (DeserializationFeature .FAIL_ON_UNKNOWN_PROPERTIES , false );
77
+ private final GoFeatureFlagProviderOptions options ;
79
78
private HttpUrl parsedEndpoint ;
80
79
// httpClient is the instance of the OkHttpClient used by the provider
81
80
private OkHttpClient httpClient ;
82
-
83
81
// apiKey contains the token to use while calling GO Feature Flag relay proxy
84
82
private String apiKey ;
85
-
86
83
@ Getter (AccessLevel .PROTECTED )
87
84
private Cache <String , ProviderEvaluation <?>> cache ;
88
-
89
85
@ Getter (AccessLevel .PROTECTED )
90
86
private EventsPublisher <Event > eventsPublisher ;
91
-
92
- private final GoFeatureFlagProviderOptions options ;
93
-
94
87
private ProviderState state = ProviderState .NOT_READY ;
95
88
96
89
/**
@@ -133,12 +126,17 @@ private void validateInputOptions(GoFeatureFlagProviderOptions options) throws I
133
126
}
134
127
}
135
128
129
+ /**
130
+ * buildDefaultCache is building a default cache configuration
131
+ *
132
+ * @return the default cache configuraiton
133
+ */
136
134
private Cache <String , ProviderEvaluation <?>> buildDefaultCache () {
137
135
return CacheBuilder .newBuilder ()
138
- .concurrencyLevel (DEFAULT_CACHE_CONCURRENCY_LEVEL )
139
- .initialCapacity (DEFAULT_CACHE_INITIAL_CAPACITY ).maximumSize (DEFAULT_CACHE_MAXIMUM_SIZE )
140
- .expireAfterWrite (Duration .ofMillis (DEFAULT_CACHE_TTL_MS ))
141
- .build ();
136
+ .concurrencyLevel (DEFAULT_CACHE_CONCURRENCY_LEVEL )
137
+ .initialCapacity (DEFAULT_CACHE_INITIAL_CAPACITY ).maximumSize (DEFAULT_CACHE_MAXIMUM_SIZE )
138
+ .expireAfterWrite (Duration .ofMillis (DEFAULT_CACHE_TTL_MS ))
139
+ .build ();
142
140
}
143
141
144
142
@ Override
@@ -186,6 +184,13 @@ public ProviderEvaluation<Value> getObjectEvaluation(
186
184
return getEvaluation (key , defaultValue , evaluationContext , Value .class );
187
185
}
188
186
187
+ /**
188
+ * buildCacheKey is creating the entry key of the cache
189
+ *
190
+ * @param key - the name of your feature flag
191
+ * @param userKey - a representation of your user
192
+ * @return the cache key
193
+ */
189
194
private String buildCacheKey (String key , String userKey ) {
190
195
return key + "," + userKey ;
191
196
}
@@ -232,13 +237,25 @@ public void initialize(EvaluationContext evaluationContext) throws Exception {
232
237
eventsPublisher = new EventsPublisher <>(publisher , flushIntervalMs , maxPendingEvents );
233
238
}
234
239
state = ProviderState .READY ;
240
+ log .info ("finishing initializing provider, state: {}" , state );
235
241
}
236
242
237
243
@ Override
238
244
public ProviderState getState () {
239
245
return state ;
240
246
}
241
247
248
+ /**
249
+ * getEvaluation is the function resolving the flag, it will 1st check in the cache and if it is not available
250
+ * will call the evaluation endpoint to get the value of the flag
251
+ *
252
+ * @param key - name of the feature flag
253
+ * @param defaultValue - value used if something is not working as expected
254
+ * @param evaluationContext - EvaluationContext used for the request
255
+ * @param expectedType - type expected for the value
256
+ * @param <T> the type of your evaluation
257
+ * @return a ProviderEvaluation that contains the open-feature response
258
+ */
242
259
private <T > ProviderEvaluation <T > getEvaluation (
243
260
String key , T defaultValue , EvaluationContext evaluationContext , Class <?> expectedType ) {
244
261
if (!ProviderState .READY .equals (state )) {
@@ -247,53 +264,49 @@ private <T> ProviderEvaluation<T> getEvaluation(
247
264
errorCode = ErrorCode .GENERAL ;
248
265
}
249
266
return ProviderEvaluation .<T >builder ()
250
- .errorCode (errorCode )
251
- .reason (errorCode .name ())
252
- .value (defaultValue )
253
- .build ();
267
+ .errorCode (errorCode )
268
+ .reason (errorCode .name ())
269
+ .value (defaultValue )
270
+ .build ();
254
271
}
255
- ProviderEvaluation < T > res ;
272
+
256
273
GoFeatureFlagUser user = GoFeatureFlagUser .fromEvaluationContext (evaluationContext );
257
274
if (cache == null ) {
275
+ // Cache is disabled we return directly the remote evaluation
258
276
EvaluationResponse <T > proxyRes = resolveEvaluationGoFeatureFlagProxy (key , defaultValue , user , expectedType );
259
- res = proxyRes .getProviderEvaluation ();
260
- } else {
261
- res = getProviderEvaluationWithCheckCache (key , defaultValue , expectedType , user );
262
- eventsPublisher .add (Event .builder ()
263
- .key (key )
264
- .defaultValue (defaultValue )
265
- .variation (res .getVariant ())
266
- .value (res .getValue ())
267
- .userKey (user .getKey ())
268
- .creationDate (System .currentTimeMillis ())
269
- .build ()
270
- );
277
+ return proxyRes .getProviderEvaluation ();
271
278
}
272
- return res ;
273
- }
274
279
275
- private <T > ProviderEvaluation getProviderEvaluationWithCheckCache (
276
- String key , T defaultValue , Class <?> expectedType , GoFeatureFlagUser user ) {
277
- ProviderEvaluation <?> res ;
280
+ String cacheKey = null ;
278
281
try {
279
- String cacheKey = buildCacheKey (key , BeanUtils .buildKey (user ));
280
- res = cache .getIfPresent (cacheKey );
281
- if (res == null ) {
282
- EvaluationResponse <T > proxyRes = resolveEvaluationGoFeatureFlagProxy (
283
- key , defaultValue , user , expectedType );
284
- if (Boolean .TRUE .equals (proxyRes .getCachable ())) {
285
- cache .put (cacheKey , proxyRes .getProviderEvaluation ());
286
- }
287
- res = proxyRes .getProviderEvaluation ();
288
- } else {
289
- res .setReason (CACHED_REASON );
282
+ cacheKey = buildCacheKey (key , BeanUtils .buildKey (user ));
283
+ ProviderEvaluation <?> cacheResponse = cache .getIfPresent (cacheKey );
284
+ if (cacheResponse == null ) {
285
+ throw new NotPresentInCache (cacheKey );
286
+ }
287
+ cacheResponse .setReason (CACHED_REASON );
288
+ eventsPublisher .add (Event .builder ()
289
+ .key (key )
290
+ .kind ("feature" )
291
+ .contextKind (user .isAnonymous () ? "anonymousUser" : "user" )
292
+ .defaultValue (defaultValue )
293
+ .variation (cacheResponse .getVariant ())
294
+ .value (cacheResponse .getValue ())
295
+ .userKey (user .getKey ())
296
+ .creationDate (System .currentTimeMillis ())
297
+ .build ()
298
+ );
299
+ return (ProviderEvaluation <T >) cacheResponse ;
300
+ } catch (Exception e ) {
301
+ if (!(e instanceof NotPresentInCache )) {
302
+ log .error ("Error while trying to retrieve from the cache, trying to do a remote evaluation" , e );
290
303
}
291
- } catch (JsonProcessingException e ) {
292
- log .error ("Error building key for user" , e );
293
304
EvaluationResponse <T > proxyRes = resolveEvaluationGoFeatureFlagProxy (key , defaultValue , user , expectedType );
294
- res = proxyRes .getProviderEvaluation ();
305
+ if (Boolean .TRUE .equals (proxyRes .getCachable ()) && cacheKey != null ) {
306
+ cache .put (cacheKey , proxyRes .getProviderEvaluation ());
307
+ }
308
+ return proxyRes .getProviderEvaluation ();
295
309
}
296
- return res ;
297
310
}
298
311
299
312
/**
@@ -346,13 +359,13 @@ private <T> EvaluationResponse<T> resolveEvaluationGoFeatureFlagProxy(
346
359
if (Reason .DISABLED .name ().equalsIgnoreCase (goffResp .getReason ())) {
347
360
// we don't set a variant since we are using the default value, and we are not able to know
348
361
// which variant it is.
349
- ProviderEvaluation <T > providerEvaluation = ProviderEvaluation .<T >builder ()
362
+ ProviderEvaluation <T > providerEvaluation = ProviderEvaluation .<T >builder ()
350
363
.value (defaultValue )
351
364
.variant (goffResp .getVariationType ())
352
365
.reason (Reason .DISABLED .name ()).build ();
353
366
354
367
return EvaluationResponse .<T >builder ()
355
- .providerEvaluation (providerEvaluation ).cachable (goffResp .getCacheable ()).build ();
368
+ .providerEvaluation (providerEvaluation ).cachable (goffResp .getCacheable ()).build ();
356
369
}
357
370
358
371
if (ErrorCode .FLAG_NOT_FOUND .name ().equalsIgnoreCase (goffResp .getErrorCode ())) {
@@ -368,15 +381,15 @@ private <T> EvaluationResponse<T> resolveEvaluationGoFeatureFlagProxy(
368
381
}
369
382
370
383
ProviderEvaluation <T > providerEvaluation = ProviderEvaluation .<T >builder ()
371
- .errorCode (mapErrorCode (goffResp .getErrorCode ()))
372
- .reason (goffResp .getReason ())
373
- .value (flagValue )
374
- .variant (goffResp .getVariationType ())
375
- .flagMetadata (this .convertFlagMetadata (goffResp .getMetadata ()))
376
- .build ();
384
+ .errorCode (mapErrorCode (goffResp .getErrorCode ()))
385
+ .reason (goffResp .getReason ())
386
+ .value (flagValue )
387
+ .variant (goffResp .getVariationType ())
388
+ .flagMetadata (this .convertFlagMetadata (goffResp .getMetadata ()))
389
+ .build ();
377
390
378
391
return EvaluationResponse .<T >builder ()
379
- .providerEvaluation (providerEvaluation ).cachable (goffResp .getCacheable ()).build ();
392
+ .providerEvaluation (providerEvaluation ).cachable (goffResp .getCacheable ()).build ();
380
393
}
381
394
} catch (IOException e ) {
382
395
throw new GeneralError ("unknown error while retrieving flag " + key , e );
@@ -490,37 +503,49 @@ private Structure mapToStructure(Map<String, Object> map) {
490
503
.collect (Collectors .toMap (Map .Entry ::getKey , e -> objectToValue (e .getValue ()))));
491
504
}
492
505
493
- @ SneakyThrows
506
+ /**
507
+ * publishEvents is calling the GO Feature Flag data/collector api to store the flag usage for analytics.
508
+ *
509
+ * @param eventsList - list of the event to send to GO Feature Flag
510
+ */
494
511
private void publishEvents (List <Event > eventsList ) {
495
- Events events = new Events (eventsList );
496
- HttpUrl url = this .parsedEndpoint .newBuilder ()
497
- .addEncodedPathSegment ("v1" )
498
- .addEncodedPathSegment ("data" )
499
- .addEncodedPathSegment ("collector" )
500
- .build ();
501
-
502
- Request .Builder reqBuilder = new Request .Builder ()
503
- .url (url )
504
- .addHeader ("Content-Type" , "application/json" )
505
- .post (RequestBody .create (
506
- requestMapper .writeValueAsBytes (events ),
507
- MediaType .get ("application/json; charset=utf-8" )));
508
-
509
- if (this .apiKey != null && !"" .equals (this .apiKey )) {
510
- reqBuilder .addHeader ("Authorization" , "Bearer " + this .apiKey );
511
- }
512
+ try {
513
+ Events events = new Events (eventsList );
514
+ HttpUrl url = this .parsedEndpoint .newBuilder ()
515
+ .addEncodedPathSegment ("v1" )
516
+ .addEncodedPathSegment ("data" )
517
+ .addEncodedPathSegment ("collector" )
518
+ .build ();
512
519
513
- try (Response response = this .httpClient .newCall (reqBuilder .build ()).execute ()) {
514
- if (response .code () == HTTP_UNAUTHORIZED ) {
515
- throw new GeneralError ("Unauthorized" );
516
- }
517
- if (response .code () >= HTTP_BAD_REQUEST ) {
518
- throw new GeneralError ("Bad request: " + response .body ());
520
+ Request .Builder reqBuilder = null ;
521
+
522
+ reqBuilder = new Request .Builder ()
523
+ .url (url )
524
+ .addHeader ("Content-Type" , "application/json" )
525
+ .post (RequestBody .create (
526
+ requestMapper .writeValueAsBytes (events ),
527
+ MediaType .get ("application/json; charset=utf-8" )));
528
+
529
+ if (this .apiKey != null && !"" .equals (this .apiKey )) {
530
+ reqBuilder .addHeader ("Authorization" , "Bearer " + this .apiKey );
519
531
}
520
532
521
- if (response .code () == HTTP_OK ) {
522
- log .info ("Published {} events successfully: {}" , eventsList .size (), response .body ());
533
+ try (Response response = this .httpClient .newCall (reqBuilder .build ()).execute ()) {
534
+ if (response .code () == HTTP_UNAUTHORIZED ) {
535
+ throw new GeneralError ("Unauthorized" );
536
+ }
537
+ if (response .code () >= HTTP_BAD_REQUEST ) {
538
+ throw new GeneralError ("Bad request: " + response .body ());
539
+ }
540
+
541
+ if (response .code () == HTTP_OK ) {
542
+ log .info ("Published {} events successfully: {}" , eventsList .size (), response .body ());
543
+ }
544
+ } catch (IOException e ) {
545
+ throw new GeneralError ("Impossible to send the usage data to GO Feature Flag" , e );
523
546
}
547
+ } catch (JsonProcessingException e ) {
548
+ throw new GeneralError ("Impossible to convert data collector events" , e );
524
549
}
525
550
}
526
551
0 commit comments