Skip to content

Commit

Permalink
kontent-ai#53 Implement optional retry logic for CDN failures
Browse files Browse the repository at this point in the history
  • Loading branch information
aweigold committed Feb 5, 2018
1 parent e605d66 commit 1fe6c34
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 18 deletions.
63 changes: 45 additions & 18 deletions src/main/java/com/kenticocloud/delivery/DeliveryClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public class DeliveryClient {

private static final Logger logger = LoggerFactory.getLogger(DeliveryClient.class);

static String SDK_ID;
static String sdkId;

static {
try {
Expand All @@ -67,15 +67,15 @@ public class DeliveryClient {
repositoryHost = repositoryHost == null ? "localBuild" : repositoryHost;
version = version == null ? "0.0.0" : version;
packageId = packageId == null ? "com.kenticocloud:delivery-sdk-java" : packageId;
SDK_ID = String.format(
sdkId = String.format(
"%s;%s;%s",
repositoryHost,
packageId,
version);
logger.info("SDK ID: {}", SDK_ID);
logger.info("SDK ID: {}", sdkId);
} catch (IOException e) {
logger.info("Jar manifest read error, setting developer build SDK ID");
SDK_ID = "localBuild;com.kenticocloud:delivery-sdk-java;0.0.0";
sdkId = "localBuild;com.kenticocloud:delivery-sdk-java;0.0.0";
}
}

Expand Down Expand Up @@ -140,6 +140,9 @@ public DeliveryClient(DeliveryOptions deliveryOptions, TemplateEngineConfig temp
if (deliveryOptions.isUsePreviewApi() && deliveryOptions.getProductionApiKey() != null){
throw new IllegalArgumentException("Cannot provide both a preview API key and a production API key.");
}
if (deliveryOptions.getRetryAttempts() < 0) {
throw new IllegalArgumentException("Cannot retry connections less than 0 times.");
}
this.deliveryOptions = deliveryOptions;
connManager.setMaxTotal(20);
connManager.setDefaultMaxPerRoute(20);
Expand Down Expand Up @@ -393,7 +396,7 @@ protected RequestBuilder addHeaders(RequestBuilder requestBuilder) {
);
}
requestBuilder.setHeader(HttpHeaders.ACCEPT, "application/json");
requestBuilder.setHeader("X-KC-SDKID", SDK_ID);
requestBuilder.setHeader("X-KC-SDKID", sdkId);
return requestBuilder;
}

Expand All @@ -406,19 +409,43 @@ private String getBaseUrl() {
}

private <T> T executeRequest(HttpUriRequest request, Class<T> tClass) throws IOException {
String requestUri = request.getURI().toString();
logger.info("HTTP {} - {} - {}", request.getMethod(), request.getAllHeaders(), requestUri);
JsonNode jsonNode = cacheManager.resolveRequest(requestUri, () -> {
HttpResponse response = httpClient.execute(request);
handleErrorIfNecessary(response);
InputStream inputStream = response.getEntity().getContent();
JsonNode node = objectMapper.readValue(inputStream, JsonNode.class);
logger.info("{} - {}", response.getStatusLine(), requestUri);
logger.debug("{} - {}:\n{}", request.getMethod(), requestUri, node);
inputStream.close();
return node;
});
return objectMapper.treeToValue(jsonNode, tClass);
return executeRequest(request, tClass, 0);
}

private <T> T executeRequest(HttpUriRequest request, Class<T> tClass, int attemptNumber) throws IOException {
try {
String requestUri = request.getURI().toString();
logger.info("HTTP {} - {} - {}", request.getMethod(), request.getAllHeaders(), requestUri);
JsonNode jsonNode = cacheManager.resolveRequest(requestUri, () -> {
HttpResponse response = httpClient.execute(request);
handleErrorIfNecessary(response);
InputStream inputStream = response.getEntity().getContent();
JsonNode node = objectMapper.readValue(inputStream, JsonNode.class);
logger.info("{} - {}", response.getStatusLine(), requestUri);
logger.debug("{} - {}:\n{}", request.getMethod(), requestUri, node);
inputStream.close();
return node;
});
return objectMapper.treeToValue(jsonNode, tClass);
} catch (KenticoErrorException kenticoError) {
throw kenticoError;
} catch (Exception ex) {
logger.error("Failed request: {}", ex.getMessage());
if (attemptNumber < deliveryOptions.getRetryAttempts()) {
int nextAttemptNumber = attemptNumber + 1;
//Perform a binary exponential backoff
int wait = (int) (100 * Math.pow(2, nextAttemptNumber));
logger.info("Reattempting request after {}ms", wait);
try {
Thread.sleep(wait);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return executeRequest(request, tClass, nextAttemptNumber);
} else {
throw ex;
}
}
}

private void handleErrorIfNecessary(HttpResponse response) throws IOException {
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/com/kenticocloud/delivery/DeliveryOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public class DeliveryOptions {
String previewApiKey;
boolean usePreviewApi = false;
boolean waitForLoadingNewContent = false;
int retryAttempts = 0;

/**
* Constructs an empty settings instance of {@link DeliveryOptions}.
Expand Down Expand Up @@ -183,4 +184,20 @@ public boolean isWaitForLoadingNewContent() {
public void setWaitForLoadingNewContent(boolean waitForLoadingNewContent) {
this.waitForLoadingNewContent = waitForLoadingNewContent;
}

/**
* Get the number of times the client will retry to connect to the Kentico Cloud api on failures per request.
* @return The number of retry attempts on failed responses from Kentico Cloud
*/
public int getRetryAttempts() {
return retryAttempts;
}

/**
* Sets the number of times the client will try to connect to the Kentico Cloud api on failures per request.
* @param retryAttempts The number of retry attempts.
*/
public void setRetryAttempts(int retryAttempts) {
this.retryAttempts = retryAttempts;
}
}
111 changes: 111 additions & 0 deletions src/test/java/com/kenticocloud/delivery/DeliveryClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

package com.kenticocloud.delivery;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
Expand Down Expand Up @@ -78,6 +79,103 @@ public void testSdkIdHeader() throws Exception {
Assert.assertNotNull(item);
}

@Test
public void testRetry() throws Exception {
String projectId = "02a70003-e864-464e-b62c-e0ede97deb8c";
final boolean[] sentError = {false};

this.serverBootstrap.registerHandler(
String.format("/%s/%s", projectId, "items/on_roasts"),
(request, response, context) -> {
if (sentError[0]) {
response.setEntity(
new InputStreamEntity(
this.getClass().getResourceAsStream("SampleContentItem.json")
)
);
} else {
response.setEntity(new StringEntity("Response Error!"));
sentError[0] = true;
}
});
HttpHost httpHost = this.start();
String testServerUri = httpHost.toURI() + "/%s";
DeliveryOptions deliveryOptions = new DeliveryOptions();
deliveryOptions.setProjectId(projectId);
deliveryOptions.setProductionEndpoint(testServerUri);
deliveryOptions.setRetryAttempts(1);

DeliveryClient client = new DeliveryClient(deliveryOptions);

ContentItemResponse item = client.getItem("on_roasts");
Assert.assertNotNull(item);
Assert.assertTrue(sentError[0]);
}

@Test
public void testRetryStops() throws Exception {
String projectId = "02a70003-e864-464e-b62c-e0ede97deb8c";
final int[] sentErrorCount = {0};

this.serverBootstrap.registerHandler(
String.format("/%s/%s", projectId, "items/on_roasts"),
(request, response, context) -> {
response.setEntity(new StringEntity("Response Error!"));
sentErrorCount[0] = sentErrorCount[0] + 1;
});
HttpHost httpHost = this.start();
String testServerUri = httpHost.toURI() + "/%s";
DeliveryOptions deliveryOptions = new DeliveryOptions();
deliveryOptions.setProjectId(projectId);
deliveryOptions.setProductionEndpoint(testServerUri);
deliveryOptions.setRetryAttempts(1);

DeliveryClient client = new DeliveryClient(deliveryOptions);

try {
client.getItem("on_roasts");
Assert.fail("Expected a failure exception");
} catch (Exception e) {
Assert.assertTrue(e instanceof JsonParseException);
}
Assert.assertEquals(2, sentErrorCount[0]);
}

@Test
public void testRetryStopsOnKenticoException() throws Exception {
String projectId = "02a70003-e864-464e-b62c-e0ede97deb8c";
final int[] sentErrorCount = {0};

this.serverBootstrap.registerHandler(
String.format("/%s/%s", projectId, "items/on_roatst"),
(request, response, context) -> {
response.setStatusCode(404);
response.setEntity(
new InputStreamEntity(
this.getClass().getResourceAsStream("SampleKenticoError.json")
)
);
sentErrorCount[0] = sentErrorCount[0] + 1;
});
HttpHost httpHost = this.start();
String testServerUri = httpHost.toURI() + "/%s";
DeliveryOptions deliveryOptions = new DeliveryOptions();
deliveryOptions.setProjectId(projectId);
deliveryOptions.setProductionEndpoint(testServerUri);
deliveryOptions.setRetryAttempts(1);

DeliveryClient client = new DeliveryClient(deliveryOptions);

try {
client.getItem("on_roatst");
Assert.fail("Expected KenticoErrorException");
} catch (KenticoErrorException e) {
Assert.assertEquals("The requested content item 'on_roatst' was not found.", e.getMessage());
Assert.assertEquals("The requested content item 'on_roatst' was not found.", e.getKenticoError().getMessage());
}
Assert.assertEquals(1, sentErrorCount[0]);
}

@Test
public void testGetItems() throws Exception {
String projectId = "02a70003-e864-464e-b62c-e0ede97deb8c";
Expand Down Expand Up @@ -1092,6 +1190,19 @@ public void testExceptionWhenBothProductionApiKeyAndPreviewApiKeyProvided() {
}
}

@Test
public void testExceptionWhenRetryCountIsLessThanZero() {
DeliveryOptions deliveryOptions = new DeliveryOptions();
deliveryOptions.setProjectId("02a70003-e864-464e-b62c-e0ede97deb8c");
deliveryOptions.setRetryAttempts(-1);
try {
DeliveryClient client = new DeliveryClient(deliveryOptions);
Assert.fail("Expected IllegalArgumentException due to negative number provided for retry count");
} catch (IllegalArgumentException e) {
Assert.assertEquals("Cannot retry connections less than 0 times.", e.getMessage());
}
}

private Map<String, String> convertNameValuePairsToMap(List<NameValuePair> nameValuePairs) {
HashMap<String, String> map = new HashMap<>();
for (NameValuePair nameValuePair : nameValuePairs) {
Expand Down

0 comments on commit 1fe6c34

Please sign in to comment.