Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 82 additions & 34 deletions src/main/java/com/configcat/ConfigCatClient.java

Large diffs are not rendered by default.

33 changes: 21 additions & 12 deletions src/main/java/com/configcat/ConfigFetcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -124,19 +124,22 @@ public void onFailure(@NotNull Call call, @NotNull IOException e) {
logger.debug(ConfigCatLogMessages.getDebugEnabledRequestFailed(requestId));
}
int logEventId = 1103;
RefreshErrorCode errorCode = RefreshErrorCode.HTTP_REQUEST_FAILURE;
Object message = ConfigCatLogMessages.getFetchFailedDueToUnexpectedError(null);
if (!isClosed.get()) {
if (e instanceof SocketTimeoutException) {
logEventId = 1102;
message = ConfigCatLogMessages.getFetchFailedDueToRequestTimeout(httpClient.connectTimeoutMillis(), httpClient.readTimeoutMillis(), httpClient.writeTimeoutMillis(), null);
errorCode = RefreshErrorCode.HTTP_REQUEST_TIMEOUT;
}
logger.error(logEventId, message, e);
}
fetchResponse = FetchResponse.failed(message, false, null, true);
fetchResponse = FetchResponse.failed(message, errorCode, e, false, null, true);
} finally {
if(fetchResponse == null) {
FormattableLogMessage formattableLogMessage = ConfigCatLogMessages.getFetchFailedDueToUnexpectedError(null);
fetchResponse = FetchResponse.failed(formattableLogMessage,false, null, false);
fetchResponse = FetchResponse.failed(formattableLogMessage, RefreshErrorCode.UNEXPECTED_ERROR,
null, false, null, false);
}
future.complete(fetchResponse);
}
Expand All @@ -159,9 +162,10 @@ public void onResponse(@NotNull Call call, @NotNull Response response) {

logger.debug(ConfigCatLogMessages.getDebugEnabledReceivedBody(requestId, content.length()));
}
Result<Config> result = deserializeConfig(content, cfRayId);
Result<Config, RefreshErrorCode> result = deserializeConfig(content, cfRayId);
if (result.error() != null) {
fetchResponse = FetchResponse.failed(result.error(), false, cfRayId, false);
fetchResponse = FetchResponse.failed(result.error(), result.errorCode(),
result.errorException(), false, cfRayId, false);
} else {
fetchResponse = FetchResponse.fetched(new Entry(result.value(), eTag, content, System.currentTimeMillis()), cfRayId);
logger.debug("Fetch was successful: new config fetched.");
Expand All @@ -175,34 +179,39 @@ public void onResponse(@NotNull Call call, @NotNull Response response) {
}
} else if (responseCode == 403 || responseCode == 404) {
FormattableLogMessage message = ConfigCatLogMessages.getFetchFailedDueToInvalidSDKKey(cfRayId);
fetchResponse = FetchResponse.failed(message, true, cfRayId, false);
fetchResponse = FetchResponse.failed(message, RefreshErrorCode.INVALID_SDK_KEY,
null, true, cfRayId, false);
logger.error(1100, message);
} else {
if (isDebugLoggingEnabled){
logger.debug(ConfigCatLogMessages.getDebugEnabledReceivedUnexpectedStatusCode(requestId));
}
FormattableLogMessage formattableLogMessage = ConfigCatLogMessages.getFetchFailedDueToUnexpectedHttpResponse(responseCode, response.message(), cfRayId);
fetchResponse = FetchResponse.failed(formattableLogMessage, false, cfRayId, true);
fetchResponse = FetchResponse.failed(formattableLogMessage,
RefreshErrorCode.UNEXPECTED_HTTP_RESPONSE, null, false, cfRayId, true);
logger.error(1101, formattableLogMessage);
}
} catch (SocketTimeoutException e) {
if (isDebugLoggingEnabled) {
logger.debug(ConfigCatLogMessages.getDebugEnabledRequestTimedOut(requestId));
}
FormattableLogMessage formattableLogMessage = ConfigCatLogMessages.getFetchFailedDueToRequestTimeout(httpClient.connectTimeoutMillis(), httpClient.readTimeoutMillis(), httpClient.writeTimeoutMillis(), cfRayId);
fetchResponse = FetchResponse.failed(formattableLogMessage, false, cfRayId, true);
fetchResponse = FetchResponse.failed(formattableLogMessage,
RefreshErrorCode.HTTP_REQUEST_TIMEOUT, e, false, cfRayId, true);
logger.error(1102, formattableLogMessage, e);
} catch (Exception e) {
if (isDebugLoggingEnabled) {
logger.debug(ConfigCatLogMessages.getDebugEnabledRequestFailed(requestId));
}
FormattableLogMessage formattableLogMessage = ConfigCatLogMessages.getFetchFailedDueToUnexpectedError(cfRayId);
fetchResponse = FetchResponse.failed(formattableLogMessage, false, cfRayId, true);
fetchResponse = FetchResponse.failed(formattableLogMessage,
RefreshErrorCode.HTTP_REQUEST_FAILURE, e, false, cfRayId, true);
logger.error(1103, formattableLogMessage, e);
} finally {
if(fetchResponse == null) {
FormattableLogMessage formattableLogMessage = ConfigCatLogMessages.getFetchFailedDueToUnexpectedError(cfRayId);
fetchResponse = FetchResponse.failed(formattableLogMessage,false, cfRayId, false);
fetchResponse = FetchResponse.failed(formattableLogMessage,
RefreshErrorCode.UNEXPECTED_ERROR, null, false, cfRayId, false);
}
future.complete(fetchResponse);
}
Expand Down Expand Up @@ -290,13 +299,13 @@ private String getProxyAddress() {
return proxy.type() + " @ " + proxy.address();
}

