-
Notifications
You must be signed in to change notification settings - Fork 912
Implemented Retries functionality #3307
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
/* | ||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"). | ||
* You may not use this file except in compliance with the License. | ||
* A copy of the License is located at | ||
* | ||
* http://aws.amazon.com/apache2.0 | ||
* | ||
* or in the "license" file accompanying this file. This file is distributed | ||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either | ||
* express or implied. See the License for the specific language governing | ||
* permissions and limitations under the License. | ||
*/ | ||
|
||
package software.amazon.awssdk.imds; | ||
|
||
import java.util.Objects; | ||
import software.amazon.awssdk.annotations.SdkPublicApi; | ||
import software.amazon.awssdk.core.retry.RetryMode; | ||
import software.amazon.awssdk.core.retry.backoff.BackoffStrategy; | ||
import software.amazon.awssdk.utils.builder.CopyableBuilder; | ||
import software.amazon.awssdk.utils.builder.ToCopyableBuilder; | ||
|
||
/** | ||
* Interface for specifying a retry policy to use when evaluating whether or not a request should be retried. The | ||
* {@link #builder()}} can be used to construct a retry policy from SDK provided policies or policies that directly implement | ||
* {@link BackoffStrategy} . | ||
* | ||
* When using the {@link #builder()} the SDK will use default values for fields that are not provided. The default number of | ||
* retries and condition is based on the current {@link RetryMode}. | ||
* | ||
* @see BackoffStrategy for a list of SDK provided backoff strategies | ||
*/ | ||
@SdkPublicApi | ||
public class Ec2MetadataRetryPolicy implements ToCopyableBuilder<Ec2MetadataRetryPolicy.Builder, Ec2MetadataRetryPolicy> { | ||
|
||
private final BackoffStrategy backoffStrategy; | ||
private final Integer numRetries; | ||
zoewangg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
private Ec2MetadataRetryPolicy(BuilderImpl builder) { | ||
|
||
this.numRetries = builder.numRetries != null ? builder.numRetries : 3; | ||
|
||
this.backoffStrategy = builder.backoffStrategy != null ? builder.backoffStrategy : | ||
BackoffStrategy.defaultStrategy(RetryMode.STANDARD); | ||
} | ||
|
||
@Override | ||
public boolean equals(Object o) { | ||
if (this == o) { | ||
return true; | ||
} | ||
if (o == null || getClass() != o.getClass()) { | ||
return false; | ||
} | ||
Ec2MetadataRetryPolicy ec2MetadataRetryPolicy = (Ec2MetadataRetryPolicy) o; | ||
|
||
if (!Objects.equals(numRetries, ec2MetadataRetryPolicy.numRetries)) { | ||
return false; | ||
} | ||
return Objects.equals(backoffStrategy, ec2MetadataRetryPolicy.backoffStrategy); | ||
} | ||
|
||
@Override | ||
public int hashCode() { | ||
|
||
Comment on lines
+48
to
+66
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's add tests for equals and hashcode There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will add EqualsVerifier method |
||
int result = numRetries.hashCode(); | ||
result = 31 * result + backoffStrategy.hashCode(); | ||
return result; | ||
} | ||
|
||
public Integer numRetries() { | ||
return numRetries; | ||
} | ||
|
||
public BackoffStrategy backoffStrategy() { | ||
return backoffStrategy; | ||
} | ||
zoewangg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
public static Builder builder() { | ||
return new BuilderImpl(); | ||
} | ||
|
||
@Override | ||
public Builder toBuilder() { | ||
return builder().numRetries(numRetries) | ||
.backoffStrategy(backoffStrategy); | ||
} | ||
|
||
public interface Builder extends CopyableBuilder<Ec2MetadataRetryPolicy.Builder, Ec2MetadataRetryPolicy> { | ||
|
||
/** | ||
* Configure the backoff strategy that should be used for waiting in between retry attempts. | ||
*/ | ||
Builder backoffStrategy(BackoffStrategy backoffStrategy); | ||
|
||
/** | ||
* Configure the maximum number of times that a single request should be retried, assuming it fails for a retryable error. | ||
*/ | ||
Builder numRetries(Integer numRetries); | ||
|
||
@Override | ||
Ec2MetadataRetryPolicy build(); | ||
} | ||
|
||
private static final class BuilderImpl implements Builder { | ||
|
||
private Integer numRetries; | ||
private BackoffStrategy backoffStrategy; | ||
|
||
private BuilderImpl() { | ||
} | ||
|
||
@Override | ||
public Builder numRetries(Integer numRetries) { | ||
this.numRetries = numRetries; | ||
return this; | ||
} | ||
|
||
public void setNumRetries(Integer numRetries) { | ||
numRetries(numRetries); | ||
} | ||
|
||
@Override | ||
public Builder backoffStrategy(BackoffStrategy backoffStrategy) { | ||
this.backoffStrategy = backoffStrategy; | ||
return this; | ||
} | ||
|
||
public void setBackoffStrategy(BackoffStrategy backoffStrategy) { | ||
backoffStrategy(backoffStrategy); | ||
} | ||
|
||
@Override | ||
public Ec2MetadataRetryPolicy build() { | ||
return new Ec2MetadataRetryPolicy(this); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,14 +28,15 @@ | |
import software.amazon.awssdk.annotations.ThreadSafe; | ||
import software.amazon.awssdk.core.exception.SdkClientException; | ||
import software.amazon.awssdk.core.exception.SdkServiceException; | ||
import software.amazon.awssdk.core.retry.RetryPolicy; | ||
import software.amazon.awssdk.core.retry.RetryPolicyContext; | ||
import software.amazon.awssdk.http.AbortableInputStream; | ||
import software.amazon.awssdk.http.HttpExecuteRequest; | ||
import software.amazon.awssdk.http.HttpExecuteResponse; | ||
import software.amazon.awssdk.http.SdkHttpClient; | ||
import software.amazon.awssdk.http.SdkHttpMethod; | ||
import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; | ||
import software.amazon.awssdk.imds.Ec2Metadata; | ||
import software.amazon.awssdk.imds.Ec2MetadataRetryPolicy; | ||
import software.amazon.awssdk.imds.MetadataResponse; | ||
import software.amazon.awssdk.utils.IoUtils; | ||
|
||
|
@@ -54,7 +55,8 @@ public final class DefaultEc2Metadata implements Ec2Metadata { | |
private static final RequestMarshaller REQUEST_MARSHALLER = new RequestMarshaller(); | ||
|
||
private static final EndpointProvider ENDPOINT_PROVIDER = EndpointProvider.builder().build(); | ||
private final RetryPolicy retryPolicy; | ||
|
||
private final Ec2MetadataRetryPolicy retryPolicy; | ||
|
||
private final URI endpoint; | ||
|
||
|
@@ -68,7 +70,7 @@ public final class DefaultEc2Metadata implements Ec2Metadata { | |
|
||
private DefaultEc2Metadata(DefaultEc2Metadata.Ec2MetadataBuilder builder) { | ||
|
||
this.retryPolicy = builder.retryPolicy != null ? builder.retryPolicy : RetryPolicy.builder().build(); | ||
this.retryPolicy = builder.retryPolicy != null ? builder.retryPolicy : Ec2MetadataRetryPolicy.builder().build(); | ||
this.endpoint = URI.create(ENDPOINT_PROVIDER.resolveEndpoint(builder.endpoint, builder.endpointMode)); | ||
this.tokenTtl = builder.tokenTtl != null ? builder.tokenTtl : Duration.ofSeconds(21600); | ||
this.endpointMode = ENDPOINT_PROVIDER.resolveEndpointMode(builder.endpointMode); | ||
|
@@ -152,41 +154,57 @@ public String toString() { | |
public MetadataResponse get(String path) { | ||
|
||
MetadataResponse metadataResponse = null; | ||
String data = null; | ||
AbortableInputStream abortableInputStream = null; | ||
try { | ||
String token = getToken(); | ||
URI uri = URI.create(endpoint + path); | ||
HttpExecuteRequest httpExecuteRequest = REQUEST_MARSHALLER.createDataRequest(uri, SdkHttpMethod.GET, token, | ||
tokenTtl); | ||
HttpExecuteResponse response = httpClient.prepareRequest(httpExecuteRequest).call(); | ||
int statusCode = response.httpResponse().statusCode(); | ||
Optional<AbortableInputStream> responseBody = response.responseBody(); | ||
|
||
if (statusCode == HttpURLConnection.HTTP_OK && responseBody.isPresent()) { | ||
abortableInputStream = responseBody.get(); | ||
data = IoUtils.toUtf8String(abortableInputStream); | ||
metadataResponse = new MetadataResponse(data); | ||
} else if (statusCode == HttpURLConnection.HTTP_NOT_FOUND) { | ||
throw SdkServiceException.builder() | ||
.message("The requested metadata at path ( " + path + " ) is not found ").build(); | ||
} else if (statusCode == HttpURLConnection.HTTP_OK) { | ||
throw SdkClientException.builder() | ||
.message("Response body empty with Status Code " + statusCode).build(); | ||
} else { | ||
throw SdkClientException.builder() | ||
.message("Instance metadata service returned unexpected status code " + statusCode) | ||
.build(); | ||
for (int tries = 1 ; tries <= retryPolicy.numRetries() ; tries ++) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The max number of attempts should be retryPolicy.numRetries() + 1, right? |
||
|
||
try { | ||
String token = getToken(); | ||
zoewangg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
URI uri = URI.create(endpoint + path); | ||
HttpExecuteRequest httpExecuteRequest = REQUEST_MARSHALLER.createDataRequest(uri, SdkHttpMethod.GET, token, | ||
tokenTtl); | ||
HttpExecuteResponse response = httpClient.prepareRequest(httpExecuteRequest).call(); | ||
int statusCode = response.httpResponse().statusCode(); | ||
Optional<AbortableInputStream> responseBody = response.responseBody(); | ||
|
||
if (statusCode == HttpURLConnection.HTTP_OK && responseBody.isPresent()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should not use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Used HTTP_OK as it would be more neat. But as per discussion , as the variables are only of HttpURLConnection, will change it to direct status codes. |
||
abortableInputStream = responseBody.get(); | ||
String data = IoUtils.toUtf8String(abortableInputStream); | ||
metadataResponse = new MetadataResponse(data); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we return metadataResponse here to avoid else block? if (statusCode == 200 && responseBody.isPresent()) {
abortableInputStream = responseBody.get();
String data = IoUtils.toUtf8String(abortableInputStream);
metadataResponse = new MetadataResponse(data);
return metadataResponse;
}
handleException(statusCode, path); |
||
} else if (statusCode == HttpURLConnection.HTTP_NOT_FOUND) { | ||
throw SdkServiceException.builder() | ||
.message("The requested metadata at path ( " + path + " ) is not found ").build(); | ||
} else if (statusCode == HttpURLConnection.HTTP_OK) { | ||
throw SdkClientException.builder() | ||
.message("Response body empty with Status Code " + statusCode).build(); | ||
} else { | ||
throw SdkClientException.builder() | ||
.message("Instance metadata service returned unexpected status code " + statusCode) | ||
.build(); | ||
} | ||
} catch (SdkServiceException sd) { | ||
zoewangg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
throw SdkServiceException.builder().message(sd.getMessage()).cause(sd).build(); | ||
} catch (IOException | SdkClientException io) { | ||
zoewangg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
log.warn("Received an IOException {0} ", io); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Noted. |
||
if (tries == 3) { | ||
throw SdkClientException.builder().message("Unable to contact EC2 metadata service.").cause(io).build(); | ||
} | ||
|
||
Duration backoffTime = retryPolicy.backoffStrategy() | ||
.computeDelayBeforeNextRetry(RetryPolicyContext.builder() | ||
.retriesAttempted(tries - 1) | ||
.build()); | ||
|
||
try { | ||
Thread.sleep(backoffTime.toMillis()); | ||
} catch (InterruptedException e) { | ||
Thread.currentThread().interrupt(); | ||
zoewangg marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think interruptedException is already handled in |
||
} | ||
} finally { | ||
IoUtils.closeQuietly(abortableInputStream, log); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you break up this block(line 159 - 206 in case multi-line comment is not working) a little bit? It looks like it's doing retry, sending request and handling response. Ideally, a method should just do one thing.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Noted. Will modularize the code. |
||
} catch (SdkServiceException sd) { | ||
throw SdkServiceException.builder().message(sd.getMessage()).cause(sd).build(); | ||
} catch (IOException | SdkClientException io) { | ||
// TODO Retry Logic will be added | ||
log.warn("Received an IOException {0} " , io); | ||
} finally { | ||
IoUtils.closeQuietly(abortableInputStream, log); | ||
} | ||
|
||
return metadataResponse; | ||
} | ||
|
||
|
@@ -223,7 +241,7 @@ private String getToken() throws IOException { | |
|
||
private static final class Ec2MetadataBuilder implements Ec2Metadata.Builder { | ||
|
||
private RetryPolicy retryPolicy; | ||
private Ec2MetadataRetryPolicy retryPolicy; | ||
|
||
private URI endpoint; | ||
|
||
|
@@ -238,7 +256,7 @@ private static final class Ec2MetadataBuilder implements Ec2Metadata.Builder { | |
private Ec2MetadataBuilder() { | ||
} | ||
|
||
public void setRetryPolicy(RetryPolicy retryPolicy) { | ||
public void setRetryPolicy(Ec2MetadataRetryPolicy retryPolicy) { | ||
this.retryPolicy = retryPolicy; | ||
} | ||
|
||
|
@@ -263,7 +281,7 @@ public void setHttpClient(SdkHttpClient httpClient) { | |
} | ||
|
||
@Override | ||
public Builder retryPolicy(RetryPolicy retryPolicy) { | ||
public Builder retryPolicy(Ec2MetadataRetryPolicy retryPolicy) { | ||
this.retryPolicy = retryPolicy; | ||
return this; | ||
} | ||
|
Uh oh!
There was an error while loading. Please reload this page.