Skip to content

Commit

Permalink
add unit test for flag cache invalidation
Browse files Browse the repository at this point in the history
Signed-off-by: Thomas Poignant <thomas.poignant@gofeatureflag.org>
  • Loading branch information
thomaspoignant committed Jun 11, 2024
1 parent 6d20683 commit 64964c5
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,15 @@ public void initialize(EvaluationContext evaluationContext) throws Exception {

if (options.getEnableCache() == null || options.getEnableCache()) {
this.cacheCtrl = CacheController.builder().options(options).build();
this.dataCollectorHook = new DataCollectorHook(DataCollectorHookOptions.builder()
.flushIntervalMs(options.getFlushIntervalMs())
.gofeatureflagController(this.gofeatureflagController)
.maxPendingEvents(options.getMaxPendingEvents())
.build());
this.hooks.add(this.dataCollectorHook);

if (!this.options.isDisableDataCollection()) {
this.dataCollectorHook = new DataCollectorHook(DataCollectorHookOptions.builder()
.flushIntervalMs(options.getFlushIntervalMs())
.gofeatureflagController(this.gofeatureflagController)
.maxPendingEvents(options.getMaxPendingEvents())
.build());
this.hooks.add(this.dataCollectorHook);
}
this.flagChangeDisposable =
this.startCheckFlagConfigurationChangesDaemon();
}
Expand All @@ -147,7 +150,7 @@ private Disposable startCheckFlagConfigurationChangesDaemon() {
.takeUntil(stopSignal)
.flatMap(tick -> Observable.fromCallable(() -> this.gofeatureflagController.configurationHasChanged())
.onErrorResumeNext(e -> {
log.error("error while calling flag change API, error", e);
log.error("error while calling flag change API", e);
if (e instanceof ConfigurationChangeEndpointNotFound) {
// emit an item to stop the interval to stop the loop
stopSignal.onNext(new Object());
Expand All @@ -164,6 +167,8 @@ private Disposable startCheckFlagConfigurationChangesDaemon() {
this.cacheCtrl.invalidateAll();
super.emitProviderConfigurationChanged(ProviderEventDetails.builder()
.message("GO Feature Flag Configuration changed, clearing the cache").build());
} else {
log.debug("flag configuration has not changed: {}", response);
}
},
throwable -> log.error("error while calling flag change API, error: {}", throwable.getMessage())
Expand Down Expand Up @@ -242,7 +247,7 @@ private <T> ProviderEvaluation<T> getEvaluation(

@Override
public void shutdown() {
log.info("shutdown");
log.debug("shutdown");
if (this.dataCollectorHook != null) {
this.dataCollectorHook.shutdown();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,11 @@ public class GoFeatureFlagProviderOptions {
* default: 120000
*/
private Long flagChangePollingIntervalMs;

/**
* (optional) disableDataCollection set to true if you don't want to collect the usage of
* flags retrieved in the cache.
* default: false
*/
private boolean disableDataCollection;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* ConfigurationChange is an enum to represent the change of the configuration.
*/
public enum ConfigurationChange {
FLAG_CONFIGURATION_INITIALIZED,
FLAG_CONFIGURATION_UPDATED,
FLAG_CONFIGURATION_NOT_CHANGED
}
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ public ConfigurationChange configurationHasChanged() throws GoFeatureFlagExcepti
HttpUrl url = this.parsedEndpoint.newBuilder()
.addEncodedPathSegment("v1")
.addEncodedPathSegment("flag")
.addEncodedPathSegment("chang1e")
.addEncodedPathSegment("change")
.build();

Request.Builder reqBuilder = new Request.Builder()
Expand Down Expand Up @@ -281,8 +281,11 @@ public ConfigurationChange configurationHasChanged() throws GoFeatureFlagExcepti
throw new ConfigurationChangeEndpointUnknownErr();
}

this.etag = response.header("ETag");
return ConfigurationChange.FLAG_CONFIGURATION_UPDATED;
boolean isInitialConfiguration = this.etag == null;
this.etag = response.header(HttpHeaders.ETAG);
return isInitialConfiguration
? ConfigurationChange.FLAG_CONFIGURATION_INITIALIZED
: ConfigurationChange.FLAG_CONFIGURATION_UPDATED;
} catch (IOException e) {
throw new ConfigurationChangeEndpointUnknownErr(e);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package dev.openfeature.contrib.providers.gofeatureflag.hook.events;


import dev.openfeature.contrib.providers.gofeatureflag.concurrent.ConcurrentUtils;
import dev.openfeature.contrib.providers.gofeatureflag.util.ConcurrentUtils;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package dev.openfeature.contrib.providers.gofeatureflag.concurrent;
package dev.openfeature.contrib.providers.gofeatureflag.util;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.util.Map;

import com.google.common.cache.CacheBuilder;
import com.google.common.net.HttpHeaders;
import dev.openfeature.sdk.Client;
import dev.openfeature.sdk.ErrorCode;
import dev.openfeature.sdk.EvaluationContext;
Expand Down Expand Up @@ -49,6 +50,8 @@
@Slf4j
class GoFeatureFlagProviderTest {
private int publishEventsRequestsReceived = 0;
private int flagChangeCallCounter = 0;
private boolean flagChanged404 = false;

// Dispatcher is the configuration of the mock server to test the provider.
final Dispatcher dispatcher = new Dispatcher() {
Expand Down Expand Up @@ -78,8 +81,21 @@ public MockResponse dispatch(RecordedRequest request) {
}
return new MockResponse().setResponseCode(200);
}
if (request.getPath().startsWith("/v1/flag/change")) {
return new MockResponse().setResponseCode(200).setHeader("etag", "123456");
if (request.getPath().contains("/v1/flag/change")) {
flagChangeCallCounter++;
if (flagChanged404) {
return new MockResponse().setResponseCode(404);
}
if (flagChangeCallCounter == 2) {
return new MockResponse().setResponseCode(200).setHeader(HttpHeaders.ETAG, "7891011");
}
if (request.getHeader(HttpHeaders.IF_NONE_MATCH) != null
&& (request.getHeader(HttpHeaders.IF_NONE_MATCH).equals("123456")
|| request.getHeader(HttpHeaders.IF_NONE_MATCH).equals("7891011"))) {
return new MockResponse().setResponseCode(304);
}

return new MockResponse().setResponseCode(200).setHeader(HttpHeaders.ETAG, "123456");
}
return new MockResponse().setResponseCode(404);
}
Expand All @@ -98,6 +114,8 @@ public MockResponse dispatch(RecordedRequest request) {

@BeforeEach
void beforeEach(TestInfo testInfo) throws IOException {
this.flagChangeCallCounter = 0;
this.flagChanged404 = false;
this.testName = testInfo.getDisplayName();
this.server = new MockWebServer();
this.server.setDispatcher(dispatcher);
Expand Down Expand Up @@ -777,6 +795,50 @@ void should_publish_events_context_without_anonymous() {
assertEquals(3, publishEventsRequestsReceived, "We pass the flush interval, we should have 3 events");
}

@SneakyThrows
@Test
void should_not_get_cached_value_if_flag_configuration_changed() {
this.evaluationContext = new MutableContext("d45e303a-38c2-11ed-a261-0242ac120002");
GoFeatureFlagProvider g = new GoFeatureFlagProvider(GoFeatureFlagProviderOptions.builder()
.endpoint(this.baseUrl.toString())
.timeout(1000)
.disableDataCollection(true)
.enableCache(true)
.flagChangePollingIntervalMs(100L)
.disableDataCollection(true)
.build());
String providerName = this.testName;
OpenFeatureAPI.getInstance().setProviderAndWait(providerName, g);
Client client = OpenFeatureAPI.getInstance().getClient(providerName);
FlagEvaluationDetails<Boolean> got = client.getBooleanDetails("bool_targeting_match", false, this.evaluationContext);
assertEquals(Reason.TARGETING_MATCH.name(), got.getReason());
got = client.getBooleanDetails("bool_targeting_match", false, this.evaluationContext);
assertEquals(Reason.CACHED.name(), got.getReason());
got = client.getBooleanDetails("bool_targeting_match", false, this.evaluationContext);
assertEquals(Reason.CACHED.name(), got.getReason());
Thread.sleep(200L);
got = client.getBooleanDetails("bool_targeting_match", false, this.evaluationContext);
assertEquals(Reason.TARGETING_MATCH.name(), got.getReason());
}

@SneakyThrows
@Test
void should_stop_calling_flag_change_if_receive_404() {
this.flagChanged404 = true;
this.evaluationContext = new MutableContext("d45e303a-38c2-11ed-a261-0242ac120002");
GoFeatureFlagProvider g = new GoFeatureFlagProvider(GoFeatureFlagProviderOptions.builder()
.endpoint(this.baseUrl.toString())
.timeout(1000)
.enableCache(true)
.flagChangePollingIntervalMs(10L)
.build());
String providerName = this.testName;
OpenFeatureAPI.getInstance().setProviderAndWait(providerName, g);
Client client = OpenFeatureAPI.getInstance().getClient(providerName);
Thread.sleep(150L);
assertEquals(1, this.flagChangeCallCounter);
}

private String readMockResponse(String filename) throws Exception {
URL url = getClass().getClassLoader().getResource("mock_responses/" + filename);
assert url != null;
Expand Down

0 comments on commit 64964c5

Please sign in to comment.