Skip to content

Commit

Permalink
Hooks update (#52)
Browse files Browse the repository at this point in the history
* Replace hooks sync with ReentrantReadWriteLock.

* Rework clientReady hook.
Added waitForReady method.

* Deprecated original addOnClientReady hook.
Prepare to release 10.2.0
  • Loading branch information
novalisdenahi authored Jun 7, 2024
1 parent eb7326a commit d2e5904
Show file tree
Hide file tree
Showing 11 changed files with 232 additions and 34 deletions.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
version=10.1.2
version=10.2.0

org.gradle.jvmargs=-Xmx2g
23 changes: 23 additions & 0 deletions src/main/java/com/configcat/ClientCacheState.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.configcat;

/**
* Describes the Client state.
*/
public enum ClientCacheState {
/**
* The SDK has no feature flag data neither from the cache nor from the ConfigCat CDN.
*/
NO_FLAG_DATA,
/**
* The SDK runs with local only feature flag data.
*/
HAS_LOCAL_OVERRIDE_FLAG_DATA_ONLY,
/**
* The SDK has feature flag data to work with only from the cache.
*/
HAS_CACHED_FLAG_DATA_ONLY,
/**
* The SDK works with the latest feature flag data received from the ConfigCat CDN.
*/
HAS_UP_TO_DATE_FLAG_DATA,
}
23 changes: 20 additions & 3 deletions src/main/java/com/configcat/ConfigCatClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ private ConfigCatClient(String sdkKey, Options options) throws IllegalArgumentEx
options.pollingMode.getPollingIdentifier());

this.configService = new ConfigService(sdkKey, options.pollingMode, options.cache, logger, fetcher, options.hooks, options.offline);
} else {
this.hooks.invokeOnClientReady(ClientCacheState.HAS_LOCAL_OVERRIDE_FLAG_DATA_ONLY);
}
}

