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
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,33 @@

import com.azure.core.http.HttpPipelineBuilder;
import com.azure.core.annotation.ServiceClientBuilder;
import com.azure.core.http.policy.AddDatePolicy;
import com.azure.core.util.logging.ClientLogger;
import com.azure.data.appconfiguration.implementation.ConfigurationClientCredentials;
import com.azure.data.appconfiguration.implementation.ConfigurationCredentialsPolicy;
import com.azure.data.appconfiguration.models.ConfigurationSetting;
import com.azure.core.util.Configuration;
import com.azure.core.http.HttpClient;
import com.azure.core.http.HttpHeaders;
import com.azure.core.http.HttpPipeline;
import com.azure.core.http.policy.AddHeadersPolicy;
import com.azure.core.http.policy.HttpLogDetailLevel;
import com.azure.core.http.policy.HttpLoggingPolicy;
import com.azure.core.http.policy.HttpPipelinePolicy;
import com.azure.core.http.policy.RequestIdPolicy;
import com.azure.core.http.policy.RetryPolicy;
import com.azure.core.http.policy.RetryPolicyOptions;
import com.azure.core.http.policy.ExponentialBackoff;
import com.azure.core.http.policy.AddDatePolicy;
import com.azure.core.http.policy.UserAgentPolicy;
import com.azure.core.http.policy.HttpPolicyProviders;
import com.azure.core.http.policy.HttpLogOptions;
import com.azure.core.util.logging.ClientLogger;
import com.azure.data.appconfiguration.implementation.ConfigurationClientCredentials;
import com.azure.data.appconfiguration.implementation.ConfigurationCredentialsPolicy;
import com.azure.data.appconfiguration.models.ConfigurationSetting;
import com.azure.core.util.Configuration;
import com.azure.core.http.HttpClient;
import com.azure.core.http.HttpHeaders;
import com.azure.core.http.HttpPipeline;
import com.azure.core.util.CoreUtils;