private Result<Config> deserializeConfig(String json, String cfRayId) {
private Result<Config, RefreshErrorCode> deserializeConfig(String json, String cfRayId) {
try {
return Result.success(Utils.deserializeConfig(json));
return Result.success(Utils.deserializeConfig(json), RefreshErrorCode.NONE);
} catch (Exception e) {
FormattableLogMessage message = ConfigCatLogMessages.getFetchReceived200WithInvalidBodyError(cfRayId);
this.logger.error(1105, message, e);
return Result.error(message, null);
return Result.error(message, null, RefreshErrorCode.INVALID_HTTP_RESPONSE_CONTENT, e);
}
}
}
28 changes: 17 additions & 11 deletions src/main/java/com/configcat/ConfigService.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public class ConfigService implements Closeable {
private final PollingMode pollingMode;
private ScheduledExecutorService pollScheduler;
private ScheduledExecutorService initScheduler;
private CompletableFuture<Result<Entry>> runningTask;
private CompletableFuture<Result<Entry, RefreshErrorCode>> runningTask;
private final AtomicBoolean initialized = new AtomicBoolean(false);
private final AtomicBoolean closed = new AtomicBoolean(false);
private final AtomicBoolean offline;
Expand Down Expand Up @@ -58,7 +58,8 @@ public ConfigService(String sdkKey,
this.configCatHooks.invokeOnClientReady(determineCacheState(cachedEntry.get()));
FormattableLogMessage formattableLogMessage = ConfigCatLogMessages.getAutoPollMaxInitWaitTimeReached(autoPollingMode.getMaxInitWaitTimeSeconds());
this.logger.warn(4200, formattableLogMessage);
completeRunningTask(Result.error(formattableLogMessage, cachedEntry.get()));
completeRunningTask(Result.error(formattableLogMessage, cachedEntry.get(),
RefreshErrorCode.CLIENT_INIT_TIMED_OUT, null));
} finally {
lock.unlock();
}
Expand Down Expand Up @@ -90,10 +91,15 @@ public CompletableFuture<RefreshResult> refresh() {
if (offline.get()) {
String offlineWarning = ConfigCatLogMessages.CONFIG_SERVICE_CANNOT_INITIATE_HTTP_CALLS_WARN;
logger.warn(3200, offlineWarning);
return CompletableFuture.completedFuture(new RefreshResult(false, offlineWarning));
return CompletableFuture.completedFuture(new RefreshResult(false, offlineWarning,
RefreshErrorCode.OFFLINE_CLIENT, null));
}
return fetchIfOlder(Constants.DISTANT_FUTURE, false)
.thenApply(entryResult -> new RefreshResult(entryResult.error() == null, entryResult.error()));
.thenApply(entryResult -> {
boolean success = entryResult.error() == null;
return new RefreshResult(success, entryResult.error(),
success ? RefreshErrorCode.NONE : entryResult.errorCode(), entryResult.errorException());
});
}

public CompletableFuture<SettingResult> getSettings() {
Expand All @@ -117,7 +123,7 @@ public CompletableFuture<SettingResult> getSettings() {

}

private CompletableFuture<Result<Entry>> fetchIfOlder(long threshold, boolean preferCached) {
private CompletableFuture<Result<Entry, RefreshErrorCode>> fetchIfOlder(long threshold, boolean preferCached) {
// Sync up with the cache and use it when it's not expired.
Entry fromCache = readCache();
if (!fromCache.isEmpty() && !fromCache.getETag().equals(cachedEntry.get().getETag()) && fromCache.getFetchTime() > cachedEntry.get().getFetchTime()) {
Expand All @@ -127,11 +133,11 @@ private CompletableFuture<Result<Entry>> fetchIfOlder(long threshold, boolean pr
// Cache isn't expired
if (cachedEntry.get().getFetchTime() > threshold) {
setInitialized();
return CompletableFuture.completedFuture(Result.success(cachedEntry.get()));
return CompletableFuture.completedFuture(Result.success(cachedEntry.get(), RefreshErrorCode.NONE));
}
// If we are in offline mode or the caller prefers cached values, do not initiate fetch.
if (offline.get() || preferCached) {
return CompletableFuture.completedFuture(Result.success(cachedEntry.get()));
return CompletableFuture.completedFuture(Result.success(cachedEntry.get(), RefreshErrorCode.NONE));
}

lock.lock();
Expand Down Expand Up @@ -199,23 +205,23 @@ private void processResponse(FetchResponse response) {
cachedEntry.set(entry);
writeCache(entry);
configCatHooks.invokeOnConfigChanged(entry.getConfig().getEntries());
completeRunningTask(Result.success(entry));
completeRunningTask(Result.success(entry, RefreshErrorCode.NONE));
} else {
if (response.isFetchTimeUpdatable()) {
cachedEntry.set(previousEntry.withFetchTime(System.currentTimeMillis()));
writeCache(cachedEntry.get());
}
completeRunningTask(response.isFailed()
? Result.error(response.error(), cachedEntry.get())
: Result.success(cachedEntry.get()));
? Result.error(response.error(), cachedEntry.get(), response.errorCode(), response.errorException())
: Result.success(cachedEntry.get(), RefreshErrorCode.NONE));
}
setInitialized();
} finally {
lock.unlock();
}
}

private void completeRunningTask(Result<Entry> result) {
private void completeRunningTask(Result<Entry, RefreshErrorCode> result) {
runningTask.complete(result);
runningTask = null;
}
Expand Down
32 changes: 28 additions & 4 deletions src/main/java/com/configcat/EvaluationDetails.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public class EvaluationDetails<T> {
private final User user;
private final boolean isDefaultValue;
private final Object error;
private final EvaluationErrorCode errorCode;
private final Throwable errorException;
private final long fetchTimeUnixMilliseconds;
private final TargetingRule matchedTargetingRule;
private final PercentageOption matchedPercentageOption;
Expand All @@ -20,6 +22,8 @@ public EvaluationDetails(T value,
User user,
boolean isDefaultValue,
Object error,
EvaluationErrorCode errorCode,
Throwable errorException,
long fetchTimeUnixMilliseconds,
TargetingRule matchedTargetingRule,
PercentageOption matchedPercentageOption) {
Expand All @@ -29,17 +33,22 @@ public EvaluationDetails(T value,
this.user = user;
this.isDefaultValue = isDefaultValue;
this.error = error;
this.errorCode = errorCode;
this.errorException = errorException;
this.fetchTimeUnixMilliseconds = fetchTimeUnixMilliseconds;
this.matchedTargetingRule = matchedTargetingRule;
this.matchedPercentageOption = matchedPercentageOption;
}

static <T> EvaluationDetails<T> fromError(String key, T defaultValue, Object error, User user) {
return new EvaluationDetails<>(defaultValue, key, "", user, true, error, Constants.DISTANT_PAST, null, null);
static <T> EvaluationDetails<T> fromError(String key, T defaultValue, EvaluationErrorCode errorCode,
Object error, Throwable errorException, User user) {
return new EvaluationDetails<>(defaultValue, key, "", user, true, error, errorCode, errorException,
Constants.DISTANT_PAST, null, null);
}

<TR> EvaluationDetails<TR> asTypeSpecific() {
return new EvaluationDetails<>((TR) value, key, variationId, user, isDefaultValue, error, fetchTimeUnixMilliseconds, matchedTargetingRule, matchedPercentageOption);
return new EvaluationDetails<>((TR) value, key, variationId, user, isDefaultValue, error, errorCode,
errorException, fetchTimeUnixMilliseconds, matchedTargetingRule, matchedPercentageOption);
}

/**
Expand Down Expand Up @@ -81,12 +90,27 @@ public boolean isDefaultValue() {
* In case of an error, this field contains the error message.
*/
public String getError() {
if(error != null) {
if (error != null) {
return error.toString();
}
return null;
}

/**
* The code identifying the reason for the error in case the operation failed.
* If the evaluation was successful, this will be {@link EvaluationErrorCode#NONE}.
*/
public EvaluationErrorCode getErrorCode() {
return errorCode;
}

/**
* The exception object related to the error in case the operation failed. (If the evaluation was successful, this will be null.)
*/
Comment thread
adams85 marked this conversation as resolved.
Resolved
public Throwable getErrorException() {
return errorException;
}

/**
* The last fetch time of the config.json in unix milliseconds format.
*/
Expand Down
55 changes: 55 additions & 0 deletions src/main/java/com/configcat/EvaluationErrorCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.configcat;

/**
* Specifies the possible evaluation error codes.
*/
public enum EvaluationErrorCode implements ErrorCode {

/** An unexpected error occurred during the evaluation. */
UNEXPECTED_ERROR(-1),

/** No error occurred (the evaluation was successful). */
NONE(0),

/**
* The evaluation failed because of an error in the config model.
* (Most likely, invalid data was passed to the SDK via flag overrides.)
*/
INVALID_CONFIG_MODEL(1),

/**
* The evaluation failed because of a type mismatch between the evaluated
* setting value and the specified default value.
*/
SETTING_VALUE_TYPE_MISMATCH(2),

/** The evaluation failed because the config JSON was not available locally. */
CONFIG_JSON_NOT_AVAILABLE(1000),

/**
* The evaluation failed because the key of the evaluated setting was not found in
* the config JSON.
*/
SETTING_KEY_MISSING(1001);

public final int code;

Check failure on line 35 in src/main/java/com/configcat/EvaluationErrorCode.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename field "code" to prevent any misunderstanding/clash with method "code".

See more on https://sonarcloud.io/project/issues?id=configcat_java-sdk&issues=AaCAUf9NUcM4pixfwaOI&open=AaCAUf9NUcM4pixfwaOI&pullRequest=84

EvaluationErrorCode(int code) {
this.code = code;
}

@Override
public int code() {
return this.code;
}

public static EvaluationErrorCode fromException(Throwable exception) {
if (exception instanceof InvalidConfigModelException) {
return INVALID_CONFIG_MODEL;
}
if (exception instanceof EvaluationException) {
return SETTING_VALUE_TYPE_MISMATCH;
}
return UNEXPECTED_ERROR;
}
}
23 changes: 18 additions & 5 deletions src/main/java/com/configcat/FetchResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
private final Status status;
private final Entry entry;
private final Object error;
private final RefreshErrorCode errorCode;
private final Throwable errorException;
private final boolean fetchTimeUpdatable;
private final String cfRayId;
private final boolean shouldRetry;
Expand Down Expand Up @@ -38,28 +40,39 @@
return error;
}

public RefreshErrorCode errorCode() {return this.errorCode;}

public Throwable errorException() {return this.errorException;}

public String cfRayId() {return this.cfRayId;}

public boolean shouldRetry() {return shouldRetry;}

FetchResponse(Status status, Entry entry, Object error, boolean fetchTimeUpdatable, String cfRayId, boolean shouldRetry) {
FetchResponse(Status status, Entry entry, Object error, RefreshErrorCode errorCode, Throwable errorException,

Check warning on line 51 in src/main/java/com/configcat/FetchResponse.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Constructor has 8 parameters, which is greater than 7 authorized.

See more on https://sonarcloud.io/project/issues?id=configcat_java-sdk&issues=AaCAUf_1UcM4pixfwaOJ&open=AaCAUf_1UcM4pixfwaOJ&pullRequest=84
boolean fetchTimeUpdatable, String cfRayId, boolean shouldRetry) {
this.status = status;
this.entry = entry;
this.error = error;
this.errorCode = errorCode;
this.errorException = errorException;
this.fetchTimeUpdatable = fetchTimeUpdatable;
this.cfRayId = cfRayId;
this.shouldRetry = shouldRetry;
}

public static FetchResponse fetched(Entry entry, String cfRayId) {
return new FetchResponse(Status.FETCHED, entry == null ? Entry.EMPTY : entry, null, false, cfRayId, false);
return new FetchResponse(Status.FETCHED, entry == null ? Entry.EMPTY : entry, null,
RefreshErrorCode.NONE, null, false, cfRayId, false);
}

public static FetchResponse notModified(String cfRayId) {
return new FetchResponse(Status.NOT_MODIFIED, Entry.EMPTY, null, true, cfRayId, false);
return new FetchResponse(Status.NOT_MODIFIED, Entry.EMPTY, null, RefreshErrorCode.NONE, null,
true, cfRayId, false);
}

public static FetchResponse failed(Object error, boolean fetchTimeUpdatable, String cfRayId, boolean shouldRetry) {
return new FetchResponse(Status.FAILED, Entry.EMPTY, error, fetchTimeUpdatable, cfRayId, shouldRetry);
public static FetchResponse failed(Object error, RefreshErrorCode errorCode, Throwable errorException,
boolean fetchTimeUpdatable, String cfRayId, boolean shouldRetry) {
return new FetchResponse(Status.FAILED, Entry.EMPTY, error, errorCode, errorException,
fetchTimeUpdatable, cfRayId, shouldRetry);
}
}
Loading
Loading