Skip to content

Commit c9619cb

Browse files
DVC-7132 fix java test harness (#73)
* minor refactor * fixed data type to match what test harness expects * properly throw an error when the SDK key is bad in the cloud sdk * added retries to cloud track() and fixed response messages to match test harness * fixing retries/error handling for the test harness * adding retry for loading local bucketing config * Fix error message to match test harness expectations * more fixes to initialization workflow * cleaning up polling and errors * removed unnecessary threading and error reporting * use new status code for bad json responses * Update src/main/java/com/devcycle/sdk/server/local/managers/EnvironmentConfigManager.java Co-authored-by: Jonathan Norris <jonathan@taplytics.com> * Update src/main/java/com/devcycle/sdk/server/local/managers/EnvironmentConfigManager.java Co-authored-by: Jonathan Norris <jonathan@taplytics.com> * Update src/main/java/com/devcycle/sdk/server/local/managers/EnvironmentConfigManager.java Co-authored-by: Jonathan Norris <jonathan@taplytics.com> * Update src/main/java/com/devcycle/sdk/server/cloud/api/DVCCloudClient.java Co-authored-by: Jonathan Norris <jonathan@taplytics.com> --------- Co-authored-by: Jonathan Norris <jonathan@taplytics.com>
1 parent 642d470 commit c9619cb

File tree

7 files changed

+159
-54
lines changed

7 files changed

+159
-54
lines changed

src/main/java/com/devcycle/sdk/server/cloud/api/DVCCloudClient.java

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import com.fasterxml.jackson.annotation.JsonInclude;
99
import com.fasterxml.jackson.core.JsonProcessingException;
1010
import com.fasterxml.jackson.databind.ObjectMapper;
11+
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
1112
import retrofit2.Call;
1213
import retrofit2.Response;
1314

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

3031
public DVCCloudClient(String sdkKey, DVCCloudOptions options) {
32+
if(sdkKey == null || sdkKey.equals("")) {
33+
throw new IllegalArgumentException("Missing environment key! Call initialize with a valid environment key");
34+
}
35+
36+
if(!isValidServerKey(sdkKey)) {
37+
throw new IllegalArgumentException("Invalid environment key provided. Please call initialize with a valid server environment key");
38+
}
39+
3140
this.dvcOptions = options;
3241
api = new DVCCloudApiClient(sdkKey, options).initialize();
3342
OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
@@ -88,7 +97,7 @@ public <T> Variable<T> variable(User user, String key, T defaultValue) throws DV
8897

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

138-
if (event == null || event.getType().equals("")) {
147+
if (event == null || event.getType() == null || event.getType().equals("")) {
139148
throw new IllegalArgumentException("Invalid Event");
140149
}
141150

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

147156
Call<DVCResponse> response = api.track(userAndEvents, dvcOptions.getEnableEdgeDB());
148-
getResponse(response);
157+
getResponseWithRetries(response, 5);
149158
}
150159

160+
161+
private <T> T getResponseWithRetries(Call<T> call, int maxRetries) throws DVCException {
162+
// attempt 0 is the initial request, attempt > 0 are all retries
163+
int attempt = 0;
164+
do {
165+
try {
166+
return getResponse(call);
167+
} catch (DVCException e) {
168+
attempt++;
169+
170+
// if out of retries or this is an unauthorized error, throw up exception
171+
if (!e.isRetryable() || attempt > maxRetries) {
172+
throw e;
173+
}
174+
175+
try {
176+
// exponential backoff
177+
long waitIntervalMS = (long) (10 * Math.pow(2, attempt));
178+
Thread.sleep(waitIntervalMS);
179+
} catch (InterruptedException ex) {
180+
// no-op
181+
}
182+
183+
// prep the call for a retry
184+
call = call.clone();
185+
}
186+
}while (attempt <= maxRetries);
187+
188+
// getting here should not happen, but is technically possible
189+
ErrorResponse errorResponse = ErrorResponse.builder().build();
190+
errorResponse.setMessage("Out of retry attempts");
191+
throw new DVCException(HttpResponseCode.SERVER_ERROR, errorResponse);
192+
}
193+
194+
151195
private <T> T getResponse(Call<T> call) throws DVCException {
152196
ErrorResponse errorResponse = ErrorResponse.builder().build();
153197
Response<T> response;
154198

155199
try {
156200
response = call.execute();
201+
} catch(MismatchedInputException mie) {
202+
// got a badly formatted JSON response from the server
203+
errorResponse.setMessage(mie.getMessage());
204+
throw new DVCException(HttpResponseCode.NO_CONTENT, errorResponse);
157205
} catch (IOException e) {
206+
// issues reaching the server or reading the response
158207
errorResponse.setMessage(e.getMessage());
159208
throw new DVCException(HttpResponseCode.byCode(500), errorResponse);
160209
}
161210

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

165-
if (response.errorBody() != null) {
166-
try {
167-
errorResponse = OBJECT_MAPPER.readValue(response.errorBody().string(), ErrorResponse.class);
168-
} catch (IOException e) {
169-
errorResponse.setMessage(e.getMessage());
170-
throw new DVCException(httpResponseCode, errorResponse);
171-
}
214+
if (response.errorBody() != null) {
215+
try {
216+
errorResponse = OBJECT_MAPPER.readValue(response.errorBody().string(), ErrorResponse.class);
217+
} catch (IOException e) {
218+
errorResponse.setMessage(e.getMessage());
172219
throw new DVCException(httpResponseCode, errorResponse);
173220
}
221+
throw new DVCException(httpResponseCode, errorResponse);
222+
}
174223

175224
if (response.body() == null) {
176225
throw new DVCException(httpResponseCode, errorResponse);
@@ -180,7 +229,7 @@ private <T> T getResponse(Call<T> call) throws DVCException {
180229
return response.body();
181230
} else {
182231
if (httpResponseCode == HttpResponseCode.UNAUTHORIZED) {
183-
errorResponse.setMessage("API Key is unauthorized");
232+
errorResponse.setMessage("Invalid SDK Key");
184233
} else if (!response.message().equals("")) {
185234
try {
186235
errorResponse = OBJECT_MAPPER.readValue(response.message(), ErrorResponse.class);
@@ -194,6 +243,10 @@ private <T> T getResponse(Call<T> call) throws DVCException {
194243
}
195244
}
196245

246+
private boolean isValidServerKey(String serverKey) {
247+
return serverKey.startsWith("server") || serverKey.startsWith("dvc_server");
248+
}
249+
197250
private void validateUser(User user) {
198251
if (user == null) {
199252
throw new IllegalArgumentException("User cannot be null");

src/main/java/com/devcycle/sdk/server/common/exception/DVCException.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,8 @@ public DVCException(HttpResponseCode httpResponseCode, ErrorResponse errorRespon
1515
this.httpResponseCode = httpResponseCode;
1616
this.errorResponse = errorResponse;
1717
}
18+
19+
public boolean isRetryable() {
20+
return httpResponseCode.code() >= HttpResponseCode.SERVER_ERROR.code();
21+
}
1822
}

src/main/java/com/devcycle/sdk/server/common/model/HttpResponseCode.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ public enum HttpResponseCode {
66

77
OK(200),
88
ACCEPTED(201),
9+
NO_CONTENT(204),
910
NOT_MODIFIED(304),
1011
BAD_REQUEST(400),
1112
UNAUTHORIZED(401),
13+
FORBIDDEN(403),
1214
NOT_FOUND(404),
1315
SERVER_ERROR(500);
1416

src/main/java/com/devcycle/sdk/server/common/model/User.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
package com.devcycle.sdk.server.common.model;
1414

1515
import com.devcycle.sdk.server.local.utils.LongTimestampDeserializer;
16+
import com.fasterxml.jackson.annotation.JsonFormat;
1617
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
1718
import com.fasterxml.jackson.annotation.JsonInclude;
1819
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -70,11 +71,13 @@ public class User {
7071
@Schema(description = "Date the user was created, Unix epoch timestamp format")
7172
@JsonProperty("createdDate")
7273
@JsonDeserialize(using = LongTimestampDeserializer.class)
74+
@JsonFormat(shape = JsonFormat.Shape.STRING)
7375
private Long createdDate;
7476

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

8083
@Schema(description = "Platform the SDK is running on")

src/main/java/com/devcycle/sdk/server/local/api/DVCLocalClient.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ public DVCLocalClient(String sdkKey) {
3434

3535
public DVCLocalClient(String sdkKey, DVCLocalOptions dvcOptions) {
3636
if(sdkKey == null || sdkKey.equals("")) {
37-
throw new IllegalArgumentException("Missing sdk key! Call initialize with a valid sdk key");
37+
throw new IllegalArgumentException("Missing SDK key! Call initialize with a valid SDK key");
3838
}
3939
if(!isValidServerKey(sdkKey)) {
40-
throw new IllegalArgumentException("Invalid sdk key provided. Please call initialize with a valid server sdk key");
40+
throw new IllegalArgumentException("Invalid SDK key provided. Please call initialize with a valid server SDK key");
4141
}
4242

4343
if(!isValidRuntime()){

src/main/java/com/devcycle/sdk/server/local/managers/EnvironmentConfigManager.java

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
package com.devcycle.sdk.server.local.managers;
22

3-
import java.io.IOException;
4-
import java.util.concurrent.Executors;
5-
import java.util.concurrent.ScheduledExecutorService;
6-
import java.util.concurrent.TimeUnit;
7-
83
import com.devcycle.sdk.server.common.api.IDVCApi;
94
import com.devcycle.sdk.server.common.exception.DVCException;
105
import com.devcycle.sdk.server.common.model.ErrorResponse;
@@ -13,12 +8,17 @@
138
import com.devcycle.sdk.server.local.api.DVCLocalApiClient;
149
import com.devcycle.sdk.server.local.bucketing.LocalBucketing;
1510
import com.devcycle.sdk.server.local.model.DVCLocalOptions;
11+
import com.fasterxml.jackson.core.JsonParseException;
1612
import com.fasterxml.jackson.core.JsonProcessingException;
1713
import com.fasterxml.jackson.databind.ObjectMapper;
18-
1914
import retrofit2.Call;
2015
import retrofit2.Response;
2116

17+
import java.io.IOException;
18+
import java.util.concurrent.Executors;
19+
import java.util.concurrent.ScheduledExecutorService;
20+
import java.util.concurrent.TimeUnit;
21+
2222
public final class EnvironmentConfigManager {
2323
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
2424
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@@ -33,6 +33,7 @@ public final class EnvironmentConfigManager {
3333

3434
private String sdkKey;
3535
private int pollingIntervalMS;
36+
private boolean pollingEnabled = true;
3637

3738
public EnvironmentConfigManager(String sdkKey, LocalBucketing localBucketing, DVCLocalOptions options) {
3839
this.sdkKey = sdkKey;
@@ -51,9 +52,11 @@ private void setupScheduler() {
5152
Runnable getConfigRunnable = new Runnable() {
5253
public void run() {
5354
try {
54-
getConfig();
55-
} catch (DVCException | JsonProcessingException e) {
56-
e.printStackTrace();
55+
if (pollingEnabled) {
56+
getConfig();
57+
}
58+
} catch (DVCException e) {
59+
System.out.println("Failed to load config: " + e.getMessage());
5760
}
5861
}
5962
};
@@ -65,19 +68,57 @@ public boolean isConfigInitialized() {
6568
return config != null;
6669
}
6770

68-
private ProjectConfig getConfig() throws DVCException, JsonProcessingException {
71+
private ProjectConfig getConfig() throws DVCException {
6972
Call<ProjectConfig> config = this.configApiClient.getConfig(this.sdkKey, this.configETag);
70-
71-
this.config = getConfigResponse(config);
73+
this.config = getResponseWithRetries(config, 1);
7274
return this.config;
7375
}
7476

75-
private ProjectConfig getConfigResponse(Call<ProjectConfig> call) throws DVCException, JsonProcessingException {
77+
private ProjectConfig getResponseWithRetries(Call<ProjectConfig> call, int maxRetries) throws DVCException {
78+
// attempt 0 is the initial request, attempt > 0 are all retries
79+
int attempt = 0;
80+
do {
81+
try {
82+
return getConfigResponse(call);
83+
} catch (DVCException e) {
84+
85+
attempt++;
86+
87+
// if out of retries or this is an unauthorized error, throw up exception
88+
if ( !e.isRetryable() || attempt > maxRetries) {
89+
throw e;
90+
}
91+
92+
try {
93+
// exponential backoff
94+
long waitIntervalMS = (long) (10 * Math.pow(2, attempt));
95+
Thread.sleep(waitIntervalMS);
96+
} catch (InterruptedException ex) {
97+
// no-op
98+
}
99+
100+
// prep the call for a retry
101+
call = call.clone();
102+
}
103+
} while (attempt <= maxRetries && pollingEnabled);
104+
105+
// getting here should not happen, but is technically possible
106+
ErrorResponse errorResponse = ErrorResponse.builder().build();
107+
errorResponse.setMessage("Out of retry attempts");
108+
throw new DVCException(HttpResponseCode.SERVER_ERROR, errorResponse);
109+
}
110+
111+
private ProjectConfig getConfigResponse(Call<ProjectConfig> call) throws DVCException {
76112
ErrorResponse errorResponse = ErrorResponse.builder().build();
77113
Response<ProjectConfig> response;
78114

79115
try {
80116
response = call.execute();
117+
} catch(JsonParseException badJsonExc) {
118+
// Got a valid status code but the response body was not valid json,
119+
// need to ignore this attempt and let the polling retry
120+
errorResponse.setMessage(badJsonExc.getMessage());
121+
throw new DVCException(HttpResponseCode.NO_CONTENT, errorResponse);
81122
} catch (IOException e) {
82123
errorResponse.setMessage(e.getMessage());
83124
throw new DVCException(HttpResponseCode.byCode(500), errorResponse);
@@ -97,7 +138,8 @@ private ProjectConfig getConfigResponse(Call<ProjectConfig> call) throws DVCExce
97138
System.out.printf("Unable to parse config with etag: %s. Using cache, etag %s%n", currentETag, this.configETag);
98139
return this.config;
99140
} else {
100-
throw e;
141+
errorResponse.setMessage(e.getMessage());
142+
throw new DVCException(HttpResponseCode.SERVER_ERROR, errorResponse);
101143
}
102144
}
103145
this.configETag = currentETag;
@@ -109,15 +151,20 @@ private ProjectConfig getConfigResponse(Call<ProjectConfig> call) throws DVCExce
109151
if (response.errorBody() != null) {
110152
try {
111153
errorResponse = OBJECT_MAPPER.readValue(response.errorBody().string(), ErrorResponse.class);
154+
} catch (JsonProcessingException e) {
155+
errorResponse.setMessage("Unable to parse error response: " + e.getMessage());
156+
throw new DVCException(httpResponseCode, errorResponse);
112157
} catch (IOException e) {
113158
errorResponse.setMessage(e.getMessage());
114159
throw new DVCException(httpResponseCode, errorResponse);
115160
}
116161
throw new DVCException(httpResponseCode, errorResponse);
117162
}
118163

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

134-
public void cleanup() {
181+
private void stopPolling() {
182+
pollingEnabled = false;
135183
scheduler.shutdown();
136184
}
185+
186+
public void cleanup() {
187+
stopPolling();
188+
}
137189
}

0 commit comments

Comments
 (0)