Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import retrofit2.Call;
import retrofit2.Response;

Expand All @@ -28,6 +29,14 @@ public DVCCloudClient(String sdkKey) {
}

public DVCCloudClient(String sdkKey, DVCCloudOptions options) {
if(sdkKey == null || sdkKey.equals("")) {
throw new IllegalArgumentException("Missing environment key! Call initialize with a valid environment key");
}

if(!isValidServerKey(sdkKey)) {
throw new IllegalArgumentException("Invalid environment key provided. Please call initialize with a valid server environment key");
}

this.dvcOptions = options;
api = new DVCCloudApiClient(sdkKey, options).initialize();
OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
Expand Down Expand Up @@ -88,7 +97,7 @@ public <T> Variable<T> variable(User user, String key, T defaultValue) throws DV

try {
Call<Variable> response = api.getVariableByKey(user, key, dvcOptions.getEnableEdgeDB());
variable = getResponse(response);
variable = getResponseWithRetries(response, 5);
if (variable.getType() != variableType) {
throw new IllegalArgumentException("Variable type mismatch, returning default value");
}
Expand Down Expand Up @@ -135,7 +144,7 @@ public Map<String, BaseVariable> allVariables(User user) throws DVCException {
public void track(User user, Event event) throws DVCException {
validateUser(user);

if (event == null || event.getType().equals("")) {
if (event == null || event.getType() == null || event.getType().equals("")) {
throw new IllegalArgumentException("Invalid Event");
}

Expand All @@ -145,32 +154,72 @@ public void track(User user, Event event) throws DVCException {
.build();

Call<DVCResponse> response = api.track(userAndEvents, dvcOptions.getEnableEdgeDB());
getResponse(response);
getResponseWithRetries(response, 5);
}


private <T> T getResponseWithRetries(Call<T> call, int maxRetries) throws DVCException {
// attempt 0 is the initial request, attempt > 0 are all retries
int attempt = 0;
do {
try {
return getResponse(call);
} catch (DVCException e) {
attempt++;

// if out of retries or this is an unauthorized error, throw up exception
if (!e.isRetryable() || attempt > maxRetries) {
throw e;
}

try {
// exponential backoff
long waitIntervalMS = (long) (10 * Math.pow(2, attempt));
Thread.sleep(waitIntervalMS);
} catch (InterruptedException ex) {
// no-op
}

// prep the call for a retry
call = call.clone();
}
}while (attempt <= maxRetries);

// getting here should not happen, but is technically possible
ErrorResponse errorResponse = ErrorResponse.builder().build();
errorResponse.setMessage("Out of retry attempts");
throw new DVCException(HttpResponseCode.SERVER_ERROR, errorResponse);
}


private <T> T getResponse(Call<T> call) throws DVCException {
ErrorResponse errorResponse = ErrorResponse.builder().build();
Response<T> response;

try {
response = call.execute();
} catch(MismatchedInputException mie) {
// got a badly formatted JSON response from the server
errorResponse.setMessage(mie.getMessage());
throw new DVCException(HttpResponseCode.NOT_FOUND, errorResponse);
} catch (IOException e) {
// issues reaching the server or reading the response
errorResponse.setMessage(e.getMessage());
throw new DVCException(HttpResponseCode.byCode(500), errorResponse);
}

HttpResponseCode httpResponseCode = HttpResponseCode.byCode(response.code());
errorResponse.setMessage("Unknown error");

if (response.errorBody() != null) {
try {
errorResponse = OBJECT_MAPPER.readValue(response.errorBody().string(), ErrorResponse.class);
} catch (IOException e) {
errorResponse.setMessage(e.getMessage());
throw new DVCException(httpResponseCode, errorResponse);
}
if (response.errorBody() != null) {
try {
errorResponse = OBJECT_MAPPER.readValue(response.errorBody().string(), ErrorResponse.class);
} catch (IOException e) {
errorResponse.setMessage(e.getMessage());
throw new DVCException(httpResponseCode, errorResponse);
}
throw new DVCException(httpResponseCode, errorResponse);
}

if (response.body() == null) {
throw new DVCException(httpResponseCode, errorResponse);
Expand All @@ -180,7 +229,7 @@ private <T> T getResponse(Call<T> call) throws DVCException {
return response.body();
} else {
if (httpResponseCode == HttpResponseCode.UNAUTHORIZED) {
errorResponse.setMessage("API Key is unauthorized");
errorResponse.setMessage("Invalid sdk key");
} else if (!response.message().equals("")) {
try {
errorResponse = OBJECT_MAPPER.readValue(response.message(), ErrorResponse.class);
Expand All @@ -194,6 +243,10 @@ private <T> T getResponse(Call<T> call) throws DVCException {
}
}

private boolean isValidServerKey(String serverKey) {
return serverKey.startsWith("server") || serverKey.startsWith("dvc_server");
}

private void validateUser(User user) {
if (user == null) {
throw new IllegalArgumentException("User cannot be null");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@ public DVCException(HttpResponseCode httpResponseCode, ErrorResponse errorRespon
this.httpResponseCode = httpResponseCode;
this.errorResponse = errorResponse;
}

public boolean isRetryable() {
return httpResponseCode.code() >= HttpResponseCode.SERVER_ERROR.code();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ public enum HttpResponseCode {

OK(200),
ACCEPTED(201),
NO_CONTENT(204),
NOT_MODIFIED(304),
BAD_REQUEST(400),
UNAUTHORIZED(401),
FORBIDDEN(403),
NOT_FOUND(404),
SERVER_ERROR(500);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
package com.devcycle.sdk.server.common.model;

import com.devcycle.sdk.server.local.utils.LongTimestampDeserializer;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
Expand Down Expand Up @@ -70,11 +71,13 @@ public class User {
@Schema(description = "Date the user was created, Unix epoch timestamp format")
@JsonProperty("createdDate")
@JsonDeserialize(using = LongTimestampDeserializer.class)
@JsonFormat(shape = JsonFormat.Shape.STRING)
private Long createdDate;

@Schema(description = "Date the user was last seen, Unix epoch timestamp format")
@JsonProperty("lastSeenDate")
@JsonDeserialize(using = LongTimestampDeserializer.class)
@JsonFormat(shape = JsonFormat.Shape.STRING)
private Long lastSeenDate;

@Schema(description = "Platform the SDK is running on")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ public DVCLocalClient(String sdkKey) {

public DVCLocalClient(String sdkKey, DVCLocalOptions dvcOptions) {
if(sdkKey == null || sdkKey.equals("")) {
throw new IllegalArgumentException("Missing sdk key! Call initialize with a valid sdk key");
throw new IllegalArgumentException("Missing SDK key! Call initialize with a valid SDK key");
}
if(!isValidServerKey(sdkKey)) {
throw new IllegalArgumentException("Invalid sdk key provided. Please call initialize with a valid server sdk key");
throw new IllegalArgumentException("Invalid SDK key provided. Please call initialize with a valid server SDK key");
}

if(!isValidRuntime()){
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
package com.devcycle.sdk.server.local.managers;

import java.io.IOException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import com.devcycle.sdk.server.common.api.IDVCApi;
import com.devcycle.sdk.server.common.exception.DVCException;
import com.devcycle.sdk.server.common.model.ErrorResponse;
Expand All @@ -13,12 +8,17 @@
import com.devcycle.sdk.server.local.api.DVCLocalApiClient;
import com.devcycle.sdk.server.local.bucketing.LocalBucketing;
import com.devcycle.sdk.server.local.model.DVCLocalOptions;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import retrofit2.Call;
import retrofit2.Response;

import java.io.IOException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public final class EnvironmentConfigManager {
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
Expand All @@ -33,6 +33,7 @@ public final class EnvironmentConfigManager {

private String sdkKey;
private int pollingIntervalMS;
private boolean pollingEnabled = true;

public EnvironmentConfigManager(String sdkKey, LocalBucketing localBucketing, DVCLocalOptions options) {
this.sdkKey = sdkKey;
Expand All @@ -51,9 +52,11 @@ private void setupScheduler() {
Runnable getConfigRunnable = new Runnable() {
public void run() {
try {
getConfig();
} catch (DVCException | JsonProcessingException e) {
e.printStackTrace();
if(pollingEnabled){
getConfig();
}
} catch (DVCException e) {
System.out.println("Failed to load config: " + e.getMessage());
}
}
};
Expand All @@ -65,19 +68,57 @@ public boolean isConfigInitialized() {
return config != null;
}

private ProjectConfig getConfig() throws DVCException, JsonProcessingException {
private ProjectConfig getConfig() throws DVCException {
Call<ProjectConfig> config = this.configApiClient.getConfig(this.sdkKey, this.configETag);

this.config = getConfigResponse(config);
this.config = getResponseWithRetries(config, 1);
return this.config;
}

private ProjectConfig getConfigResponse(Call<ProjectConfig> call) throws DVCException, JsonProcessingException {
private ProjectConfig getResponseWithRetries(Call<ProjectConfig> call, int maxRetries) throws DVCException {
// attempt 0 is the initial request, attempt > 0 are all retries
int attempt = 0;
do {
try {
return getConfigResponse(call);
} catch (DVCException e) {

attempt++;

// if out of retries or this is an unauthorized error, throw up exception
if ( !e.isRetryable() || attempt > maxRetries) {
throw e;
}

try {
// exponential backoff
long waitIntervalMS = (long) (10 * Math.pow(2, attempt));
Thread.sleep(waitIntervalMS);
} catch (InterruptedException ex) {
// no-op
}

// prep the call for a retry
call = call.clone();
}
}while (attempt <= maxRetries && pollingEnabled);

// getting here should not happen, but is technically possible
ErrorResponse errorResponse = ErrorResponse.builder().build();
errorResponse.setMessage("Out of retry attempts");
throw new DVCException(HttpResponseCode.SERVER_ERROR, errorResponse);
}

private ProjectConfig getConfigResponse(Call<ProjectConfig> call) throws DVCException {
ErrorResponse errorResponse = ErrorResponse.builder().build();
Response<ProjectConfig> response;

try {
response = call.execute();
} catch(JsonParseException badJsonExc) {
// Got a valid status code but the response body was not valid json,
// need to ignore this attempt and let the polling retry
errorResponse.setMessage(badJsonExc.getMessage());
throw new DVCException(HttpResponseCode.NO_CONTENT, errorResponse);
} catch (IOException e) {
errorResponse.setMessage(e.getMessage());
throw new DVCException(HttpResponseCode.byCode(500), errorResponse);
Expand All @@ -97,7 +138,8 @@ private ProjectConfig getConfigResponse(Call<ProjectConfig> call) throws DVCExce
System.out.printf("Unable to parse config with etag: %s. Using cache, etag %s%n", currentETag, this.configETag);
return this.config;
} else {
throw e;
errorResponse.setMessage(e.getMessage());
throw new DVCException(HttpResponseCode.SERVER_ERROR, errorResponse);
}
}
this.configETag = currentETag;
Expand All @@ -109,15 +151,20 @@ private ProjectConfig getConfigResponse(Call<ProjectConfig> call) throws DVCExce
if (response.errorBody() != null) {
try {
errorResponse = OBJECT_MAPPER.readValue(response.errorBody().string(), ErrorResponse.class);
} catch (JsonProcessingException e) {
errorResponse.setMessage("Unable to parse error response: " + e.getMessage());
throw new DVCException(httpResponseCode, errorResponse);
} catch (IOException e) {
errorResponse.setMessage(e.getMessage());
throw new DVCException(httpResponseCode, errorResponse);
}
throw new DVCException(httpResponseCode, errorResponse);
}

if (httpResponseCode == HttpResponseCode.UNAUTHORIZED) {
if (httpResponseCode == HttpResponseCode.UNAUTHORIZED || httpResponseCode == HttpResponseCode.FORBIDDEN) {
// SDK Key is no longer authorized or now blocked, stop polling for configs
errorResponse.setMessage("API Key is unauthorized");
stopPolling();
} else if (!response.message().equals("")) {
try {
errorResponse = OBJECT_MAPPER.readValue(response.message(), ErrorResponse.class);
Expand All @@ -131,7 +178,12 @@ private ProjectConfig getConfigResponse(Call<ProjectConfig> call) throws DVCExce
}
}

public void cleanup() {
private void stopPolling() {
pollingEnabled = false;
scheduler.shutdown();
}

public void cleanup() {
stopPolling();
}
}
Loading