Skip to content

Commit 8923a47

Browse files
Refactoring a bit to make it more readable
Signed-off-by: Thomas Poignant <[email protected]>
1 parent 3512291 commit 8923a47

File tree

3 files changed

+122
-90
lines changed

3 files changed

+122
-90
lines changed

Diff for: providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProvider.java

+114-89
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import dev.openfeature.contrib.providers.gofeatureflag.events.EventsPublisher;
1717
import dev.openfeature.contrib.providers.gofeatureflag.exception.InvalidEndpoint;
1818
import dev.openfeature.contrib.providers.gofeatureflag.exception.InvalidOptions;
19+
import dev.openfeature.contrib.providers.gofeatureflag.exception.NotPresentInCache;
1920
import dev.openfeature.sdk.ErrorCode;
2021
import dev.openfeature.sdk.EvaluationContext;
2122
import dev.openfeature.sdk.FeatureProvider;
@@ -34,7 +35,6 @@
3435
import dev.openfeature.sdk.exceptions.TypeMismatchError;
3536
import lombok.AccessLevel;
3637
import lombok.Getter;
37-
import lombok.SneakyThrows;
3838
import lombok.extern.slf4j.Slf4j;
3939
import okhttp3.ConnectionPool;
4040
import okhttp3.HttpUrl;
@@ -54,7 +54,6 @@
5454
import java.util.function.Consumer;
5555
import java.util.stream.Collectors;
5656

57-
5857
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
5958
import static java.net.HttpURLConnection.HTTP_OK;
6059
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
@@ -64,33 +63,27 @@
6463
*/
6564
@Slf4j
6665
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-
7266
public static final long DEFAULT_FLUSH_INTERVAL_MS = Duration.ofMinutes(1).toMillis();
7367
public static final int DEFAULT_MAX_PENDING_EVENTS = 10000;
7468
public static final long DEFAULT_CACHE_TTL_MS = 1000;
7569
public static final int DEFAULT_CACHE_CONCURRENCY_LEVEL = 1;
7670
public static final int DEFAULT_CACHE_INITIAL_CAPACITY = 100;
7771
public static final int DEFAULT_CACHE_MAXIMUM_SIZE = 10000;
7872
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;
7978
private HttpUrl parsedEndpoint;
8079
// httpClient is the instance of the OkHttpClient used by the provider
8180
private OkHttpClient httpClient;
82-
8381
// apiKey contains the token to use while calling GO Feature Flag relay proxy
8482
private String apiKey;
85-
8683
@Getter(AccessLevel.PROTECTED)
8784
private Cache<String, ProviderEvaluation<?>> cache;
88-
8985
@Getter(AccessLevel.PROTECTED)
9086
private EventsPublisher<Event> eventsPublisher;
91-
92-
private final GoFeatureFlagProviderOptions options;
93-
9487
private ProviderState state = ProviderState.NOT_READY;
9588

9689
/**
@@ -133,12 +126,17 @@ private void validateInputOptions(GoFeatureFlagProviderOptions options) throws I
133126
}
134127
}
135128

129+
/**
130+
* buildDefaultCache is building a default cache configuration
131+
*
132+
* @return the default cache configuraiton
133+
*/
136134
private Cache<String, ProviderEvaluation<?>> buildDefaultCache() {
137135
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();
142140
}
143141

144142
@Override
@@ -186,6 +184,13 @@ public ProviderEvaluation<Value> getObjectEvaluation(
186184
return getEvaluation(key, defaultValue, evaluationContext, Value.class);
187185
}
188186

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+
*/
189194
private String buildCacheKey(String key, String userKey) {
190195
return key + "," + userKey;
191196
}
@@ -232,13 +237,25 @@ public void initialize(EvaluationContext evaluationContext) throws Exception {
232237
eventsPublisher = new EventsPublisher<>(publisher, flushIntervalMs, maxPendingEvents);
233238
}
234239
state = ProviderState.READY;
240+
log.info("finishing initializing provider, state: {}", state);
235241
}
236242

