Skip to content

Commit b366878

Browse files
Readyhook rework (#51)
* Rework onClientReady to return with new ClientReadyState. * Fix isExpired. Small fixes on init calls. Added test cases for ready hook. * Add waitForReadyAsync method and test. Fix addOnClientReady to return with the state if it was already set. * Rename state enum to ClientCacheState * Deprecated the old onClientReady * Prepare 9.2.0 release
1 parent 88044b2 commit b366878

File tree

9 files changed

+168
-17
lines changed

9 files changed

+168
-17
lines changed

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
version=9.1.3
1+
version=9.2.0
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.configcat;
2+
3+
/**
4+
* Describes the Client state.
5+
*/
6+
public enum ClientCacheState {
7+
/**
8+
* The SDK has no feature flag data neither from the cache nor from the ConfigCat CDN.
9+
*/
10+
NO_FLAG_DATA,
11+
/**
12+
* The SDK runs with local only feature flag data.
13+
*/
14+
HAS_LOCAL_OVERRIDE_FLAG_DATA_ONLY,
15+
/**
16+
* The SDK has feature flag data to work with only from the cache.
17+
*/
18+
HAS_CACHED_FLAG_DATA_ONLY,
19+
/**
20+
* The SDK works with the latest feature flag data received from the ConfigCat CDN.
21+
*/
22+
HAS_UP_TO_DATE_FLAG_DATA,
23+
}

src/main/java/com/configcat/ConfigCatClient.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ private ConfigCatClient(String sdkKey, Options options) {
5656
options.pollingMode.getPollingIdentifier());
5757

5858
this.configService = new ConfigService(sdkKey, fetcher, options.pollingMode, options.cache, logger, options.offline, options.configCatHooks);
59+
} else {
60+
configCatHooks.invokeOnClientReady(ClientCacheState.HAS_LOCAL_OVERRIDE_FLAG_DATA_ONLY);
5961
}
6062

6163
this.defaultUser = options.defaultUser;
@@ -385,6 +387,14 @@ public ConfigCatHooks getHooks() {
385387
return this.configCatHooks;
386388
}
387389

