Skip to content

Commit 1867697

Browse files
add unit test for flag cache invalidation
Signed-off-by: Thomas Poignant <[email protected]>
1 parent 6d20683 commit 1867697

File tree

7 files changed

+92
-15
lines changed

7 files changed

+92
-15
lines changed

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

+13-8
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,15 @@ public void initialize(EvaluationContext evaluationContext) throws Exception {
115115

116116
if (options.getEnableCache() == null || options.getEnableCache()) {
117117
this.cacheCtrl = CacheController.builder().options(options).build();
118-
this.dataCollectorHook = new DataCollectorHook(DataCollectorHookOptions.builder()
119-
.flushIntervalMs(options.getFlushIntervalMs())
120-
.gofeatureflagController(this.gofeatureflagController)
121-
.maxPendingEvents(options.getMaxPendingEvents())
122-
.build());
123-
this.hooks.add(this.dataCollectorHook);
118+
119+
if (!this.options.isDisableDataCollection()) {
120+
this.dataCollectorHook = new DataCollectorHook(DataCollectorHookOptions.builder()
121+
.flushIntervalMs(options.getFlushIntervalMs())
122+
.gofeatureflagController(this.gofeatureflagController)
123+
.maxPendingEvents(options.getMaxPendingEvents())
124+
.build());
125+
this.hooks.add(this.dataCollectorHook);
126+
}
124127
this.flagChangeDisposable =
125128
this.startCheckFlagConfigurationChangesDaemon();
126129
}
@@ -147,7 +150,7 @@ private Disposable startCheckFlagConfigurationChangesDaemon() {
147150
.takeUntil(stopSignal)
148151
.flatMap(tick -> Observable.fromCallable(() -> this.gofeatureflagController.configurationHasChanged())
149152
.onErrorResumeNext(e -> {
150-
log.error("error while calling flag change API, error", e);
153+
log.error("error while calling flag change API", e);
151154
if (e instanceof ConfigurationChangeEndpointNotFound) {
152155
// emit an item to stop the interval to stop the loop
153156
stopSignal.onNext(new Object());
@@ -164,6 +167,8 @@ private Disposable startCheckFlagConfigurationChangesDaemon() {
164167
this.cacheCtrl.invalidateAll();
165168
super.emitProviderConfigurationChanged(ProviderEventDetails.builder()
166169
.message("GO Feature Flag Configuration changed, clearing the cache").build());
170+
} else {
171+
log.debug("flag configuration has not changed: {}", response);
167172
}
168173
},
169174
throwable -> log.error("error while calling flag change API, error: {}", throwable.getMessage())
@@ -242,7 +247,7 @@ private <T> ProviderEvaluation<T> getEvaluation(
242247

243248
@Override
244249
public void shutdown() {
245-
log.info("shutdown");
250+
log.debug("shutdown");
246251
if (this.dataCollectorHook != null) {
247252
this.dataCollectorHook.shutdown();
248253
}

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

+6
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,10 @@ public class GoFeatureFlagProviderOptions {
8686
* default: 120000
8787
*/
8888
private Long flagChangePollingIntervalMs;
89+
90+
/**
91+
* (optional) disableDataCollection set to true if you don't want to collect the usage of flags retrieved in the cache.
92+
* default: false
93+
*/
94+
private boolean disableDataCollection;
8995
}

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

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* ConfigurationChange is an enum to represent the change of the configuration.
55
*/
66
public enum ConfigurationChange {
7+
FLAG_CONFIGURATION_INITIALIZED,
78
FLAG_CONFIGURATION_UPDATED,
89
FLAG_CONFIGURATION_NOT_CHANGED
910
}

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

+6-3
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ public ConfigurationChange configurationHasChanged() throws GoFeatureFlagExcepti
253253
HttpUrl url = this.parsedEndpoint.newBuilder()
254254
.addEncodedPathSegment("v1")
255255
.addEncodedPathSegment("flag")
256-
.addEncodedPathSegment("chang1e")
256+
.addEncodedPathSegment("change")
257257
.build();
258258

259259
Request.Builder reqBuilder = new Request.Builder()
@@ -281,8 +281,11 @@ public ConfigurationChange configurationHasChanged() throws GoFeatureFlagExcepti
281281
throw new ConfigurationChangeEndpointUnknownErr();
282282
}
283283

284-
this.etag = response.header("ETag");
285-
return ConfigurationChange.FLAG_CONFIGURATION_UPDATED;
284+
boolean isInitialConfiguration = this.etag == null;
285+
this.etag = response.header(HttpHeaders.ETAG);
286+
return isInitialConfiguration
287+
? ConfigurationChange.FLAG_CONFIGURATION_INITIALIZED
288+
: ConfigurationChange.FLAG_CONFIGURATION_UPDATED;
286289
} catch (IOException e) {
287290
throw new ConfigurationChangeEndpointUnknownErr(e);
288291
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package dev.openfeature.contrib.providers.gofeatureflag.hook.events;
22

33

4-
import dev.openfeature.contrib.providers.gofeatureflag.concurrent.ConcurrentUtils;
4+
import dev.openfeature.contrib.providers.gofeatureflag.util.ConcurrentUtils;
55
import lombok.extern.slf4j.Slf4j;
66

77
import java.util.ArrayList;

Diff for: providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/concurrent/ConcurrentUtils.java renamed to providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/util/ConcurrentUtils.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package dev.openfeature.contrib.providers.gofeatureflag.concurrent;
1+
package dev.openfeature.contrib.providers.gofeatureflag.util;
22

33
import lombok.AccessLevel;
44
import lombok.NoArgsConstructor;

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

+64-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import java.util.Map;
1212

1313
import com.google.common.cache.CacheBuilder;
14+
import com.google.common.net.HttpHeaders;
1415
import dev.openfeature.sdk.Client;
1516
import dev.openfeature.sdk.ErrorCode;
1617
import dev.openfeature.sdk.EvaluationContext;
@@ -49,6 +50,8 @@
4950
@Slf4j
5051
class GoFeatureFlagProviderTest {
5152
private int publishEventsRequestsReceived = 0;
53+
private int flagChangeCallCounter = 0;
54+
private boolean flagChanged404 = false;
5255

5356
// Dispatcher is the configuration of the mock server to test the provider.
5457
final Dispatcher dispatcher = new Dispatcher() {
@@ -78,8 +81,21 @@ public MockResponse dispatch(RecordedRequest request) {
7881
}
7982
return new MockResponse().setResponseCode(200);
8083
}
81-
if (request.getPath().startsWith("/v1/flag/change")) {
82-
return new MockResponse().setResponseCode(200).setHeader("etag", "123456");
84+
if (request.getPath().contains("/v1/flag/change")) {
85+
flagChangeCallCounter++;
86+
if (flagChanged404) {
87+
return new MockResponse().setResponseCode(404);
88+
}
89+
if (flagChangeCallCounter == 2) {
90+
return new MockResponse().setResponseCode(200).setHeader(HttpHeaders.ETAG, "7891011");
91+
}
92+
if (request.getHeader(HttpHeaders.IF_NONE_MATCH) != null
93+
&& (request.getHeader(HttpHeaders.IF_NONE_MATCH).equals("123456")
94+
|| request.getHeader(HttpHeaders.IF_NONE_MATCH).equals("7891011"))) {
95+
return new MockResponse().setResponseCode(304);
96+
}
97+
98+
return new MockResponse().setResponseCode(200).setHeader(HttpHeaders.ETAG, "123456");
8399
}
84100
return new MockResponse().setResponseCode(404);
85101
}
@@ -98,6 +114,8 @@ public MockResponse dispatch(RecordedRequest request) {
98114

99115
@BeforeEach
100116
void beforeEach(TestInfo testInfo) throws IOException {
117+
this.flagChangeCallCounter = 0;
118+
this.flagChanged404 = false;
101119
this.testName = testInfo.getDisplayName();
102120
this.server = new MockWebServer();
103121
this.server.setDispatcher(dispatcher);
@@ -777,6 +795,50 @@ void should_publish_events_context_without_anonymous() {
777795
assertEquals(3, publishEventsRequestsReceived, "We pass the flush interval, we should have 3 events");
778796
}
779797

798+
@SneakyThrows
799+
@Test
800+
void should_not_get_cached_value_if_flag_configuration_changed() {
801+
this.evaluationContext = new MutableContext("d45e303a-38c2-11ed-a261-0242ac120002");
802+
GoFeatureFlagProvider g = new GoFeatureFlagProvider(GoFeatureFlagProviderOptions.builder()
803+
.endpoint(this.baseUrl.toString())
804+
.timeout(1000)
805+
.disableDataCollection(true)
806+
.enableCache(true)
807+
.flagChangePollingIntervalMs(100L)
808+
.disableDataCollection(true)
809+
.build());
810+
String providerName = this.testName;
811+
OpenFeatureAPI.getInstance().setProviderAndWait(providerName, g);
812+
Client client = OpenFeatureAPI.getInstance().getClient(providerName);
813+
FlagEvaluationDetails<Boolean> got = client.getBooleanDetails("bool_targeting_match", false, this.evaluationContext);
814+
assertEquals(Reason.TARGETING_MATCH.name(), got.getReason());
815+
got = client.getBooleanDetails("bool_targeting_match", false, this.evaluationContext);
816+
assertEquals(Reason.CACHED.name(), got.getReason());
817+
got = client.getBooleanDetails("bool_targeting_match", false, this.evaluationContext);
818+
assertEquals(Reason.CACHED.name(), got.getReason());
819+
Thread.sleep(200L);
820+
got = client.getBooleanDetails("bool_targeting_match", false, this.evaluationContext);
821+
assertEquals(Reason.TARGETING_MATCH.name(), got.getReason());
822+
}
823+
824+
@SneakyThrows
825+
@Test
826+
void should_stop_calling_flag_change_if_receive_404() {
827+
this.flagChanged404 = true;
828+
this.evaluationContext = new MutableContext("d45e303a-38c2-11ed-a261-0242ac120002");
829+
GoFeatureFlagProvider g = new GoFeatureFlagProvider(GoFeatureFlagProviderOptions.builder()
830+
.endpoint(this.baseUrl.toString())
831+
.timeout(1000)
832+
.enableCache(true)
833+
.flagChangePollingIntervalMs(10L)
834+
.build());
835+
String providerName = this.testName;
836+
OpenFeatureAPI.getInstance().setProviderAndWait(providerName, g);
837+
Client client = OpenFeatureAPI.getInstance().getClient(providerName);
838+
Thread.sleep(150L);
839+
assertEquals(1, this.flagChangeCallCounter);
840+
}
841+
780842
private String readMockResponse(String filename) throws Exception {
781843
URL url = getClass().getClassLoader().getResource("mock_responses/" + filename);
782844
assert url != null;

0 commit comments

Comments
 (0)