diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/ConfigurationClientBuilder.java b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/ConfigurationClientBuilder.java index 5bde168b3adb..7bf355f96c58 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/ConfigurationClientBuilder.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/ConfigurationClientBuilder.java @@ -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; @@ -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 policies; @@ -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); diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/RetryPolicy.java b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/RetryPolicy.java index 9073b51c7881..0a87157a2a35 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/RetryPolicy.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/RetryPolicy.java @@ -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; @@ -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())); } /** @@ -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 @@ -62,11 +79,11 @@ private Mono 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()), @@ -76,7 +93,8 @@ private Mono 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); } /** @@ -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()); } } diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/RetryPolicyOptions.java b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/RetryPolicyOptions.java new file mode 100644 index 000000000000..be3fd313287f --- /dev/null +++ b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/RetryPolicyOptions.java @@ -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(); + } 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; + } + +} diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/http/policy/RequestIdPolicyTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/http/policy/RequestIdPolicyTests.java index 8225182df2a5..b28c67c3a8e7 100644 --- a/sdk/core/azure-core/src/test/java/com/azure/core/http/policy/RequestIdPolicyTests.java +++ b/sdk/core/azure-core/src/test/java/com/azure/core/http/policy/RequestIdPolicyTests.java @@ -108,7 +108,7 @@ public Mono 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(); diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/http/policy/RetryPolicyTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/http/policy/RetryPolicyTests.java index 9157b54f1442..27f81b0b2a09 100644 --- a/sdk/core/azure-core/src/test/java/com/azure/core/http/policy/RetryPolicyTests.java +++ b/sdk/core/azure-core/src/test/java/com/azure/core/http/policy/RetryPolicyTests.java @@ -33,7 +33,7 @@ public Mono 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, @@ -55,7 +55,7 @@ public Mono 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, @@ -83,7 +83,7 @@ public Mono 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, @@ -115,7 +115,7 @@ public Mono 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,