390+
391+
@Override
392+
public CompletableFuture<ClientCacheState> waitForReadyAsync() {
393+
CompletableFuture<ClientCacheState> completableFuture = new CompletableFuture<>();
394+
getHooks().addOnClientReady((completableFuture::complete));
395+
return completableFuture;
396+
}
397+
388398
@Override
389399
public void close() throws IOException {
390400
if (!this.isClosed.compareAndSet(false, true)) {
@@ -616,18 +626,20 @@ public static ConfigCatClient get(final String sdkKey) {
616626
* @return the ConfigCatClient instance.
617627
*/
618628
public static ConfigCatClient get(String sdkKey, Consumer<Options> optionsCallback) {
619-
if (sdkKey == null || sdkKey.isEmpty()) {
620-
throw new IllegalArgumentException("SDK Key cannot be null or empty.");
621-
}
622-
Options clientOptions = new Options();
623629

630+
Options clientOptions = new Options();
624631
if (optionsCallback != null) {
625632
Options options = new Options();
626633
optionsCallback.accept(options);
627634
clientOptions = options;
628635
}
629636

637+
if (sdkKey == null || sdkKey.isEmpty()) {
638+
clientOptions.configCatHooks.invokeOnClientReady(ClientCacheState.NO_FLAG_DATA);
639+
throw new IllegalArgumentException("SDK Key cannot be null or empty.");
640+
}
630641
if (!OverrideBehaviour.LOCAL_ONLY.equals(clientOptions.overrideBehaviour) && !isValidKey(sdkKey, clientOptions.isBaseURLCustom())) {
642+
clientOptions.configCatHooks.invokeOnClientReady(ClientCacheState.NO_FLAG_DATA);
631643
throw new IllegalArgumentException("SDK Key '" + sdkKey + "' is invalid.");
632644
}
633645

src/main/java/com/configcat/ConfigCatHooks.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
import java.util.ArrayList;
44
import java.util.List;
55
import java.util.Map;
6+
import java.util.concurrent.atomic.AtomicReference;
67
import java.util.concurrent.locks.ReentrantReadWriteLock;
78
import java.util.function.Consumer;
89

910
public class ConfigCatHooks {
11+
private final AtomicReference<ClientCacheState> clientCacheState = new AtomicReference<>(null);
1012
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
1113
private final List<Consumer<Map<String, Setting>>> onConfigChanged = new ArrayList<>();
14+
private final List<Consumer<ClientCacheState>> onClientReadyWithState = new ArrayList<>();
1215
private final List<Runnable> onClientReady = new ArrayList<>();
1316
private final List<Consumer<EvaluationDetails<Object>>> onFlagEvaluated = new ArrayList<>();
1417
private final List<Consumer<String>> onError = new ArrayList<>();
@@ -22,6 +25,29 @@ public class ConfigCatHooks {
2225
*
2326
* @param callback the method to call when the event fires.
2427
*/
28+
public void addOnClientReady(Consumer<ClientCacheState> callback) {
29+
lock.writeLock().lock();
30+
try {
31+
if(clientCacheState.get() != null) {
32+
callback.accept(clientCacheState.get());
33+
} else {
34+
this.onClientReadyWithState.add(callback);
35+
}
36+
} finally {
37+
lock.writeLock().unlock();
38+
}
39+
}
40+
41+
/**
42+
* Subscribes to the onReady event. This event is fired when the SDK reaches the ready state.
43+
* If the SDK is configured with lazy load or manual polling it's considered ready right after instantiation.
44+
* In case of auto polling, the ready state is reached when the SDK has a valid config.json loaded
45+
* into memory either from cache or from HTTP. If the config couldn't be loaded neither from cache nor from HTTP the
46+
* onReady event fires when the auto polling's maxInitWaitTimeInSeconds is reached.
47+
*
48+
* @param callback the method to call when the event fires.
49+
*/
50+
@Deprecated
2551
public void addOnClientReady(Runnable callback) {
2652
lock.writeLock().lock();
2753
try {
@@ -75,9 +101,13 @@ public void addOnFlagEvaluated(Consumer<EvaluationDetails<Object>> callback) {
75101
}
76102
}
77103

78-
void invokeOnClientReady() {
104+
void invokeOnClientReady(ClientCacheState clientCacheState) {
79105
lock.readLock().lock();
80106
try {
107+
this.clientCacheState.set(clientCacheState);
108+
for (Consumer<ClientCacheState> func : this.onClientReadyWithState) {
109+
func.accept(clientCacheState);
110+
}
81111
for (Runnable func : this.onClientReady) {
82112
func.run();
83113
}

src/main/java/com/configcat/ConfigService.java

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public ConfigService(String sdkKey,
5858
lock.lock();
5959
try {
6060
if (initialized.compareAndSet(false, true)) {
61-
this.configCatHooks.invokeOnClientReady();
61+
this.configCatHooks.invokeOnClientReady(determineCacheState());
6262
String message = ConfigCatLogMessages.getAutoPollMaxInitWaitTimeReached(autoPollingMode.getMaxInitWaitTimeSeconds());
6363
this.logger.warn(4200, message);
6464
completeRunningTask(Result.error(message, cachedEntry));
@@ -69,13 +69,15 @@ public ConfigService(String sdkKey,
6969
}, autoPollingMode.getMaxInitWaitTimeSeconds(), TimeUnit.SECONDS);
7070

7171
} else {
72+
// Sync up with cache before reporting ready state
73+
cachedEntry = readCache();
7274
setInitialized();
7375
}
7476
}
7577

7678
private void setInitialized() {
7779
if (initialized.compareAndSet(false, true)) {
78-
configCatHooks.invokeOnClientReady();
80+
configCatHooks.invokeOnClientReady(determineCacheState());
7981
}
8082
}
8183

@@ -128,7 +130,7 @@ private CompletableFuture<Result<Entry>> fetchIfOlder(long threshold, boolean pr
128130
cachedEntry = fromCache;
129131
}
130132
// Cache isn't expired
131-
if (cachedEntry.getFetchTime() > threshold) {
133+
if (!cachedEntry.isExpired(threshold)) {
132134
setInitialized();
133135
return CompletableFuture.completedFuture(Result.success(cachedEntry));
134136
}
@@ -194,7 +196,6 @@ public boolean isOffline() {
194196
private void processResponse(FetchResponse response) {
195197
lock.lock();
196198
try {
197-
setInitialized();
198199
if (response.isFetched()) {
199200
Entry entry = response.entry();
200201
cachedEntry = entry;
@@ -210,6 +211,7 @@ private void processResponse(FetchResponse response) {
210211
? Result.error(response.error(), cachedEntry)
211212
: Result.success(cachedEntry));
212213
}
214+
setInitialized();
213215
} finally {
214216
lock.unlock();
215217
}
@@ -244,4 +246,22 @@ private void writeCache(Entry entry) {
244246
logger.error(2201, ConfigCatLogMessages.CONFIG_SERVICE_CACHE_WRITE_ERROR, e);
245247
}
246248
}
249+
250+
private ClientCacheState determineCacheState(){
251+
if(cachedEntry.isEmpty()) {
252+
return ClientCacheState.NO_FLAG_DATA;
253+
}
254+
if(pollingMode instanceof ManualPollingMode) {
255+
return ClientCacheState.HAS_CACHED_FLAG_DATA_ONLY;
256+
} else if(pollingMode instanceof LazyLoadingMode) {
257+
if(cachedEntry.isExpired(System.currentTimeMillis() - (((LazyLoadingMode)pollingMode).getCacheRefreshIntervalInSeconds() * 1000L))) {
258+
return ClientCacheState.HAS_CACHED_FLAG_DATA_ONLY;
259+
}
260+
} else if(pollingMode instanceof AutoPollingMode) {
261+
if(cachedEntry.isExpired(System.currentTimeMillis() - (((AutoPollingMode)pollingMode).getAutoPollRateInSeconds() * 1000L))) {
262+
return ClientCacheState.HAS_CACHED_FLAG_DATA_ONLY;
263+
}
264+
}
265+
return ClientCacheState.HAS_UP_TO_DATE_FLAG_DATA;
266+
}
247267
}

src/main/java/com/configcat/ConfigurationProvider.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,4 +254,12 @@ public interface ConfigurationProvider extends Closeable {
254254
* @return the hooks object used for event subscription.
255255
*/
256256
ConfigCatHooks getHooks();
257+
258+
/**
259+
* Awaits for SDK initialization.
260+
*
261+
* @return the future which executes the wait for ready and return with the client state.
262+
*/
263+
CompletableFuture<ClientCacheState> waitForReadyAsync();
264+
257265
}

src/main/java/com/configcat/Constants.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ private Constants() { /* prevent from instantiation*/ }
77
static final long DISTANT_PAST = 0;
88
static final String CONFIG_JSON_NAME = "config_v6.json";
99
static final String SERIALIZATION_FORMAT_VERSION = "v2";
10-
static final String VERSION = "9.1.3";
10+
static final String VERSION = "9.2.0";
1111

1212
static final String SDK_KEY_PROXY_PREFIX = "configcat-proxy/";
1313
static final String SDK_KEY_PREFIX = "configcat-sdk-1";

src/main/java/com/configcat/Entry.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ public Entry withFetchTime(long fetchTime) {
2626
return new Entry(getConfig(), getETag(), getConfigJson(), fetchTime);
2727
}
2828

29+
public boolean isExpired(long threshold) {
30+
return fetchTime <= threshold ;
31+
}
2932
public Entry(Config config, String eTag, String configJson, long fetchTime) {
3033
this.config = config;
3134
this.eTag = eTag;

src/test/java/com/configcat/ConfigCatClientTest.java

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.time.Duration;
1515
import java.time.Instant;
1616
import java.util.*;
17+
import java.util.concurrent.CompletableFuture;
1718
import java.util.concurrent.ExecutionException;
1819
import java.util.concurrent.TimeUnit;
1920
import java.util.concurrent.atomic.AtomicBoolean;
@@ -752,22 +753,22 @@ void testHooks() throws IOException {
752753
server.enqueue(new MockResponse().setResponseCode(500).setBody(""));
753754

754755
AtomicBoolean changed = new AtomicBoolean(false);
755-
AtomicBoolean ready = new AtomicBoolean(false);
756+
AtomicReference<ClientCacheState> ready = new AtomicReference(null);
756757
AtomicReference<String> error = new AtomicReference<>("");
757758

758759
ConfigCatClient cl = ConfigCatClient.get(Helpers.SDK_KEY, options -> {
759760
options.pollingMode(PollingModes.manualPoll());
760761
options.baseUrl(server.url("/").toString());
761762
options.hooks().addOnConfigChanged(map -> changed.set(true));
762-
options.hooks().addOnClientReady(() -> ready.set(true));
763+
options.hooks().addOnClientReady(clientReadyState -> ready.set(clientReadyState));
763764
options.hooks().addOnError(error::set);
764765
});
765766

766767
cl.forceRefresh();
767768
cl.forceRefresh();
768769

769770
assertTrue(changed.get());
770-
assertTrue(ready.get());
771+
assertEquals(ClientCacheState.NO_FLAG_DATA, ready.get());
771772
assertEquals("Unexpected HTTP response was received while trying to fetch config JSON: 500 Server Error", error.get());
772773

773774
server.shutdown();
@@ -803,6 +804,38 @@ void testHooksSub() throws IOException {
803804
cl.close();
804805
}
805806

807+
@Test
808+
void testReadyHookManualPollWithCache() throws IOException {
809+
810+
AtomicReference<ClientCacheState> ready = new AtomicReference(null);
811+
ConfigCache cache = new SingleValueCache(Helpers.cacheValueFromConfigJson(String.format(TEST_JSON, "test")));
812+
813+
ConfigCatClient cl = ConfigCatClient.get(Helpers.SDK_KEY, options -> {
814+
options.pollingMode(PollingModes.manualPoll());
815+
options.cache(cache);
816+
options.hooks().addOnClientReady(clientReadyState -> ready.set(clientReadyState));
817+
});
818+
819+
assertEquals(ClientCacheState.HAS_CACHED_FLAG_DATA_ONLY, ready.get());
820+
821+
cl.close();
822+
}
823+
824+
@Test
825+
void testReadyHookLocalOnly() throws IOException {
826+
AtomicReference<ClientCacheState> ready = new AtomicReference(null);
827+
828+
ConfigCatClient cl = ConfigCatClient.get(Helpers.SDK_KEY, options -> {
829+
options.pollingMode(PollingModes.manualPoll());
830+
options.flagOverrides(OverrideDataSourceBuilder.map(Collections.EMPTY_MAP), OverrideBehaviour.LOCAL_ONLY);
831+
options.hooks().addOnClientReady(clientReadyState -> ready.set(clientReadyState));
832+
});
833+
834+
assertEquals(ClientCacheState.HAS_LOCAL_OVERRIDE_FLAG_DATA_ONLY, ready.get());
835+
836+
cl.close();
837+
}
838+
806839
@Test
807840
void testHooksAutoPollSub() throws IOException {
808841
MockWebServer server = new MockWebServer();
@@ -812,7 +845,7 @@ void testHooksAutoPollSub() throws IOException {
812845
server.enqueue(new MockResponse().setResponseCode(500).setBody(""));
813846

814847
AtomicBoolean changed = new AtomicBoolean(false);
815-
AtomicBoolean ready = new AtomicBoolean(false);
848+
AtomicReference<ClientCacheState> ready = new AtomicReference(null);
816849
AtomicReference<String> error = new AtomicReference<>("");
817850

818851
ConfigCatClient cl = ConfigCatClient.get(Helpers.SDK_KEY, options -> {
@@ -821,14 +854,14 @@ void testHooksAutoPollSub() throws IOException {
821854
});
822855

823856
cl.getHooks().addOnConfigChanged(map -> changed.set(true));
824-
cl.getHooks().addOnClientReady(() -> ready.set(true));
857+
cl.getHooks().addOnClientReady(clientReadyState -> ready.set(clientReadyState));
825858
cl.getHooks().addOnError(error::set);
826859

827860
cl.forceRefresh();
828861
cl.forceRefresh();
829862

830863
assertTrue(changed.get());
831-
assertTrue(ready.get());
864+
assertEquals(ClientCacheState.HAS_UP_TO_DATE_FLAG_DATA, ready.get());
832865
assertEquals("Unexpected HTTP response was received while trying to fetch config JSON: 500 Server Error", error.get());
833866

834867
server.shutdown();
@@ -980,4 +1013,26 @@ void testGetValueInvalidTypes(String settingKey, Class callType, Object defaultV
9801013
cl.close();
9811014
}
9821015

1016+
@Test
1017+
void testWaitForReady() throws IOException, InterruptedException, ExecutionException {
1018+
MockWebServer server = new MockWebServer();
1019+
server.start();
1020+
1021+
server.enqueue(new MockResponse().setResponseCode(200).setBody(TEST_JSON));
1022+
1023+
ConfigCatClient cl = ConfigCatClient.get(Helpers.SDK_KEY, options -> {
1024+
options.pollingMode(PollingModes.autoPoll(2));
1025+
options.baseUrl(server.url("/").toString());
1026+
});
1027+
1028+
CompletableFuture<ClientCacheState> clientReadyStateCompletableFuture = cl.waitForReadyAsync();
1029+
if(clientReadyStateCompletableFuture.isDone()) {
1030+
assertEquals(clientReadyStateCompletableFuture.get(), ClientCacheState.HAS_UP_TO_DATE_FLAG_DATA);
1031+
}
1032+
1033+
server.shutdown();
1034+
cl.close();
1035+
}
1036+
1037+
9831038
}

0 commit comments

Comments
 (0)