Skip to content

Commit

Permalink
default retry strategy
Browse files Browse the repository at this point in the history
  • Loading branch information
samvaity committed Aug 13, 2021
1 parent 67aa124 commit ee0f532
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,58 @@

package com.azure.core.http.policy;

import com.azure.core.http.HttpHeaders;
import com.azure.core.http.HttpMethod;
import com.azure.core.http.HttpPipelineCallContext;
import com.azure.core.http.HttpResponse;
import com.azure.core.util.logging.ClientLogger;

import java.net.HttpURLConnection;
import java.util.Objects;
import java.util.HashSet;
import java.util.Set;

/**
* A default implementation of {@link RedirectStrategy} that uses the status code to determine if to redirect
* between each retry attempt.
*/
public class DefaultRedirectStrategy implements RedirectStrategy {
static final int PERMANENT_REDIRECT_STATUS_CODE = 308;

// Based on Stamp specific redirects design doc
static final int MAX_REDIRECT_RETRIES = 10;
private static final String LOCATION_HEADER_NAME = "Location";

private final int maxRetries;
private final int statusCode;
private final String locationHeader;
private final Set<HttpMethod> redirectMethods;

/**
* Creates an instance of {@link DefaultRedirectStrategy}.
*
* @param maxRetries The max number of retry attempts that can be made.
* @param statusCode HTTP response status code
* @param maxRetries The max number of redirect attempts that can be made.
*/
public DefaultRedirectStrategy(int statusCode, int maxRetries) {
public DefaultRedirectStrategy(int maxRetries, String locationHeader, Set<HttpMethod> redirectableMethods) {
this.maxRetries = maxRetries;
this.locationHeader = locationHeader;
this.redirectMethods = redirectableMethods;
}

/**
* Creates an instance of {@link DefaultRedirectStrategy}.
*
* @param maxRetries The max number of redirect attempts that can be made.
*/
public DefaultRedirectStrategy(int maxRetries) {
if (maxRetries < 0) {
ClientLogger logger = new ClientLogger(DefaultRedirectStrategy.class);
throw logger.logExceptionAsError(new IllegalArgumentException("Max retries cannot be less than 0."));
}
this.maxRetries = maxRetries;
this.statusCode = Objects.requireNonNull(statusCode, "'statusCode' cannot be null.");
this.locationHeader = LOCATION_HEADER_NAME;
this.redirectMethods = new HashSet<HttpMethod>() {
{
add(HttpMethod.GET);
add(HttpMethod.HEAD);
}
};
}

@Override
Expand All @@ -41,14 +63,25 @@ public int getMaxRetries() {
}

@Override
public boolean shouldAttemptRedirect() {
if (maxRetries > MAX_REDIRECT_RETRIES) {
logger.verbose("Max redirect retries limit reached: {}.", MAX_REDIRECT_RETRIES);
public boolean shouldAttemptRedirect(HttpHeaders responseHeaders, int tryCount,
Set<String> attemptedRedirectLocations) {
if (tryCount > MAX_REDIRECT_RETRIES) {
logger.error(String.format("Request has been redirected more than %d times.", MAX_REDIRECT_RETRIES));
return false;
}
if (attemptedRedirectLocations.contains(responseHeaders.get(LOCATION_HEADER_NAME))) {
logger.error(String.format("Request was redirected more than once to: %s",
responseHeaders.get(LOCATION_HEADER_NAME)));
return false;
}
return statusCode == HttpURLConnection.HTTP_MOVED_TEMP
|| statusCode == HttpURLConnection.HTTP_MOVED_PERM
|| statusCode == PERMANENT_REDIRECT_STATUS_CODE;
return true;
}

public String getLocationHeader() {
return locationHeader;
}

public Set<HttpMethod> getRedirectableMethods() {
return redirectMethods;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,42 @@

package com.azure.core.http.policy;

import com.azure.core.http.HttpHeaders;
import com.azure.core.http.HttpMethod;
import com.azure.core.http.HttpPipelineCallContext;
import com.azure.core.http.HttpPipelineNextPolicy;
import com.azure.core.http.HttpRequest;
import com.azure.core.http.HttpResponse;
import com.azure.core.util.logging.ClientLogger;
import com.azure.core.util.CoreUtils;
import reactor.core.publisher.Mono;

import java.net.HttpURLConnection;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

/**
* A HttpPipeline policy that retries when a HTTP Redirect is received as response.
*/
public final class RedirectPolicy implements HttpPipelinePolicy {

// http methods to consider
// Location header string lookup
//

private static final int PERMANENT_REDIRECT_STATUS_CODE = 308;
Set<String> attemptedRedirectLocations = new HashSet<>();

// Based on Stamp specific redirects design doc
private static final int MAX_REDIRECT_RETRIES = 10;
private final ClientLogger logger = new ClientLogger(RedirectPolicy.class);
private String redirectedEndpointUrl;


private final RedirectStrategy retryStrategy;

/**
* Creates {@link RedirectPolicy} with default {@link DefaultRedirectStrategy} as {@link RedirectStrategy} and
* use the provided {@code statusCode} to determine if this request should be retried
* and MAX_REDIRECT_RETRIES for the retry count.
*
* @param statusCode the {@code HttpResponse} status code.
* @throws NullPointerException When {@code statusCode} is null.
*/
public RedirectPolicy(int statusCode) {
this(new DefaultRedirectStrategy(statusCode, MAX_REDIRECT_RETRIES));
public RedirectPolicy() {
this(new DefaultRedirectStrategy(MAX_REDIRECT_RETRIES));
}

/**
Expand All @@ -58,8 +58,8 @@ public Mono<HttpResponse> process(HttpPipelineCallContext context, HttpPipelineN
}

/**
* Function to process through the HTTP Response received in the pipeline
* and retry sending the request with new redirect url.
* Function to process through the HTTP Response received in the pipeline
* and retry sending the request with new redirect url.
*/
private Mono<HttpResponse> attemptRetry(final HttpPipelineCallContext context,
final HttpPipelineNextPolicy next,
Expand All @@ -72,14 +72,35 @@ private Mono<HttpResponse> attemptRetry(final HttpPipelineCallContext context,
}
return next.clone().process()
.flatMap(httpResponse -> {
if (retryStrategy.shouldAttemptRedirect()) {
String responseLocation = httpResponse.getHeaderValue("Location");
if (isRedirectableStatusCode(httpResponse.getStatusCode()) &&
isRedirectableMethod(httpResponse.getRequest().getHttpMethod()) &&
retryStrategy.shouldAttemptRedirect(httpResponse.getHeaders(), retryCount,
attemptedRedirectLocations)) {
String responseLocation =
tryGetRedirectHeader(httpResponse.getHeaders(), retryStrategy.getLocationHeader());
if (responseLocation != null) {
attemptedRedirectLocations.add(responseLocation);
this.redirectedEndpointUrl = responseLocation;
return attemptRetry(context, next, originalHttpRequest, retryCount + 1);
}
}
return Mono.just(httpResponse);
});
}

private boolean isRedirectableMethod(HttpMethod httpMethod) {
return retryStrategy.getRedirectableMethods().contains(httpMethod);
}

private boolean isRedirectableStatusCode(int statusCode) {
return statusCode == HttpURLConnection.HTTP_MOVED_TEMP
|| statusCode == HttpURLConnection.HTTP_MOVED_PERM
|| statusCode == PERMANENT_REDIRECT_STATUS_CODE;
}

private static String tryGetRedirectHeader(HttpHeaders headers, String headerName) {
String headerValue = headers.getValue(headerName);

return CoreUtils.isNullOrEmpty(headerValue) ? null : headerValue;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@

package com.azure.core.http.policy;

import com.azure.core.http.HttpHeaders;
import com.azure.core.http.HttpMethod;
import com.azure.core.http.HttpPipelineCallContext;
import com.azure.core.http.HttpPipelineNextPolicy;
import com.azure.core.http.HttpRequest;
import com.azure.core.http.HttpResponse;
import com.azure.core.util.logging.ClientLogger;

import java.net.HttpURLConnection;
import java.util.Set;

/**
* The interface for determining the retry strategy used in {@link RetryPolicy}.
Expand All @@ -24,30 +28,24 @@ public interface RedirectStrategy {
* @return The max number of retry attempts.
*/
int getMaxRetries();
String getLocationHeader();
Set<HttpMethod> getRedirectableMethods();

/**
*
* @param
*
*/
/**
* Determines if the url should be redirected between each retry.
*
* @param responseHeaders the ongoing request headers
* @param tryCount redirect retries so far
* @param attemptedRedirectLocations attempted redirect retries locations so far
*
* @return {@code true} if the request should be redirected, {@code false}
* otherwise
*/
boolean shouldAttemptRedirect();
boolean shouldAttemptRedirect(HttpHeaders responseHeaders, int tryCount, Set<String> attemptedRedirectLocations);

/**
* Determines if it's a valid retry scenario based on statusCode and tryCount.
*
* @param statusCode HTTP response status code
* @param tryCount Redirect retries so far
* @return True if statusCode corresponds to HTTP redirect response codes and redirect
* retries is less than {@code MAX_REDIRECT_RETRIES}.
*/
// default boolean shouldAttemptDirect(int statusCode, int tryCount) {
// if (tryCount >= MAX_REDIRECT_RETRIES) {
// logger.verbose("Max redirect retries limit reached: {}.", MAX_REDIRECT_RETRIES);
// return false;
// }
// return statusCode == HttpURLConnection.HTTP_MOVED_TEMP
// || statusCode == HttpURLConnection.HTTP_MOVED_PERM
// || statusCode == PERMANENT_REDIRECT_STATUS_CODE;
// }
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public void retryWith308Test() throws Exception {

HttpPipeline pipeline = new HttpPipelineBuilder()
.httpClient(httpClient)
.policies(new RedirectPolicy(308))
.policies(new RedirectPolicy())
.build();

HttpResponse response = pipeline.send(new HttpRequest(HttpMethod.GET,
Expand Down

0 comments on commit ee0f532

Please sign in to comment.