237243
@Override
238244
public ProviderState getState() {
239245
return state;
240246
}
241247

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+
*/
242259
private <T> ProviderEvaluation<T> getEvaluation(
243260
String key, T defaultValue, EvaluationContext evaluationContext, Class<?> expectedType) {
244261
if (!ProviderState.READY.equals(state)) {
@@ -247,53 +264,49 @@ private <T> ProviderEvaluation<T> getEvaluation(
247264
errorCode = ErrorCode.GENERAL;
248265
}
249266
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();
254271
}
255-
ProviderEvaluation<T> res;
272+
256273
GoFeatureFlagUser user = GoFeatureFlagUser.fromEvaluationContext(evaluationContext);
257274
if (cache == null) {
275+
// Cache is disabled we return directly the remote evaluation
258276
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();
271278
}
272-
return res;
273-
}
274279

275-
private <T> ProviderEvaluation getProviderEvaluationWithCheckCache(
276-
String key, T defaultValue, Class<?> expectedType, GoFeatureFlagUser user) {
277-
ProviderEvaluation<?> res;
280+
String cacheKey = null;
278281
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);
290303
}
291-
} catch (JsonProcessingException e) {
292-
log.error("Error building key for user", e);
293304
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();
295309
}
296-
return res;
297310
}
298311

299312
/**
@@ -346,13 +359,13 @@ private <T> EvaluationResponse<T> resolveEvaluationGoFeatureFlagProxy(
346359
if (Reason.DISABLED.name().equalsIgnoreCase(goffResp.getReason())) {
347360
// we don't set a variant since we are using the default value, and we are not able to know
348361
// which variant it is.
349-
ProviderEvaluation<T> providerEvaluation = ProviderEvaluation.<T>builder()
362+
ProviderEvaluation<T> providerEvaluation = ProviderEvaluation.<T>builder()
350363
.value(defaultValue)
351364
.variant(goffResp.getVariationType())
352365
.reason(Reason.DISABLED.name()).build();
353366

354367
return EvaluationResponse.<T>builder()
355-
.providerEvaluation(providerEvaluation).cachable(goffResp.getCacheable()).build();
368+
.providerEvaluation(providerEvaluation).cachable(goffResp.getCacheable()).build();
356369
}
357370

358371
if (ErrorCode.FLAG_NOT_FOUND.name().equalsIgnoreCase(goffResp.getErrorCode())) {
@@ -368,15 +381,15 @@ private <T> EvaluationResponse<T> resolveEvaluationGoFeatureFlagProxy(
368381
}
369382

370383
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();
377390

378391
return EvaluationResponse.<T>builder()
379-
.providerEvaluation(providerEvaluation).cachable(goffResp.getCacheable()).build();
392+
.providerEvaluation(providerEvaluation).cachable(goffResp.getCacheable()).build();
380393
}
381394
} catch (IOException e) {
382395
throw new GeneralError("unknown error while retrieving flag " + key, e);
@@ -490,37 +503,49 @@ private Structure mapToStructure(Map<String, Object> map) {
490503
.collect(Collectors.toMap(Map.Entry::getKey, e -> objectToValue(e.getValue()))));
491504
}
492505

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+
*/
494511
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();
512519

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);
519531
}
520532

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);
523546
}
547+
} catch (JsonProcessingException e) {
548+
throw new GeneralError("Impossible to convert data collector events", e);
524549
}
525550
}
526551

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package dev.openfeature.contrib.providers.gofeatureflag.exception;
2+
3+
public class NotPresentInCache extends GoFeatureFlagException {
4+
public NotPresentInCache(String cacheKey){
5+
super("No item found for key: "+ cacheKey);
6+
}
7+
}

Diff for: providers/go-feature-flag/src/test/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProviderTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -514,7 +514,7 @@ void should_publish_events() {
514514
assertEquals(0, g.getEventsPublisher().publish(), "first attempt expected to fail");
515515

516516
// simulate publish on next interval
517-
assertEquals(3, g.getEventsPublisher().publish(), "expected to publish all events after retry");
517+
assertEquals(2, g.getEventsPublisher().publish(), "expected to publish all events after retry");
518518

519519
g.getBooleanEvaluation("bool_targeting_match", false, this.evaluationContext);
520520
g.getBooleanEvaluation("bool_targeting_match", false, this.evaluationContext);

0 commit comments

Comments
 (0)