Expand Down Expand Up @@ -594,8 +596,7 @@ public static ConfigCatClient get(String sdkKey) {
* @return the ConfigCatClient instance.
*/
public static ConfigCatClient get(String sdkKey, Consumer<Options> optionsCallback) {
if (sdkKey == null || sdkKey.isEmpty())
throw new IllegalArgumentException("SDK Key cannot be null or empty.");


Options clientOptions = new Options();

Expand All @@ -605,8 +606,17 @@ public static ConfigCatClient get(String sdkKey, Consumer<Options> optionsCallba
clientOptions = options;
}

if (!OverrideBehaviour.LOCAL_ONLY.equals(clientOptions.overrideBehaviour) && !isValidKey(sdkKey, clientOptions.isBaseURLCustom()))
if (sdkKey == null || sdkKey.isEmpty()) {
clientOptions.hooks.invokeOnClientReady(ClientCacheState.NO_FLAG_DATA);
throw new IllegalArgumentException("SDK Key cannot be null or empty.");
}


if (!OverrideBehaviour.LOCAL_ONLY.equals(clientOptions.overrideBehaviour) && !isValidKey(sdkKey, clientOptions.isBaseURLCustom())) {
clientOptions.hooks.invokeOnClientReady(ClientCacheState.NO_FLAG_DATA);
throw new IllegalArgumentException("SDK Key '" + sdkKey + "' is invalid.");
}


synchronized (INSTANCES) {
ConfigCatClient client = INSTANCES.get(sdkKey);
Expand Down Expand Up @@ -649,6 +659,13 @@ public static void closeAll() throws IOException {
}
}

@Override
public CompletableFuture<ClientCacheState> waitForReadyAsync() {
CompletableFuture<ClientCacheState> completableFuture = new CompletableFuture<>();
getHooks().addOnClientReady((completableFuture::complete));
return completableFuture;
}

/**
* Configuration options for a {@link ConfigCatClient} instance.
*/
Expand Down
83 changes: 70 additions & 13 deletions src/main/java/com/configcat/ConfigCatHooks.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ConfigCatHooks {
private final Object sync = new Object();
private final AtomicReference<ClientCacheState> clientCacheState = new AtomicReference<>(null);
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
private final List<Consumer<Map<String, Setting>>> onConfigChanged = new ArrayList<>();
private final List<Runnable> onClientReady = new ArrayList<>();
private final List<Consumer<EvaluationDetails<Object>>> onFlagEvaluated = new ArrayList<>();
private final List<Consumer<ClientCacheState>> onClientReadyWithState = new ArrayList<>();
private final List<Runnable> onClientReady = new ArrayList<>(); private final List<Consumer<EvaluationDetails<Object>>> onFlagEvaluated = new ArrayList<>();
private final List<Consumer<String>> onError = new ArrayList<>();

/**
Expand All @@ -22,9 +25,35 @@ public class ConfigCatHooks {
*
* @param callback the method to call when the event fires.
*/
public void addOnClientReady(Consumer<ClientCacheState> callback) {
lock.writeLock().lock();
try {
if(clientCacheState.get() != null) {
callback.accept(clientCacheState.get());
} else {
this.onClientReadyWithState.add(callback);
}
} finally {
lock.writeLock().unlock();
}
}

/**
* Subscribes to the onReady event. This event is fired when the SDK reaches the ready state.
* If the SDK is configured with lazy load or manual polling it's considered ready right after instantiation.
* In case of auto polling, the ready state is reached when the SDK has a valid config.json loaded
* into memory either from cache or from HTTP. If the config couldn't be loaded neither from cache nor from HTTP the
* onReady event fires when the auto polling's maxInitWaitTimeInSeconds is reached.
*
* @param callback the method to call when the event fires.
*/
@Deprecated
public void addOnClientReady(Runnable callback) {
synchronized (sync) {
lock.writeLock().lock();
try {
this.onClientReady.add(callback);
} finally {
lock.writeLock().unlock();
}
}

Expand All @@ -35,8 +64,11 @@ public void addOnClientReady(Runnable callback) {
* @param callback the method to call when the event fires.
*/
public void addOnConfigChanged(Consumer<Map<String, Setting>> callback) {
synchronized (sync) {
lock.writeLock().lock();
try {
this.onConfigChanged.add(callback);
} finally {
lock.writeLock().unlock();
}
}

Expand All @@ -46,8 +78,11 @@ public void addOnConfigChanged(Consumer<Map<String, Setting>> callback) {
* @param callback the method to call when the event fires.
*/
public void addOnError(Consumer<String> callback) {
synchronized (sync) {
lock.writeLock().lock();
try {
this.onError.add(callback);
} finally {
lock.writeLock().unlock();
}
}

Expand All @@ -58,49 +93,71 @@ public void addOnError(Consumer<String> callback) {
* @param callback the method to call when the event fires.
*/
public void addOnFlagEvaluated(Consumer<EvaluationDetails<Object>> callback) {
synchronized (sync) {
lock.writeLock().lock();
try {
this.onFlagEvaluated.add(callback);
} finally {
lock.writeLock().unlock();
}
}

void invokeOnClientReady() {
synchronized (sync) {
void invokeOnClientReady(ClientCacheState clientCacheState) {
lock.readLock().lock();
try {
this.clientCacheState.set(clientCacheState);
for (Consumer<ClientCacheState> func : this.onClientReadyWithState) {
func.accept(clientCacheState);
}
for (Runnable func : this.onClientReady) {
func.run();
}
} finally {
lock.readLock().unlock();
}
}

void invokeOnError(String error) {
synchronized (sync) {
lock.readLock().lock();
try {
for (Consumer<String> func : this.onError) {
func.accept(error);
}
} finally {
lock.readLock().unlock();
}
}

void invokeOnConfigChanged(Map<String, Setting> settingMap) {
synchronized (sync) {
lock.readLock().lock();
try {
for (Consumer<Map<String, Setting>> func : this.onConfigChanged) {
func.accept(settingMap);
}
} finally {
lock.readLock().unlock();
}
}

void invokeOnFlagEvaluated(EvaluationDetails<Object> evaluationDetails) {
synchronized (sync) {
lock.readLock().lock();
try {
for (Consumer<EvaluationDetails<Object>> func : this.onFlagEvaluated) {
func.accept(evaluationDetails);
}
} finally {
lock.readLock().unlock();
}
}

void clear() {
synchronized (sync) {
lock.writeLock().lock();
try {
this.onConfigChanged.clear();
this.onError.clear();
this.onFlagEvaluated.clear();
this.onClientReady.clear();
} finally {
lock.writeLock().unlock();
}
}
}
28 changes: 24 additions & 4 deletions src/main/java/com/configcat/ConfigService.java
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public ConfigService(String sdkKey,
lock.lock();
try {
if (initialized.compareAndSet(false, true)) {
hooks.invokeOnClientReady();
hooks.invokeOnClientReady(determineCacheState());
String message = ConfigCatLogMessages.getAutoPollMaxInitWaitTimeReached(autoPollingMode.getMaxInitWaitTimeSeconds());
logger.warn(4200, message);
completeRunningTask(Result.error(message, cachedEntry));
Expand All @@ -91,6 +91,8 @@ public ConfigService(String sdkKey,
}
}, autoPollingMode.getMaxInitWaitTimeSeconds(), TimeUnit.SECONDS);
} else {
// Sync up with cache before reporting ready state
cachedEntry = readCache();
setInitialized();
}
}
Expand Down Expand Up @@ -165,7 +167,7 @@ private CompletableFuture<Result<Entry>> fetchIfOlder(long threshold, boolean pr
cachedEntry = fromCache;
}
// Cache isn't expired
if (cachedEntry.getFetchTime() > threshold) {
if (!cachedEntry.isExpired(threshold)) {
setInitialized();
return CompletableFuture.completedFuture(Result.success(cachedEntry));
}
Expand All @@ -191,7 +193,6 @@ private CompletableFuture<Result<Entry>> fetchIfOlder(long threshold, boolean pr
private void processResponse(FetchResponse response) {
lock.lock();
try {
setInitialized();
if (response.isFetched()) {
Entry entry = response.entry();
cachedEntry = entry;
Expand All @@ -207,6 +208,7 @@ private void processResponse(FetchResponse response) {
? Result.error(response.error(), cachedEntry)
: Result.success(cachedEntry));
}
setInitialized();
} finally {
lock.unlock();
}
Expand All @@ -219,7 +221,7 @@ private void completeRunningTask(Result<Entry> result) {

private void setInitialized() {
if (initialized.compareAndSet(false, true)) {
hooks.invokeOnClientReady();
hooks.invokeOnClientReady(determineCacheState());
}
}

Expand Down Expand Up @@ -255,6 +257,24 @@ private Entry readCache() {
}
}

private ClientCacheState determineCacheState(){
if(cachedEntry.isEmpty()) {
return ClientCacheState.NO_FLAG_DATA;
}
if(mode instanceof ManualPollingMode) {
return ClientCacheState.HAS_CACHED_FLAG_DATA_ONLY;
} else if(mode instanceof LazyLoadingMode) {
if(cachedEntry.isExpired(System.currentTimeMillis() - (((LazyLoadingMode)mode).getCacheRefreshIntervalInSeconds() * 1000L))) {
return ClientCacheState.HAS_CACHED_FLAG_DATA_ONLY;
}
} else if(mode instanceof AutoPollingMode) {
if(cachedEntry.isExpired(System.currentTimeMillis() - (((AutoPollingMode)mode).getAutoPollRateInSeconds() * 1000L))) {
return ClientCacheState.HAS_CACHED_FLAG_DATA_ONLY;
}
}
return ClientCacheState.HAS_UP_TO_DATE_FLAG_DATA;
}

@Override
public void close() throws IOException {
if (!this.closed.compareAndSet(false, true)) {
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/com/configcat/ConfigurationProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -255,4 +255,11 @@ public interface ConfigurationProvider extends Closeable {
* @return True if the client is closed.
*/
boolean isClosed();

/**
* Awaits for SDK initialization.
*
* @return the future which executes the wait for ready and return with the client state.
*/
CompletableFuture<ClientCacheState> waitForReadyAsync();
}
4 changes: 4 additions & 0 deletions src/main/java/com/configcat/Entry.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ boolean isEmpty() {
return EMPTY.equals(this);
}

public boolean isExpired(long threshold) {
return fetchTime <= threshold ;
}

public static final Entry EMPTY = new Entry(Config.EMPTY, "", "", Constants.DISTANT_PAST);

public String serialize() {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/configcat/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ private Constants() { /* prevent from instantiation*/ }
static final long DISTANT_PAST = 0;
static final String CONFIG_JSON_NAME = "config_v6.json";
static final String SERIALIZATION_FORMAT_VERSION = "v2";
static final String VERSION = "10.1.2";
static final String VERSION = "10.2.0";

static final String SDK_KEY_PROXY_PREFIX = "configcat-proxy/";
static final String SDK_KEY_PREFIX = "configcat-sdk-1";
Expand Down
9 changes: 6 additions & 3 deletions src/test/java/com/configcat/AutoPollingTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
Expand Down Expand Up @@ -249,9 +250,9 @@ void testPollsWhenCacheExpired() throws Exception {
void testNonExpiredCacheCallsReady() throws Exception {
ConfigCache cache = new SingleValueCache(Helpers.cacheValueFromConfigJson(String.format(TEST_JSON, "test")));

AtomicBoolean ready = new AtomicBoolean(false);
AtomicReference<ClientCacheState> ready = new AtomicReference(null);
ConfigCatHooks hooks = new ConfigCatHooks();
hooks.addOnClientReady(() -> ready.set(true));
hooks.addOnClientReady(clientReadyState -> ready.set(clientReadyState));
PollingMode pollingMode = PollingModes.autoPoll(2);
ConfigFetcher fetcher = new ConfigFetcher(new OkHttpClient(),
logger,
Expand All @@ -263,7 +264,9 @@ void testNonExpiredCacheCallsReady() throws Exception {

assertEquals(0, this.server.getRequestCount());

Helpers.waitFor(ready::get);
Helpers.waitForClientCacheState(2000, ready::get);

assertEquals(ClientCacheState.HAS_UP_TO_DATE_FLAG_DATA, ready.get());

policy.close();
}
Expand Down
Loading

0 comments on commit d2e5904

Please sign in to comment.