import java.net.MalformedURLException;
import java.net.URL;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -76,6 +79,9 @@ public final class ConfigurationClientBuilder {
private static final String APP_CONFIG_PROPERTIES = "azure-appconfig.properties";
private static final String NAME = "name";
private static final String VERSION = "version";
private static final String RETRY_AFTER_MS_HEADER = "retry-after-ms";
private static final RetryPolicy DEFAULT_RETRY_POLICY = new RetryPolicy(
new RetryPolicyOptions(new ExponentialBackoff(), RETRY_AFTER_MS_HEADER, ChronoUnit.MILLIS));

private final ClientLogger logger = new ClientLogger(ConfigurationClientBuilder.class);
private final List<HttpPipelinePolicy> policies;
Expand Down Expand Up @@ -175,7 +181,7 @@ public ConfigurationAsyncClient buildAsyncClient() {
policies.add(new ConfigurationCredentialsPolicy(buildCredential));
HttpPolicyProviders.addBeforeRetryPolicies(policies);

policies.add(retryPolicy == null ? new RetryPolicy() : retryPolicy);
policies.add(retryPolicy == null ? DEFAULT_RETRY_POLICY : retryPolicy);

policies.addAll(this.policies);
HttpPolicyProviders.addAfterRetryPolicies(policies);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.azure.core.http.HttpPipelineNextPolicy;
import com.azure.core.http.HttpRequest;
import com.azure.core.http.HttpResponse;

import java.util.Objects;
import com.azure.core.util.logging.ClientLogger;
import reactor.core.publisher.Mono;
Expand All @@ -17,19 +18,20 @@

/**
* A pipeline policy that retries when a recoverable HTTP error occurs.
* @see RetryPolicyOptions
*/
public class RetryPolicy implements HttpPipelinePolicy {

private static final String RETRY_AFTER_MS_HEADER = "retry-after-ms";

private final ClientLogger logger = new ClientLogger(RetryPolicy.class);
private final RetryStrategy retryStrategy;

private final RetryPolicyOptions retryPolicyOptions;

/**
* Creates a default {@link ExponentialBackoff} retry policy.
* Creates {@link RetryPolicy} with default {@link ExponentialBackoff} as {@link RetryStrategy}and use
* 'retry-after-ms' in {@link HttpResponse} header for calculating retry delay.
*/
public RetryPolicy() {
this(new ExponentialBackoff());
this(new RetryPolicyOptions(new ExponentialBackoff()));
}

/**
Expand All @@ -38,7 +40,22 @@ public RetryPolicy() {
* @param retryStrategy The {@link RetryStrategy} used for retries.
*/
public RetryPolicy(RetryStrategy retryStrategy) {
this.retryStrategy = Objects.requireNonNull(retryStrategy, "'retryStrategy' cannot be null");
Objects.requireNonNull(retryStrategy, "'retryStrategy' cannot be null");
this.retryPolicyOptions = new RetryPolicyOptions(retryStrategy);
}

/**
* Creates a {@link RetryPolicy} with the provided {@link RetryPolicyOptions}.
*
* @param retryPolicyOptions with given {@link RetryPolicyOptions}.
* @throws NullPointerException if {@code retryPolicyOptions} or {@code retryPolicyOptions getRetryStrategy }
* is {@code null}.
*/
public RetryPolicy(RetryPolicyOptions retryPolicyOptions) {
this.retryPolicyOptions = Objects.requireNonNull(retryPolicyOptions,
"'retryPolicyOptions' cannot be null.");
Objects.requireNonNull(retryPolicyOptions.getRetryStrategy(),
"'retryPolicyOptions.retryStrategy' cannot be null.");
}

@Override
Expand All @@ -62,11 +79,11 @@ private Mono<HttpResponse> attemptAsync(final HttpPipelineCallContext context, f
}
})
.onErrorResume(err -> {
int maxRetries = retryStrategy.getMaxRetries();
int maxRetries = retryPolicyOptions.getRetryStrategy().getMaxRetries();
if (tryCount < maxRetries) {
logger.verbose("[Error Resume] Try count: {}, Error: {}", tryCount, err);
return attemptAsync(context, next, originalHttpRequest, tryCount + 1)
.delaySubscription(retryStrategy.calculateRetryDelay(tryCount));
.delaySubscription(retryPolicyOptions.getRetryStrategy().calculateRetryDelay(tryCount));
} else {
return Mono.error(new RuntimeException(
String.format("Max retries %d times exceeded. Error Details: %s", maxRetries, err.getMessage()),
Expand All @@ -76,7 +93,8 @@ private Mono<HttpResponse> attemptAsync(final HttpPipelineCallContext context, f
}

private boolean shouldRetry(HttpResponse response, int tryCount) {
return tryCount < retryStrategy.getMaxRetries() && retryStrategy.shouldRetry(response);
return tryCount < retryPolicyOptions.getRetryStrategy().getMaxRetries()
&& retryPolicyOptions.getRetryStrategy().shouldRetry(response);
}

/**
Expand All @@ -91,17 +109,21 @@ private Duration determineDelayDuration(HttpResponse response, int tryCount) {
// Response will not have a retry-after-ms header.
if (code != 429 // too many requests
&& code != 503) { // service unavailable
return retryStrategy.calculateRetryDelay(tryCount);
return retryPolicyOptions.getRetryStrategy().calculateRetryDelay(tryCount);
}

String retryHeader = response.getHeaderValue(RETRY_AFTER_MS_HEADER);
String retryHeaderValue = null;

if (!isNullOrEmpty(retryPolicyOptions.getRetryAfterHeader())) {
retryHeaderValue = response.getHeaderValue(retryPolicyOptions.getRetryAfterHeader());
}

// Retry header is missing or empty, return the default delay duration.
if (isNullOrEmpty(retryHeader)) {
return retryStrategy.calculateRetryDelay(tryCount);
if (isNullOrEmpty(retryHeaderValue)) {
return retryPolicyOptions.getRetryStrategy().calculateRetryDelay(tryCount);
}

// Use the response delay duration, the server returned it for a reason.
return Duration.ofMillis(Integer.parseInt(retryHeader));
return Duration.of(Integer.parseInt(retryHeaderValue), retryPolicyOptions.getRetryAfterTimeUnit());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.core.http.policy;

import com.azure.core.annotation.Immutable;

import java.time.temporal.ChronoUnit;
import java.util.Objects;

import static com.azure.core.util.CoreUtils.isNullOrEmpty;

/**
* Immutable Configuration options for {@link RetryPolicy}.
*/
@Immutable
public class RetryPolicyOptions {

private final RetryStrategy retryStrategy;
private final String retryAfterHeader;
private final ChronoUnit retryAfterTimeUnit;

/**
* Creates a default {@link RetryPolicyOptions} used by a {@link RetryPolicy}. This will use
* {@link ExponentialBackoff} as the {@link #getRetryStrategy retry strategy} and will ignore retry delay headers.
*/
public RetryPolicyOptions() {
this(new ExponentialBackoff(), null, null);
}

/**
* Creates the {@link RetryPolicyOptions} with provided {@link RetryStrategy} that will be used when a request is
* retried. It will ignore retry delay headers.
*
* @param retryStrategy The {@link RetryStrategy} used for retries. It will default to {@link ExponentialBackoff}
* if provided value is {@code null}
*/
public RetryPolicyOptions(RetryStrategy retryStrategy) {
this(retryStrategy, null, null);
}

/**
* Creates the {@link RetryPolicyOptions} with provided {@link RetryStrategy}, {@code retryAfterHeader} and
* {@code retryAfterTimeUnit} that will be used when a request is retried.
*
* @param retryStrategy The {@link RetryStrategy} used for retries. It will default to {@link ExponentialBackoff}
* if provided value is {@code null}.
* @param retryAfterHeader The HTTP header, such as 'Retry-After' or 'x-ms-retry-after-ms', to lookup for the
* retry delay. If the value is {@code null}, {@link RetryPolicy} will use the retry strategy to compute the delay
* and ignore the delay provided in response header.
* @param retryAfterTimeUnit The time unit to use when applying the retry delay. {@code null} is valid if, and only
* if, {@code retryAfterHeader} is {@code null}.
* @throws NullPointerException When {@code retryAfterTimeUnit} is {@code null} and {@code retryAfterHeader} is
* not {@code null}.
*/
public RetryPolicyOptions(RetryStrategy retryStrategy, String retryAfterHeader, ChronoUnit retryAfterTimeUnit) {

if (Objects.isNull(retryStrategy)) {
this.retryStrategy = new ExponentialBackoff();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This overload should throw NPE if retryStrategy is null.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We tried to avoid many variations on constructor. Strategy cannot be null, Thus defaulting to ExponentialBackoff . Following constructor was removed.

public RetryPolicyOptions( String retryAfterHeader, ChronoUnit retryAfterTimeUnit) ;
trying to manage between many variations in constructor or few constructor.

} else {
this.retryStrategy = retryStrategy;
}
this.retryAfterHeader = retryAfterHeader;
this.retryAfterTimeUnit = retryAfterTimeUnit;
if (!isNullOrEmpty(retryAfterHeader)) {
Objects.requireNonNull(retryAfterTimeUnit, "'retryAfterTimeUnit' cannot be null.");
}
}

/**
* @return The {@link RetryStrategy} used when retrying requests.
*/
public RetryStrategy getRetryStrategy() {
return retryStrategy;
}

/**
* @return The HTTP header which contains the retry delay returned by the service.
*/
public String getRetryAfterHeader() {
return retryAfterHeader;
}

/**
* @return The {@link ChronoUnit} used when applying request retry delays.
*/
public ChronoUnit getRetryAfterTimeUnit() {
return retryAfterTimeUnit;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public Mono<HttpResponse> send(HttpRequest request) {
return Mono.just(mockResponse);
}
})
.policies(new RequestIdPolicy(), new RetryPolicy(new FixedDelay(1, Duration.of(0, ChronoUnit.SECONDS))))
.policies(new RequestIdPolicy(), new RetryPolicy(new RetryPolicyOptions(new FixedDelay(1, Duration.of(0, ChronoUnit.SECONDS)))))
.build();

pipeline.send(new HttpRequest(HttpMethod.GET, new URL("http://localhost/"))).block();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public Mono<HttpResponse> send(HttpRequest request) {
return Mono.just(new MockHttpResponse(request, codes[count++]));
}
})
.policies(new RetryPolicy(new FixedDelay(3, Duration.of(0, ChronoUnit.MILLIS))))
.policies(new RetryPolicy(new RetryPolicyOptions(new FixedDelay(3, Duration.of(0, ChronoUnit.MILLIS)))))
.build();

HttpResponse response = pipeline.send(new HttpRequest(HttpMethod.GET,
Expand All @@ -55,7 +55,7 @@ public Mono<HttpResponse> send(HttpRequest request) {
return Mono.just(new MockHttpResponse(request, 500));
}
})
.policies(new RetryPolicy(new FixedDelay(maxRetries, Duration.of(0, ChronoUnit.MILLIS))))
.policies(new RetryPolicy(new RetryPolicyOptions(new FixedDelay(maxRetries, Duration.of(0, ChronoUnit.MILLIS)))))
.build();

HttpResponse response = pipeline.send(new HttpRequest(HttpMethod.GET,
Expand Down Expand Up @@ -83,7 +83,7 @@ public Mono<HttpResponse> send(HttpRequest request) {
return Mono.just(new MockHttpResponse(request, 500));
}
})
.policies(new RetryPolicy(new FixedDelay(maxRetries, Duration.ofMillis(delayMillis))))
.policies(new RetryPolicy(new RetryPolicyOptions(new FixedDelay(maxRetries, Duration.ofMillis(delayMillis)))))
.build();

HttpResponse response = pipeline.send(new HttpRequest(HttpMethod.GET,
Expand Down Expand Up @@ -115,7 +115,7 @@ public Mono<HttpResponse> send(HttpRequest request) {
return Mono.just(new MockHttpResponse(request, 503));
}
})
.policies(new RetryPolicy(exponentialBackoff))
.policies(new RetryPolicy(new RetryPolicyOptions(exponentialBackoff)))
.build();

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