From cf225b2f9030dd359bba9a0870b75296a6f205b7 Mon Sep 17 00:00:00 2001 From: Zoe Wang Date: Mon, 30 Jul 2018 17:00:17 -0700 Subject: [PATCH] Implement apiCallAttemptTimeout and apiCallTimeout feature for asynchrounous calls. --- .../feature-AWSSDKforJavav2-bd6fe1d.json | 5 + .../awscore/client/utils/HttpTestUtils.java | 2 +- core/sdk-core/pom.xml | 6 + ...onClientExecutionTimerIntegrationTest.java | 4 +- ...myErrorResponseServerIntegrationTests.java | 84 ------ ...cessfulResponseServerIntegrationTests.java | 84 ------ .../UnresponsiveServerIntegrationTests.java | 147 --------- .../core/RequestOverrideConfiguration.java | 113 +++++++ .../builder/SdkDefaultClientBuilder.java | 15 +- .../config/ClientOverrideConfiguration.java | 116 +++++++ .../core/client/config/SdkClientOption.java | 16 +- .../ApiCallAttemptTimeoutException.java | 90 ++++++ ...tion.java => ApiCallTimeoutException.java} | 28 +- .../internal/RequestExecutionContext.java | 146 --------- .../internal/http/AmazonAsyncHttpClient.java | 2 - .../internal/http/AmazonSyncHttpClient.java | 11 - .../internal/http/HttpClientDependencies.java | 21 +- .../http/RequestExecutionContext.java | 19 +- .../pipeline/stages/AsyncRetryableStage.java | 32 +- .../stages/ClientExecutionTimedStage.java | 132 -------- .../stages/MakeAsyncHttpRequestStage.java | 72 ++++- .../stages/TimerExceptionHandlingStage.java | 79 ----- ...skImpl.java => ApiCallTimeoutTracker.java} | 34 +-- .../http/timers/AsyncTimeoutTask.java | 75 +++++ ...ackerTask.java => NoOpTimeoutTracker.java} | 25 +- ...AbortTrackerTask.java => TimeoutTask.java} | 16 +- .../http/timers/TimeoutThreadPoolBuilder.java | 62 ---- ...tionAbortTask.java => TimeoutTracker.java} | 29 +- .../core/internal/http/timers/TimerUtils.java | 62 ++++ .../client/ClientExecutionAbortTaskImpl.java | 63 ---- .../timers/client/ClientExecutionTimer.java | 102 ------- .../retry/SdkDefaultRetrySetting.java | 2 + .../stages/ClientExecutionTimedStageTest.java | 94 ------ .../stages/MakeAsyncHttpRequestStageTest.java | 110 +++++++ .../stages/MoveParametersToBodyStageTest.java | 3 + .../AsyncHttpClientApiCallTimeoutTests.java | 192 ++++++++++++ ...ientExecutionAndRequestTimerTestUtils.java | 15 - .../http/timers/TimeoutTestConstants.java | 4 +- .../http/timers/client/MockedClientTests.java | 88 ------ .../util/AsyncResponseHandlerTestUtils.java | 81 +++++ .../src/test/java/utils/HttpTestUtils.java | 80 ++++- .../src/test/java/utils/ValidSdkObjects.java | 11 +- .../src/test/resources/log4j.properties | 8 +- .../nio/netty/internal/RunnableRequest.java | 2 + .../src/test/resources/log4j.properties | 2 +- .../AsyncGetObjectFaultIntegrationTest.java | 124 ++++++++ .../s3/GetObjectFaultIntegrationTest.java | 10 +- .../s3/src/test/resources/log4j.properties | 2 +- .../protocol/wiremock/WireMockUtils.java | 7 + .../tests/retry/AsyncAwsJsonRetryTest.java | 158 ++++++++++ .../AsyncApiCallAttemptsTimeoutTest.java | 283 ++++++++++++++++++ .../timeout/AsyncApiCallTimeoutTest.java | 203 +++++++++++++ .../tests/timeout/AsyncTimeoutTest.java | 93 ++++++ .../src/test/resources/log4j.properties | 1 - .../amazon/awssdk/utils/OptionalUtils.java | 8 + .../amazon/awssdk/utils/Validate.java | 16 + 56 files changed, 2045 insertions(+), 1244 deletions(-) create mode 100644 .changes/next-release/feature-AWSSDKforJavav2-bd6fe1d.json delete mode 100644 core/sdk-core/src/it/java/software/amazon/awssdk/core/http/timers/client/DummyErrorResponseServerIntegrationTests.java delete mode 100644 core/sdk-core/src/it/java/software/amazon/awssdk/core/http/timers/client/DummySuccessfulResponseServerIntegrationTests.java delete mode 100644 core/sdk-core/src/it/java/software/amazon/awssdk/core/http/timers/client/UnresponsiveServerIntegrationTests.java create mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/exception/ApiCallAttemptTimeoutException.java rename core/sdk-core/src/main/java/software/amazon/awssdk/core/exception/{ClientExecutionTimeoutException.java => ApiCallTimeoutException.java} (62%) delete mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/RequestExecutionContext.java delete mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ClientExecutionTimedStage.java delete mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/TimerExceptionHandlingStage.java rename core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/{client/ClientExecutionAbortTrackerTaskImpl.java => ApiCallTimeoutTracker.java} (50%) create mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/AsyncTimeoutTask.java rename core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/{client/NoOpClientExecutionAbortTrackerTask.java => NoOpTimeoutTracker.java} (59%) rename core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/{client/ClientExecutionAbortTrackerTask.java => TimeoutTask.java} (66%) delete mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/TimeoutThreadPoolBuilder.java rename core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/{client/ClientExecutionAbortTask.java => TimeoutTracker.java} (59%) create mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/TimerUtils.java delete mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/client/ClientExecutionAbortTaskImpl.java delete mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/client/ClientExecutionTimer.java delete mode 100644 core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ClientExecutionTimedStageTest.java create mode 100644 core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/MakeAsyncHttpRequestStageTest.java create mode 100644 core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/AsyncHttpClientApiCallTimeoutTests.java delete mode 100644 core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/client/MockedClientTests.java create mode 100644 core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/util/AsyncResponseHandlerTestUtils.java create mode 100644 services/s3/src/it/java/software/amazon/awssdk/services/s3/AsyncGetObjectFaultIntegrationTest.java create mode 100644 test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/retry/AsyncAwsJsonRetryTest.java create mode 100644 test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/timeout/AsyncApiCallAttemptsTimeoutTest.java create mode 100644 test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/timeout/AsyncApiCallTimeoutTest.java create mode 100644 test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/timeout/AsyncTimeoutTest.java diff --git a/.changes/next-release/feature-AWSSDKforJavav2-bd6fe1d.json b/.changes/next-release/feature-AWSSDKforJavav2-bd6fe1d.json new file mode 100644 index 000000000000..fcad4356985e --- /dev/null +++ b/.changes/next-release/feature-AWSSDKforJavav2-bd6fe1d.json @@ -0,0 +1,5 @@ +{ + "category": "AWS SDK for Java v2", + "type": "feature", + "description": "Implement apiCallAttemptTimeout and apiCallTimeout feature for asynchrounous calls. Customers can specify timeout via `ClientOverrideConfiguaration.Builder#apiCallTimeout(Duration)` or `RequestOverrideConfiguration.Builder#apiCallAttemptTimeout(Duration)`. Note: this feature is only implemented for asynchrounous api calls." +} diff --git a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/utils/HttpTestUtils.java b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/utils/HttpTestUtils.java index 63fc4b9aff92..5aa30fdccfc9 100644 --- a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/utils/HttpTestUtils.java +++ b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/utils/HttpTestUtils.java @@ -56,7 +56,7 @@ public static SdkClientConfiguration testClientConfiguration() { .option(SdkAdvancedClientOption.USER_AGENT_PREFIX, "") .option(SdkAdvancedClientOption.USER_AGENT_SUFFIX, "") .option(SdkAdvancedAsyncClientOption.FUTURE_COMPLETION_EXECUTOR, Runnable::run) - .option(SdkClientOption.ASYNC_RETRY_EXECUTOR_SERVICE, Executors.newScheduledThreadPool(1)) + .option(SdkClientOption.SCHEDULED_EXECUTOR_SERVICE, Executors.newScheduledThreadPool(1)) .build(); } diff --git a/core/sdk-core/pom.xml b/core/sdk-core/pom.xml index 64bc02f8ccaf..1c111e43ed97 100644 --- a/core/sdk-core/pom.xml +++ b/core/sdk-core/pom.xml @@ -164,6 +164,12 @@ hamcrest-core test + + software.amazon.awssdk + netty-nio-client + ${awsjavasdk.version} + test + diff --git a/core/sdk-core/src/it/java/software/amazon/awssdk/core/http/timers/client/AbortedExceptionClientExecutionTimerIntegrationTest.java b/core/sdk-core/src/it/java/software/amazon/awssdk/core/http/timers/client/AbortedExceptionClientExecutionTimerIntegrationTest.java index b61ac3915c9a..be534853cc92 100644 --- a/core/sdk-core/src/it/java/software/amazon/awssdk/core/http/timers/client/AbortedExceptionClientExecutionTimerIntegrationTest.java +++ b/core/sdk-core/src/it/java/software/amazon/awssdk/core/http/timers/client/AbortedExceptionClientExecutionTimerIntegrationTest.java @@ -36,7 +36,7 @@ import org.mockito.runners.MockitoJUnitRunner; import software.amazon.awssdk.annotations.ReviewBeforeRelease; import software.amazon.awssdk.core.exception.AbortedException; -import software.amazon.awssdk.core.exception.ClientExecutionTimeoutException; +import software.amazon.awssdk.core.exception.ApiCallTimeoutException; import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.core.http.ExecutionContext; import software.amazon.awssdk.core.http.MockServerTestBase; @@ -90,7 +90,7 @@ public void clientExecutionTimeoutEnabled_aborted_exception_occurs_timeout_not_e execute(httpClient, createMockGetRequest()); } - @Test(expected = ClientExecutionTimeoutException.class) + @Test(expected = ApiCallTimeoutException.class) public void clientExecutionTimeoutEnabled_aborted_exception_occurs_timeout_expired() throws Exception { // Simulate a slow HTTP request when(abortableCallable.call()).thenAnswer(i -> { diff --git a/core/sdk-core/src/it/java/software/amazon/awssdk/core/http/timers/client/DummyErrorResponseServerIntegrationTests.java b/core/sdk-core/src/it/java/software/amazon/awssdk/core/http/timers/client/DummyErrorResponseServerIntegrationTests.java deleted file mode 100644 index 257aa09091b2..000000000000 --- a/core/sdk-core/src/it/java/software/amazon/awssdk/core/http/timers/client/DummyErrorResponseServerIntegrationTests.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2010-2018 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.core.http.timers.client; - -import static software.amazon.awssdk.core.internal.http.timers.TimeoutTestConstants.SLOW_REQUEST_HANDLER_TIMEOUT; -import static software.amazon.awssdk.core.internal.http.timers.TimeoutTestConstants.TEST_TIMEOUT; - -import java.util.Collections; -import org.junit.BeforeClass; -import org.junit.Ignore; -import org.junit.Test; -import software.amazon.awssdk.annotations.ReviewBeforeRelease; -import software.amazon.awssdk.core.TestPreConditions; -import software.amazon.awssdk.core.exception.ClientExecutionTimeoutException; -import software.amazon.awssdk.core.http.ExecutionContext; -import software.amazon.awssdk.core.http.MockServerTestBase; -import software.amazon.awssdk.core.http.server.MockServer; -import software.amazon.awssdk.core.internal.http.AmazonSyncHttpClient; -import software.amazon.awssdk.core.internal.http.request.SlowExecutionInterceptor; -import software.amazon.awssdk.core.internal.http.response.NullErrorResponseHandler; -import software.amazon.awssdk.core.internal.http.response.UnresponsiveErrorResponseHandler; -import software.amazon.awssdk.core.internal.interceptor.ExecutionInterceptorChain; -import utils.HttpTestUtils; - -/** - * Tests that use a server that returns a predetermined error response within the timeout limit - */ -@Ignore -@ReviewBeforeRelease("add it back once execution time out is added back") -public class DummyErrorResponseServerIntegrationTests extends MockServerTestBase { - - private static final int STATUS_CODE = 500; - private AmazonSyncHttpClient httpClient; - - @BeforeClass - public static void preConditions() { - TestPreConditions.assumeNotJava6(); - } - - @Override - protected MockServer buildMockServer() { - return new MockServer( - MockServer.DummyResponseServerBehavior.build(STATUS_CODE, "Internal Server Failure", "Dummy response")); - } - - @Test(timeout = TEST_TIMEOUT, expected = ClientExecutionTimeoutException.class) - public void clientExecutionTimeoutEnabled_SlowErrorResponseHandler_ThrowsClientExecutionTimeoutException() - throws Exception { - httpClient = HttpTestUtils.testAmazonHttpClient(); - - httpClient.requestExecutionBuilder().request(newGetRequest()).errorResponseHandler(new UnresponsiveErrorResponseHandler()) - .execute(); - } - - @Test(timeout = TEST_TIMEOUT, expected = ClientExecutionTimeoutException.class) - public void clientExecutionTimeoutEnabled_SlowAfterErrorRequestHandler_ThrowsClientExecutionTimeoutException() - throws Exception { - httpClient = HttpTestUtils.testAmazonHttpClient(); - - ExecutionInterceptorChain interceptors = - new ExecutionInterceptorChain(Collections.singletonList( - new SlowExecutionInterceptor().onExecutionFailureWaitInSeconds(SLOW_REQUEST_HANDLER_TIMEOUT))); - - httpClient.requestExecutionBuilder() - .request(newGetRequest()) - .errorResponseHandler(new NullErrorResponseHandler()) - .executionContext(ExecutionContext.builder().interceptorChain(interceptors).build()) - .execute(); - } - -} diff --git a/core/sdk-core/src/it/java/software/amazon/awssdk/core/http/timers/client/DummySuccessfulResponseServerIntegrationTests.java b/core/sdk-core/src/it/java/software/amazon/awssdk/core/http/timers/client/DummySuccessfulResponseServerIntegrationTests.java deleted file mode 100644 index b58267c556f8..000000000000 --- a/core/sdk-core/src/it/java/software/amazon/awssdk/core/http/timers/client/DummySuccessfulResponseServerIntegrationTests.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2010-2018 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.core.http.timers.client; - -import static software.amazon.awssdk.core.internal.http.timers.TimeoutTestConstants.SLOW_REQUEST_HANDLER_TIMEOUT; -import static software.amazon.awssdk.core.internal.http.timers.TimeoutTestConstants.TEST_TIMEOUT; - -import java.util.Arrays; -import org.junit.Ignore; -import org.junit.Test; -import software.amazon.awssdk.annotations.ReviewBeforeRelease; -import software.amazon.awssdk.core.exception.ClientExecutionTimeoutException; -import software.amazon.awssdk.core.http.ExecutionContext; -import software.amazon.awssdk.core.http.MockServerTestBase; -import software.amazon.awssdk.core.http.server.MockServer; -import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; -import software.amazon.awssdk.core.internal.http.AmazonSyncHttpClient; -import software.amazon.awssdk.core.internal.http.request.SlowExecutionInterceptor; -import software.amazon.awssdk.core.internal.http.response.DummyResponseHandler; -import software.amazon.awssdk.core.internal.http.response.UnresponsiveResponseHandler; -import software.amazon.awssdk.core.internal.interceptor.ExecutionInterceptorChain; -import utils.HttpTestUtils; -@Ignore -@ReviewBeforeRelease("add it back once execution time out is added back") -public class DummySuccessfulResponseServerIntegrationTests extends MockServerTestBase { - - private static final int STATUS_CODE = 200; - - private AmazonSyncHttpClient httpClient; - - @Override - protected MockServer buildMockServer() { - return new MockServer(MockServer.DummyResponseServerBehavior.build(STATUS_CODE, "OK", "Hi")); - } - - @Test(timeout = TEST_TIMEOUT, expected = ClientExecutionTimeoutException.class) - public void clientExecutionTimeoutEnabled_SlowResponseHandler_ThrowsClientExecutionTimeoutException() - throws Exception { - httpClient = HttpTestUtils.testAmazonHttpClient(); - requestBuilder().execute(new UnresponsiveResponseHandler()); - } - - @Test(timeout = TEST_TIMEOUT, expected = ClientExecutionTimeoutException.class) - public void clientExecutionTimeoutEnabled_SlowAfterResponseRequestHandler_ThrowsClientExecutionTimeoutException() - throws Exception { - httpClient = HttpTestUtils.testAmazonHttpClient(); - - ExecutionInterceptor interceptor = - new SlowExecutionInterceptor().afterTransmissionWaitInSeconds(SLOW_REQUEST_HANDLER_TIMEOUT); - requestBuilder().executionContext(withInterceptors(interceptor)).execute(new DummyResponseHandler()); - } - - @Test(timeout = TEST_TIMEOUT, expected = ClientExecutionTimeoutException.class) - public void clientExecutionTimeoutEnabled_SlowBeforeRequestRequestHandler_ThrowsClientExecutionTimeoutException() - throws Exception { - httpClient = HttpTestUtils.testAmazonHttpClient(); - - ExecutionInterceptor interceptor = - new SlowExecutionInterceptor().beforeTransmissionWaitInSeconds(SLOW_REQUEST_HANDLER_TIMEOUT); - requestBuilder().executionContext(withInterceptors(interceptor)).execute(new DummyResponseHandler()); - } - - private AmazonSyncHttpClient.RequestExecutionBuilder requestBuilder() { - return httpClient.requestExecutionBuilder().request(newGetRequest()); - } - - private ExecutionContext withInterceptors(ExecutionInterceptor... requestHandlers) { - return ExecutionContext.builder().interceptorChain(new ExecutionInterceptorChain(Arrays.asList(requestHandlers))).build(); - } - -} diff --git a/core/sdk-core/src/it/java/software/amazon/awssdk/core/http/timers/client/UnresponsiveServerIntegrationTests.java b/core/sdk-core/src/it/java/software/amazon/awssdk/core/http/timers/client/UnresponsiveServerIntegrationTests.java deleted file mode 100644 index 64329729abf7..000000000000 --- a/core/sdk-core/src/it/java/software/amazon/awssdk/core/http/timers/client/UnresponsiveServerIntegrationTests.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright 2010-2018 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.core.http.timers.client; - -import static org.hamcrest.Matchers.instanceOf; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static software.amazon.awssdk.core.internal.http.timers.ClientExecutionAndRequestTimerTestUtils.assertClientExecutionTimerExecutorNotCreated; -import static software.amazon.awssdk.core.internal.http.timers.ClientExecutionAndRequestTimerTestUtils.assertNumberOfTasksTriggered; -import static software.amazon.awssdk.core.internal.http.timers.ClientExecutionAndRequestTimerTestUtils.interruptCurrentThreadAfterDelay; -import static software.amazon.awssdk.core.internal.http.timers.TimeoutTestConstants.CLIENT_EXECUTION_TIMEOUT; -import static software.amazon.awssdk.core.internal.http.timers.TimeoutTestConstants.PRECISION_MULTIPLIER; -import static software.amazon.awssdk.core.internal.http.timers.TimeoutTestConstants.TEST_TIMEOUT; - -import java.io.IOException; -import java.net.SocketTimeoutException; -import java.time.Duration; -import org.junit.BeforeClass; -import org.junit.Ignore; -import org.junit.Test; -import software.amazon.awssdk.annotations.ReviewBeforeRelease; -import software.amazon.awssdk.core.TestPreConditions; -import software.amazon.awssdk.core.client.config.SdkClientOption; -import software.amazon.awssdk.core.exception.ClientExecutionTimeoutException; -import software.amazon.awssdk.core.exception.SdkClientException; -import software.amazon.awssdk.core.http.UnresponsiveMockServerTestBase; -import software.amazon.awssdk.core.internal.http.AmazonSyncHttpClient; -import software.amazon.awssdk.core.retry.FixedTimeBackoffStrategy; -import software.amazon.awssdk.core.retry.RetryPolicy; -import software.amazon.awssdk.http.SdkHttpClient; -import software.amazon.awssdk.http.apache.ApacheHttpClient; -import utils.HttpTestUtils; - -@Ignore -@ReviewBeforeRelease("Add the tests back once execution, request timeout are added back") -public class UnresponsiveServerIntegrationTests extends UnresponsiveMockServerTestBase { - - private static final Duration LONGER_SOCKET_TIMEOUT = - Duration.ofMillis(CLIENT_EXECUTION_TIMEOUT.toMillis() * PRECISION_MULTIPLIER); - private static final Duration SHORTER_SOCKET_TIMEOUT = - Duration.ofMillis(CLIENT_EXECUTION_TIMEOUT.toMillis() / PRECISION_MULTIPLIER); - - private AmazonSyncHttpClient httpClient; - - @BeforeClass - public static void preConditions() { - TestPreConditions.assumeNotJava6(); - } - - @Test(timeout = TEST_TIMEOUT) - public void clientExecutionTimeoutDisabled_SocketTimeoutExceptionIsThrown_NoThreadsCreated() { - httpClient = HttpTestUtils.testClientBuilder().httpClient(createClientWithSocketTimeout(SHORTER_SOCKET_TIMEOUT)).build(); - - try { - httpClient.requestExecutionBuilder().request(newGetRequest()).execute(); - fail("Exception expected"); - } catch (SdkClientException e) { - assertThat(e.getCause(), instanceOf(SocketTimeoutException.class)); - assertClientExecutionTimerExecutorNotCreated(httpClient.getClientExecutionTimer()); - } - } - - /** - * The client execution timer uses interrupts to abort the client but if another thread - * interrupts the current thread for another reason we don't want to squash the - * {@link InterruptedException}. We should set the thread's interrupted status and throw the - * exception back out (we can't throw the actual {@link InterruptedException} because it's - * checked) - */ - @Test(timeout = TEST_TIMEOUT) - public void interruptCausedBySomethingOtherThanTimer_PropagatesInterruptToCaller() { - Duration socketTimeout = Duration.ofMillis(100); - - RetryPolicy retryPolicy = RetryPolicy.builder() - .backoffStrategy(new FixedTimeBackoffStrategy(CLIENT_EXECUTION_TIMEOUT)) - .numRetries(1) - .build(); - - httpClient = new AmazonSyncHttpClient(HttpTestUtils.testClientConfiguration().toBuilder() - .option(SdkClientOption.SYNC_HTTP_CLIENT, - HttpTestUtils.testSdkHttpClient()) - .option(SdkClientOption.RETRY_POLICY, retryPolicy) - .build()); - - // We make sure the first connection has failed due to the socket timeout before - // interrupting so we know that we are sleeping per the backoff strategy. Apache HTTP - // client doesn't seem to honor interrupts reliably but Thread.sleep does - interruptCurrentThreadAfterDelay(socketTimeout.toMillis() * 2); - - try { - httpClient.requestExecutionBuilder().request(newGetRequest()).execute(); - fail("Exception expected"); - } catch (SdkClientException e) { - assertTrue(Thread.currentThread().isInterrupted()); - assertThat(e.getCause(), instanceOf(InterruptedException.class)); - } - } - - @Test(timeout = TEST_TIMEOUT) - public void clientExecutionTimeoutEnabled_WithLongerSocketTimeout_ThrowsClientExecutionTimeoutException() - throws IOException { - httpClient = HttpTestUtils.testClientBuilder().httpClient(createClientWithSocketTimeout(LONGER_SOCKET_TIMEOUT)).build(); - - try { - httpClient.requestExecutionBuilder().request(newGetRequest()).execute(); - fail("Exception expected"); - } catch (SdkClientException e) { - assertThat(e, instanceOf(ClientExecutionTimeoutException.class)); - assertNumberOfTasksTriggered(httpClient.getClientExecutionTimer(), 1); - } - } - - private SdkHttpClient createClientWithSocketTimeout(Duration socketTimeout) { - return ApacheHttpClient.builder() - .socketTimeout(socketTimeout) - .build(); - } - - @Test(timeout = TEST_TIMEOUT) - public void clientExecutionTimeoutEnabled_WithShorterSocketTimeout_ThrowsSocketTimeoutException() - throws IOException { - httpClient = HttpTestUtils.testClientBuilder().httpClient(createClientWithSocketTimeout(SHORTER_SOCKET_TIMEOUT)).build(); - - try { - httpClient.requestExecutionBuilder().request(newGetRequest()).execute(); - fail("Exception expected"); - } catch (SdkClientException e) { - assertThat(e.getCause(), instanceOf(SocketTimeoutException.class)); - assertNumberOfTasksTriggered(httpClient.getClientExecutionTimer(), 0); - } - } - -} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/RequestOverrideConfiguration.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/RequestOverrideConfiguration.java index c0ccd9bd3654..afa010f0cc58 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/RequestOverrideConfiguration.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/RequestOverrideConfiguration.java @@ -15,11 +15,13 @@ package software.amazon.awssdk.core; +import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.TreeMap; import java.util.function.Consumer; import software.amazon.awssdk.annotations.Immutable; @@ -39,11 +41,15 @@ public abstract class RequestOverrideConfiguration { private final Map> rawQueryParameters; private final List apiNames; + private final Duration apiCallTimeout; + private final Duration apiCallAttemptTimeout; protected RequestOverrideConfiguration(Builder builder) { this.headers = CollectionUtils.deepUnmodifiableMap(builder.headers(), () -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER)); this.rawQueryParameters = CollectionUtils.deepUnmodifiableMap(builder.rawQueryParameters()); this.apiNames = Collections.unmodifiableList(new ArrayList<>(builder.apiNames())); + this.apiCallTimeout = Validate.isPositiveOrNull(builder.apiCallTimeout(), "apiCallTimeout"); + this.apiCallAttemptTimeout = Validate.isPositiveOrNull(builder.apiCallAttemptTimeout(), "apiCallAttemptTimeout"); } /** @@ -73,6 +79,44 @@ public List apiNames() { return apiNames; } + /** + * The amount of time to allow the client to complete the execution of an API call. This timeout covers the entire client + * execution except for marshalling. This includes request handler execution, all HTTP requests including retries, + * unmarshalling, etc. This value should always be positive, if present. + * + *

The api call timeout feature doesn't have strict guarantees on how quickly a request is aborted when the + * timeout is breached. The typical case aborts the request within a few milliseconds but there may occasionally be + * requests that don't get aborted until several seconds after the timer has been breached. Because of this, the client + * execution timeout feature should not be used when absolute precision is needed. + * + *

This may be used together with {@link #apiCallAttemptTimeout()} to enforce both a timeout on each individual HTTP + * request (i.e. each retry) and the total time spent on all requests across retries (i.e. the 'api call' time). + * + * @see Builder#apiCallTimeout(Duration) + */ + public Optional apiCallTimeout() { + return Optional.ofNullable(apiCallTimeout); + } + + /** + * The amount of time to wait for the http request to complete before giving up and timing out. This value should always be + * positive, if present. + * + *

The request timeout feature doesn't have strict guarantees on how quickly a request is aborted when the timeout is + * breached. The typical case aborts the request within a few milliseconds but there may occasionally be requests that + * don't get aborted until several seconds after the timer has been breached. Because of this, the request timeout + * feature should not be used when absolute precision is needed. + * + *

This may be used together with {@link #apiCallTimeout()} to enforce both a timeout on each individual HTTP + * request + * (i.e. each retry) and the total time spent on all requests across retries (i.e. the 'api call' time). + * + * @see Builder#apiCallAttemptTimeout(Duration) + */ + public Optional apiCallAttemptTimeout() { + return Optional.ofNullable(apiCallAttemptTimeout); + } + /** * Create a {@link Builder} initialized with the properties of this {@code SdkRequestOverrideConfiguration}. * @@ -196,6 +240,43 @@ default B putRawQueryParameter(String name, String value) { */ B addApiName(Consumer apiNameConsumer); + /** + * Configure the amount of time to allow the client to complete the execution of an API call. This timeout covers the + * entire client execution except for marshalling. This includes request handler execution, all HTTP requests including + * retries, unmarshalling, etc. This value should always be positive, if present. + * + *

The api call timeout feature doesn't have strict guarantees on how quickly a request is aborted when the + * timeout is breached. The typical case aborts the request within a few milliseconds but there may occasionally be + * requests that don't get aborted until several seconds after the timer has been breached. Because of this, the client + * execution timeout feature should not be used when absolute precision is needed. + * + *

This may be used together with {@link #apiCallAttemptTimeout()} to enforce both a timeout on each individual HTTP + * request (i.e. each retry) and the total time spent on all requests across retries (i.e. the 'api call' time). + * + * @see RequestOverrideConfiguration#apiCallTimeout() + */ + B apiCallTimeout(Duration apiCallTimeout); + + Duration apiCallTimeout(); + + /** + * Configure the amount of time to wait for the http request to complete before giving up and timing out. This value + * should always be positive, if present. + * + *

The request timeout feature doesn't have strict guarantees on how quickly a request is aborted when the timeout is + * breached. The typical case aborts the request within a few milliseconds but there may occasionally be requests that + * don't get aborted until several seconds after the timer has been breached. Because of this, the request timeout + * feature should not be used when absolute precision is needed. + * + *

This may be used together with {@link #apiCallTimeout()} to enforce both a timeout on each individual HTTP + * request (i.e. each retry) and the total time spent on all requests across retries (i.e. the 'api call' time). + * + * @see RequestOverrideConfiguration#apiCallAttemptTimeout() + */ + B apiCallAttemptTimeout(Duration apiCallAttemptTimeout); + + Duration apiCallAttemptTimeout(); + /** * Create a new {@code SdkRequestOverrideConfiguration} with the properties set on this builder. * @@ -210,6 +291,8 @@ protected abstract static class BuilderImpl implements Builde private Map> rawQueryParameters = new HashMap<>(); private List apiNames = new ArrayList<>(); + private Duration apiCallTimeout; + private Duration apiCallAttemptTimeout; protected BuilderImpl() { } @@ -281,5 +364,35 @@ public B addApiName(Consumer apiNameConsumer) { addApiName(b.build()); return (B) this; } + + @Override + public B apiCallTimeout(Duration apiCallTimeout) { + this.apiCallTimeout = apiCallTimeout; + return (B) this; + } + + public void setApiCallTimeout(Duration apiCallTimeout) { + apiCallTimeout(apiCallTimeout); + } + + @Override + public Duration apiCallTimeout() { + return apiCallTimeout; + } + + @Override + public B apiCallAttemptTimeout(Duration apiCallAttemptTimeout) { + this.apiCallAttemptTimeout = apiCallAttemptTimeout; + return (B) this; + } + + public void setApiCallAttemptTimeout(Duration apiCallAttemptTimeout) { + apiCallAttemptTimeout(apiCallAttemptTimeout); + } + + @Override + public Duration apiCallAttemptTimeout() { + return apiCallAttemptTimeout; + } } } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java index aa26b26c45bb..2e38d1870fdb 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java @@ -20,11 +20,13 @@ import static software.amazon.awssdk.core.client.config.SdkAdvancedClientOption.USER_AGENT_PREFIX; import static software.amazon.awssdk.core.client.config.SdkAdvancedClientOption.USER_AGENT_SUFFIX; import static software.amazon.awssdk.core.client.config.SdkClientOption.ADDITIONAL_HTTP_HEADERS; +import static software.amazon.awssdk.core.client.config.SdkClientOption.API_CALL_ATTEMPT_TIMEOUT; +import static software.amazon.awssdk.core.client.config.SdkClientOption.API_CALL_TIMEOUT; import static software.amazon.awssdk.core.client.config.SdkClientOption.ASYNC_HTTP_CLIENT; -import static software.amazon.awssdk.core.client.config.SdkClientOption.ASYNC_RETRY_EXECUTOR_SERVICE; import static software.amazon.awssdk.core.client.config.SdkClientOption.CRC32_FROM_COMPRESSED_DATA_ENABLED; import static software.amazon.awssdk.core.client.config.SdkClientOption.EXECUTION_INTERCEPTORS; import static software.amazon.awssdk.core.client.config.SdkClientOption.RETRY_POLICY; +import static software.amazon.awssdk.core.client.config.SdkClientOption.SCHEDULED_EXECUTOR_SERVICE; import static software.amazon.awssdk.utils.CollectionUtils.mergeLists; import static software.amazon.awssdk.utils.Validate.paramNotNull; @@ -216,7 +218,6 @@ private SdkClientConfiguration finalizeSyncConfiguration(SdkClientConfiguration private SdkClientConfiguration finalizeAsyncConfiguration(SdkClientConfiguration config) { return config.toBuilder() .option(FUTURE_COMPLETION_EXECUTOR, resolveAsyncFutureCompletionExecutor(config)) - .option(ASYNC_RETRY_EXECUTOR_SERVICE, resolveAsyncRetryExecutorService()) .option(ASYNC_HTTP_CLIENT, resolveAsyncHttpClient(config)) .build(); } @@ -226,6 +227,7 @@ private SdkClientConfiguration finalizeAsyncConfiguration(SdkClientConfiguration */ private SdkClientConfiguration finalizeConfiguration(SdkClientConfiguration config) { return config.toBuilder() + .option(SCHEDULED_EXECUTOR_SERVICE, resolveScheduledExecutorService()) .option(EXECUTION_INTERCEPTORS, resolveExecutionInterceptors(config)) .build(); } @@ -275,10 +277,11 @@ private Executor resolveAsyncFutureCompletionExecutor(SdkClientConfiguration con } /** - * Finalize which async executor service will be used for retries in the created client. + * Finalize which scheduled executor service will be used for retries in the created client. */ - private ScheduledExecutorService resolveAsyncRetryExecutorService() { - return Executors.newScheduledThreadPool(1, new ThreadFactoryBuilder().threadNamePrefix("sdk-retry").build()); + private ScheduledExecutorService resolveScheduledExecutorService() { + return Executors.newScheduledThreadPool(5, new ThreadFactoryBuilder() + .threadNamePrefix("sdk-ScheduledExecutor").build()); } /** @@ -316,6 +319,8 @@ public final B overrideConfiguration(ClientOverrideConfiguration overrideConfig) clientConfiguration.option(SIGNER, overrideConfig.advancedOption(SIGNER).orElse(null)); clientConfiguration.option(USER_AGENT_SUFFIX, overrideConfig.advancedOption(USER_AGENT_SUFFIX).orElse(null)); clientConfiguration.option(USER_AGENT_PREFIX, overrideConfig.advancedOption(USER_AGENT_PREFIX).orElse(null)); + clientConfiguration.option(API_CALL_TIMEOUT, overrideConfig.apiCallTimeout().orElse(null)); + clientConfiguration.option(API_CALL_ATTEMPT_TIMEOUT, overrideConfig.apiCallAttemptTimeout().orElse(null)); return thisBuilder(); } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/ClientOverrideConfiguration.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/ClientOverrideConfiguration.java index 6a929e240015..2f8ef00bbeba 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/ClientOverrideConfiguration.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/ClientOverrideConfiguration.java @@ -15,6 +15,7 @@ package software.amazon.awssdk.core.client.config; +import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -47,6 +48,8 @@ public final class ClientOverrideConfiguration private final RetryPolicy retryPolicy; private final List executionInterceptors; private final AttributeMap advancedOptions; + private final Duration apiCallAttemptTimeout; + private final Duration apiCallTimeout; /** * Initialize this configuration. Private to require use of {@link #builder()}. @@ -56,6 +59,8 @@ private ClientOverrideConfiguration(Builder builder) { this.retryPolicy = builder.retryPolicy(); this.executionInterceptors = Collections.unmodifiableList(new ArrayList<>(builder.executionInterceptors())); this.advancedOptions = builder.advancedOptions(); + this.apiCallTimeout = Validate.isPositiveOrNull(builder.apiCallTimeout(), "apiCallTimeout"); + this.apiCallAttemptTimeout = Validate.isPositiveOrNull(builder.apiCallAttemptTimeout(), "apiCallAttemptTimeout"); } @Override @@ -63,6 +68,8 @@ public Builder toBuilder() { return new DefaultClientOverrideConfigurationBuilder().advancedOptions(advancedOptions.toBuilder()) .headers(headers) .retryPolicy(retryPolicy) + .apiCallTimeout(apiCallTimeout) + .apiCallAttemptTimeout(apiCallAttemptTimeout) .executionInterceptors(executionInterceptors); } @@ -112,11 +119,51 @@ public List executionInterceptors() { return executionInterceptors; } + /** + * The amount of time to allow the client to complete the execution of an API call. This timeout covers the entire client + * execution except for marshalling. This includes request handler execution, all HTTP requests including retries, + * unmarshalling, etc. This value should always be positive, if present. + * + *

The api call timeout feature doesn't have strict guarantees on how quickly a request is aborted when the + * timeout is breached. The typical case aborts the request within a few milliseconds but there may occasionally be + * requests that don't get aborted until several seconds after the timer has been breached. Because of this, the client + * execution timeout feature should not be used when absolute precision is needed. + * + *

This may be used together with {@link #apiCallAttemptTimeout()} to enforce both a timeout on each individual HTTP + * request (i.e. each retry) and the total time spent on all requests across retries (i.e. the 'api call' time). + * + * @see Builder#apiCallTimeout(Duration) + */ + public Optional apiCallTimeout() { + return Optional.ofNullable(apiCallTimeout); + } + + /** + * The amount of time to wait for the http request to complete before giving up and timing out. This value should always be + * positive, if present. + * + *

The request timeout feature doesn't have strict guarantees on how quickly a request is aborted when the timeout is + * breached. The typical case aborts the request within a few milliseconds but there may occasionally be requests that + * don't get aborted until several seconds after the timer has been breached. Because of this, the request timeout + * feature should not be used when absolute precision is needed. + * + *

This may be used together with {@link #apiCallTimeout()} to enforce both a timeout on each individual HTTP + * request + * (i.e. each retry) and the total time spent on all requests across retries (i.e. the 'api call' time). + * + * @see Builder#apiCallAttemptTimeout(Duration) + */ + public Optional apiCallAttemptTimeout() { + return Optional.ofNullable(apiCallAttemptTimeout); + } + @Override public String toString() { return ToString.builder("ClientOverrideConfiguration") .add("headers", headers) .add("retryPolicy", retryPolicy) + .add("apiCallTimeout", apiCallTimeout) + .add("apiCallAttemptTimeout", apiCallAttemptTimeout) .add("executionInterceptors", executionInterceptors) .add("advancedOptions", advancedOptions) .build(); @@ -241,6 +288,43 @@ default Builder retryPolicy(Consumer retryPolicy) { Builder advancedOptions(Map, ?> advancedOptions); AttributeMap advancedOptions(); + + /** + * Configure the amount of time to allow the client to complete the execution of an API call. This timeout covers the + * entire client execution except for marshalling. This includes request handler execution, all HTTP requests including + * retries, unmarshalling, etc. This value should always be positive, if present. + * + *

The api call timeout feature doesn't have strict guarantees on how quickly a request is aborted when the + * timeout is breached. The typical case aborts the request within a few milliseconds but there may occasionally be + * requests that don't get aborted until several seconds after the timer has been breached. Because of this, the client + * execution timeout feature should not be used when absolute precision is needed. + * + *

This may be used together with {@link #apiCallAttemptTimeout()} to enforce both a timeout on each individual HTTP + * request (i.e. each retry) and the total time spent on all requests across retries (i.e. the 'api call' time). + * + * @see ClientOverrideConfiguration#apiCallTimeout() + */ + Builder apiCallTimeout(Duration apiCallTimeout); + + Duration apiCallTimeout(); + + /** + * Configure the amount of time to wait for the http request to complete before giving up and timing out. This value + * should always be positive, if present. + * + *

The request timeout feature doesn't have strict guarantees on how quickly a request is aborted when the timeout is + * breached. The typical case aborts the request within a few milliseconds but there may occasionally be requests that + * don't get aborted until several seconds after the timer has been breached. Because of this, the request timeout + * feature should not be used when absolute precision is needed. + * + *

This may be used together with {@link #apiCallTimeout()} to enforce both a timeout on each individual HTTP + * request (i.e. each retry) and the total time spent on all requests across retries (i.e. the 'api call' time). + * + * @see ClientOverrideConfiguration#apiCallAttemptTimeout() + */ + Builder apiCallAttemptTimeout(Duration apiCallAttemptTimeout); + + Duration apiCallAttemptTimeout(); } /** @@ -251,6 +335,8 @@ private static final class DefaultClientOverrideConfigurationBuilder implements private RetryPolicy retryPolicy; private List executionInterceptors = new ArrayList<>(); private AttributeMap.Builder advancedOptions = AttributeMap.builder(); + private Duration apiCallTimeout; + private Duration apiCallAttemptTimeout; @Override public Builder headers(Map> headers) { @@ -340,6 +426,36 @@ public AttributeMap advancedOptions() { return advancedOptions.build(); } + @Override + public Builder apiCallTimeout(Duration apiCallTimeout) { + this.apiCallTimeout = apiCallTimeout; + return this; + } + + public void setApiCallTimeout(Duration apiCallTimeout) { + apiCallTimeout(apiCallTimeout); + } + + @Override + public Duration apiCallTimeout() { + return apiCallTimeout; + } + + @Override + public Builder apiCallAttemptTimeout(Duration apiCallAttemptTimeout) { + this.apiCallAttemptTimeout = apiCallAttemptTimeout; + return this; + } + + public void setApiCallAttemptTimeout(Duration apiCallAttemptTimeout) { + apiCallAttemptTimeout(apiCallAttemptTimeout); + } + + @Override + public Duration apiCallAttemptTimeout() { + return apiCallAttemptTimeout; + } + @Override public ClientOverrideConfiguration build() { return new ClientOverrideConfiguration(this); diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java index 04c0b75798ba..5ca2bb7c654a 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.core.client.config; import java.net.URI; +import java.time.Duration; import java.util.List; import java.util.Map; import java.util.concurrent.ScheduledExecutorService; @@ -70,9 +71,10 @@ public final class SdkClientOption extends ClientOption { new SdkClientOption<>(Boolean.class); /** - * The executor used for scheduling async retry attempts. + * The internal SDK scheduled executor service that is used for scheduling tasks such as async retry attempts + * and timeout task. */ - public static final SdkClientOption ASYNC_RETRY_EXECUTOR_SERVICE = + public static final SdkClientOption SCHEDULED_EXECUTOR_SERVICE = new SdkClientOption<>(ScheduledExecutorService.class); /** @@ -87,6 +89,16 @@ public final class SdkClientOption extends ClientOption { public static final SdkClientOption SYNC_HTTP_CLIENT = new SdkClientOption<>(SdkHttpClient.class); + /** + * @see ClientOverrideConfiguration#apiCallAttemptTimeout() + */ + public static final SdkClientOption API_CALL_ATTEMPT_TIMEOUT = new SdkClientOption<>(Duration.class); + + /** + * @see ClientOverrideConfiguration#apiCallTimeout() + */ + public static final SdkClientOption API_CALL_TIMEOUT = new SdkClientOption<>(Duration.class); + private SdkClientOption(Class valueClass) { super(valueClass); } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/exception/ApiCallAttemptTimeoutException.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/exception/ApiCallAttemptTimeoutException.java new file mode 100644 index 000000000000..d4685aab4bac --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/exception/ApiCallAttemptTimeoutException.java @@ -0,0 +1,90 @@ +/* + * Copyright 2010-2018 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.core.exception; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; + +/** + * Signals that an api call attempt could not complete within the specified timeout. + * + * @see ClientOverrideConfiguration#apiCallAttemptTimeout() + */ +@SdkPublicApi +public final class ApiCallAttemptTimeoutException extends SdkClientException { + + private static final long serialVersionUID = 1L; + + private ApiCallAttemptTimeoutException(Builder b) { + super(b); + } + + public static ApiCallAttemptTimeoutException create(long timeout) { + return builder().message(String.format("HTTP request execution did not complete before the specified timeout " + + "configuration: %s millis", timeout)) + .build(); + } + + public static ApiCallAttemptTimeoutException create(String message, Throwable cause) { + return builder().message(message).cause(cause).build(); + } + + @Override + public Builder toBuilder() { + return new BuilderImpl(this); + } + + public static Builder builder() { + return new BuilderImpl(); + } + + public interface Builder extends SdkClientException.Builder { + @Override + Builder message(String message); + + @Override + Builder cause(Throwable cause); + + @Override + ApiCallAttemptTimeoutException build(); + } + + protected static final class BuilderImpl extends SdkClientException.BuilderImpl implements Builder { + + protected BuilderImpl() {} + + protected BuilderImpl(ApiCallAttemptTimeoutException ex) { + super(ex); + } + + @Override + public Builder message(String message) { + this.message = message; + return this; + } + + @Override + public Builder cause(Throwable cause) { + this.cause = cause; + return this; + } + + @Override + public ApiCallAttemptTimeoutException build() { + return new ApiCallAttemptTimeoutException(this); + } + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/exception/ClientExecutionTimeoutException.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/exception/ApiCallTimeoutException.java similarity index 62% rename from core/sdk-core/src/main/java/software/amazon/awssdk/core/exception/ClientExecutionTimeoutException.java rename to core/sdk-core/src/main/java/software/amazon/awssdk/core/exception/ApiCallTimeoutException.java index fa194a50b359..795ceb05dd08 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/exception/ClientExecutionTimeoutException.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/exception/ApiCallTimeoutException.java @@ -16,17 +16,29 @@ package software.amazon.awssdk.core.exception; import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +/** + * Signals that an api call could not complete within the specified timeout. + * + * @see ClientOverrideConfiguration#apiCallTimeout() + */ @SdkPublicApi -public class ClientExecutionTimeoutException extends SdkClientException { +public final class ApiCallTimeoutException extends SdkClientException { - private static final long serialVersionUID = 4861767589924758934L; + private static final long serialVersionUID = 1L; - protected ClientExecutionTimeoutException(Builder b) { + private ApiCallTimeoutException(Builder b) { super(b); } - public static ClientExecutionTimeoutException create(String message, Throwable cause) { + public static ApiCallTimeoutException create(long timeout) { + return builder().message(String.format("Client execution did not complete before the specified timeout configuration: " + + "%s millis", timeout)) + .build(); + } + + public static ApiCallTimeoutException create(String message, Throwable cause) { return builder().message(message).cause(cause).build(); } @@ -47,14 +59,14 @@ public interface Builder extends SdkClientException.Builder { Builder cause(Throwable cause); @Override - ClientExecutionTimeoutException build(); + ApiCallTimeoutException build(); } protected static final class BuilderImpl extends SdkClientException.BuilderImpl implements Builder { protected BuilderImpl() {} - protected BuilderImpl(ClientExecutionTimeoutException ex) { + protected BuilderImpl(ApiCallTimeoutException ex) { super(ex); } @@ -71,8 +83,8 @@ public Builder cause(Throwable cause) { } @Override - public ClientExecutionTimeoutException build() { - return new ClientExecutionTimeoutException(this); + public ApiCallTimeoutException build() { + return new ApiCallTimeoutException(this); } } } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/RequestExecutionContext.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/RequestExecutionContext.java deleted file mode 100644 index 8220629c98bf..000000000000 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/RequestExecutionContext.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright 2010-2018 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.core.internal; - -import java.util.Optional; -import software.amazon.awssdk.annotations.ReviewBeforeRelease; -import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.core.RequestOverrideConfiguration; -import software.amazon.awssdk.core.SdkRequest; -import software.amazon.awssdk.core.SdkRequestOverrideConfiguration; -import software.amazon.awssdk.core.http.ExecutionContext; -import software.amazon.awssdk.core.interceptor.ExecutionAttributes; -import software.amazon.awssdk.core.internal.http.AmazonAsyncHttpClient; -import software.amazon.awssdk.core.internal.http.AmazonSyncHttpClient; -import software.amazon.awssdk.core.internal.http.pipeline.RequestPipeline; -import software.amazon.awssdk.core.internal.http.timers.client.ClientExecutionAbortTrackerTask; -import software.amazon.awssdk.core.internal.http.timers.client.ClientExecutionTimer; -import software.amazon.awssdk.core.internal.interceptor.ExecutionInterceptorChain; -import software.amazon.awssdk.core.signer.Signer; -import software.amazon.awssdk.http.async.SdkHttpRequestProvider; -import software.amazon.awssdk.utils.Validate; - -/** - * Request scoped dependencies and context for an execution of a request by {@link AmazonSyncHttpClient} or - * {@link AmazonAsyncHttpClient}. - * Provided to the {@link RequestPipeline#execute(Object, software.amazon.awssdk.core.internal.http.RequestExecutionContext)} - * method. - */ -@SdkInternalApi -public final class RequestExecutionContext { - private static final RequestOverrideConfiguration EMPTY_CONFIG = SdkRequestOverrideConfiguration.builder().build(); - private final SdkHttpRequestProvider requestProvider; - private final SdkRequest originalRequest; - private final ExecutionContext executionContext; - - private ClientExecutionAbortTrackerTask clientExecutionTrackerTask; - - private RequestExecutionContext(Builder builder) { - this.requestProvider = builder.requestProvider; - this.originalRequest = Validate.paramNotNull(builder.originalRequest, "originalRequest"); - this.executionContext = Validate.paramNotNull(builder.executionContext, "executionContext"); - } - - /** - * Create a {@link Builder}, used to create a {@link RequestExecutionContext}. - */ - public static Builder builder() { - return new Builder(); - } - - public Optional requestProvider() { - return Optional.ofNullable(requestProvider); - } - - /** - * @return Execution interceptors to hook into execution lifecycle. - */ - public ExecutionInterceptorChain interceptorChain() { - return executionContext.interceptorChain(); - } - - public ExecutionAttributes executionAttributes() { - return executionContext.executionAttributes(); - } - - @ReviewBeforeRelease("We should combine RequestExecutionContext and ExecutionContext. There's no benefit to both of " - + "these. Once that's done, this won't be needed.") - public ExecutionContext executionContext() { - return executionContext; - } - - public SdkRequest originalRequest() { - return originalRequest; - } - - public RequestOverrideConfiguration requestConfig() { - return originalRequest.overrideConfiguration() - // ugly but needed to avoid capture of capture and creating a type mismatch - .map(c -> (RequestOverrideConfiguration) c) - .orElse(EMPTY_CONFIG); - } - - /** - * @return SignerProvider used to obtain an instance of a {@link Signer}. - */ - public Signer signer() { - return executionContext.signer(); - } - - /** - * @return Tracker task for the {@link ClientExecutionTimer}. - */ - public ClientExecutionAbortTrackerTask clientExecutionTrackerTask() { - return clientExecutionTrackerTask; - } - - /** - * Sets the tracker task for the {@link ClientExecutionTimer}. Should - * be called once per request lifecycle. - */ - public void clientExecutionTrackerTask(ClientExecutionAbortTrackerTask clientExecutionTrackerTask) { - this.clientExecutionTrackerTask = clientExecutionTrackerTask; - } - - /** - * An SDK-internal implementation of {@link Builder}. - */ - public static final class Builder { - - private SdkHttpRequestProvider requestProvider; - private SdkRequest originalRequest; - private ExecutionContext executionContext; - - public Builder requestProvider(SdkHttpRequestProvider requestProvider) { - this.requestProvider = requestProvider; - return this; - } - - public Builder originalRequest(SdkRequest originalRequest) { - this.originalRequest = originalRequest; - return this; - } - - public Builder executionContext(ExecutionContext executionContext) { - this.executionContext = executionContext; - return this; - } - - public RequestExecutionContext build() { - return new RequestExecutionContext(this); - } - } -} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/AmazonAsyncHttpClient.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/AmazonAsyncHttpClient.java index 4144d40a1080..5441823e3765 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/AmazonAsyncHttpClient.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/AmazonAsyncHttpClient.java @@ -41,7 +41,6 @@ import software.amazon.awssdk.core.internal.http.pipeline.stages.MoveParametersToBodyStage; import software.amazon.awssdk.core.internal.http.pipeline.stages.SigningStage; import software.amazon.awssdk.core.internal.http.pipeline.stages.UnwrapResponseContainer; -import software.amazon.awssdk.core.internal.http.timers.client.ClientExecutionTimer; import software.amazon.awssdk.core.internal.retry.SdkDefaultRetrySetting; import software.amazon.awssdk.core.internal.util.CapacityManager; import software.amazon.awssdk.http.SdkHttpFullRequest; @@ -57,7 +56,6 @@ public final class AmazonAsyncHttpClient implements SdkAutoCloseable { public AmazonAsyncHttpClient(SdkClientConfiguration clientConfiguration) { this.httpClientDependencies = HttpClientDependencies.builder() - .clientExecutionTimer(new ClientExecutionTimer()) .clientConfiguration(clientConfiguration) .capacityManager(createCapacityManager()) .build(); diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/AmazonSyncHttpClient.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/AmazonSyncHttpClient.java index d45d1985ff1d..d12cf06c82b7 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/AmazonSyncHttpClient.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/AmazonSyncHttpClient.java @@ -17,7 +17,6 @@ import software.amazon.awssdk.annotations.ReviewBeforeRelease; import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.annotations.SdkTestInternalApi; import software.amazon.awssdk.annotations.ThreadSafe; import software.amazon.awssdk.core.Request; import software.amazon.awssdk.core.SdkRequest; @@ -47,7 +46,6 @@ import software.amazon.awssdk.core.internal.http.pipeline.stages.RetryableStage; import software.amazon.awssdk.core.internal.http.pipeline.stages.SigningStage; import software.amazon.awssdk.core.internal.http.pipeline.stages.UnwrapResponseContainer; -import software.amazon.awssdk.core.internal.http.timers.client.ClientExecutionTimer; import software.amazon.awssdk.core.internal.retry.SdkDefaultRetrySetting; import software.amazon.awssdk.core.internal.util.CapacityManager; import software.amazon.awssdk.http.SdkHttpFullRequest; @@ -69,7 +67,6 @@ public final class AmazonSyncHttpClient implements SdkAutoCloseable { public AmazonSyncHttpClient(SdkClientConfiguration clientConfiguration) { this.httpClientDependencies = HttpClientDependencies.builder() .clientConfiguration(clientConfiguration) - .clientExecutionTimer(new ClientExecutionTimer()) .capacityManager(createCapacityManager()) .build(); } @@ -102,14 +99,6 @@ public void close() { httpClientDependencies.close(); } - /** - * For unit testing only. - */ - @SdkTestInternalApi - public ClientExecutionTimer getClientExecutionTimer() { - return this.httpClientDependencies.clientExecutionTimer(); - } - /** * Ensures the response handler is not null. If it is this method returns a dummy response * handler. diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/HttpClientDependencies.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/HttpClientDependencies.java index b4d4c08f8d33..d268c57956aa 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/HttpClientDependencies.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/HttpClientDependencies.java @@ -22,19 +22,17 @@ import software.amazon.awssdk.core.internal.client.config.SdkClientConfiguration; import software.amazon.awssdk.core.internal.http.pipeline.RequestPipeline; import software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder; -import software.amazon.awssdk.core.internal.http.timers.client.ClientExecutionTimer; import software.amazon.awssdk.core.internal.util.CapacityManager; import software.amazon.awssdk.utils.SdkAutoCloseable; /** - * Client scoped dependencies of {@link AmazonSyncHttpClient}. May be injected into constructors of {@link - * RequestPipeline} implementations by {@link RequestPipelineBuilder}. + * Client scoped dependencies of {@link AmazonSyncHttpClient} and {@link AmazonAsyncHttpClient}. + * May be injected into constructors of {@link RequestPipeline} implementations by {@link RequestPipelineBuilder}. */ @SdkInternalApi public final class HttpClientDependencies implements SdkAutoCloseable { private final SdkClientConfiguration clientConfiguration; private final CapacityManager capacityManager; - private final ClientExecutionTimer clientExecutionTimer; /** * Time offset may be mutated by {@link RequestPipeline} implementations if a clock skew is detected. @@ -44,7 +42,6 @@ public final class HttpClientDependencies implements SdkAutoCloseable { private HttpClientDependencies(Builder builder) { this.clientConfiguration = paramNotNull(builder.clientConfiguration, "ClientConfiguration"); this.capacityManager = paramNotNull(builder.capacityManager, "CapacityManager"); - this.clientExecutionTimer = paramNotNull(builder.clientExecutionTimer, "ClientExecutionTimer"); } public static Builder builder() { @@ -62,13 +59,6 @@ public CapacityManager retryCapacity() { return capacityManager; } - /** - * @return Controller for the ClientExecution timeout feature. - */ - public ClientExecutionTimer clientExecutionTimer() { - return clientExecutionTimer; - } - /** * @return Current time offset. This is mutable and should not be cached. */ @@ -88,7 +78,6 @@ public void updateTimeOffset(int timeOffset) { @Override public void close() { this.clientConfiguration.close(); - this.clientExecutionTimer.close(); } /** @@ -97,7 +86,6 @@ public void close() { public static class Builder { private SdkClientConfiguration clientConfiguration; private CapacityManager capacityManager; - private ClientExecutionTimer clientExecutionTimer; private Builder() {} @@ -111,11 +99,6 @@ public Builder capacityManager(CapacityManager capacityManager) { return this; } - public Builder clientExecutionTimer(ClientExecutionTimer clientExecutionTimer) { - this.clientExecutionTimer = clientExecutionTimer; - return this; - } - public HttpClientDependencies build() { return new HttpClientDependencies(this); } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/RequestExecutionContext.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/RequestExecutionContext.java index 35caa7bc80af..90ec89e2ab29 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/RequestExecutionContext.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/RequestExecutionContext.java @@ -23,8 +23,7 @@ import software.amazon.awssdk.core.http.ExecutionContext; import software.amazon.awssdk.core.interceptor.ExecutionAttributes; import software.amazon.awssdk.core.internal.http.pipeline.RequestPipeline; -import software.amazon.awssdk.core.internal.http.timers.client.ClientExecutionAbortTrackerTask; -import software.amazon.awssdk.core.internal.http.timers.client.ClientExecutionTimer; +import software.amazon.awssdk.core.internal.http.timers.TimeoutTracker; import software.amazon.awssdk.core.internal.interceptor.ExecutionInterceptorChain; import software.amazon.awssdk.core.signer.Signer; import software.amazon.awssdk.http.async.SdkHttpRequestProvider; @@ -41,8 +40,7 @@ public final class RequestExecutionContext { private final SdkHttpRequestProvider requestProvider; private final SdkRequest originalRequest; private final ExecutionContext executionContext; - - private ClientExecutionAbortTrackerTask clientExecutionTrackerTask; + private TimeoutTracker apiCallTimeoutTracker; private RequestExecutionContext(Builder builder) { this.requestProvider = builder.requestProvider; @@ -97,20 +95,21 @@ public Signer signer() { } /** - * @return Tracker task for the {@link ClientExecutionTimer}. + * @return Tracker task for the {@link TimeoutTracker}. */ - public ClientExecutionAbortTrackerTask clientExecutionTrackerTask() { - return clientExecutionTrackerTask; + public TimeoutTracker apiCallTimeoutTracker() { + return apiCallTimeoutTracker; } /** - * Sets the tracker task for the {@link ClientExecutionTimer}. Should + * Sets the tracker task for the . Should * be called once per request lifecycle. */ - public void clientExecutionTrackerTask(ClientExecutionAbortTrackerTask clientExecutionTrackerTask) { - this.clientExecutionTrackerTask = clientExecutionTrackerTask; + public void apiCallTimeoutTracker(TimeoutTracker timeoutTracker) { + this.apiCallTimeoutTracker = timeoutTracker; } + /** * An SDK-internal implementation of {@link Builder}. */ diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/AsyncRetryableStage.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/AsyncRetryableStage.java index bb1bab677254..c9d1ade62443 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/AsyncRetryableStage.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/AsyncRetryableStage.java @@ -15,6 +15,8 @@ package software.amazon.awssdk.core.internal.http.pipeline.stages; +import static software.amazon.awssdk.core.internal.http.timers.TimerUtils.timeCompletableFuture; + import java.io.IOException; import java.io.InputStream; import java.time.Duration; @@ -26,15 +28,19 @@ import software.amazon.awssdk.annotations.ReviewBeforeRelease; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.core.RequestOption; +import software.amazon.awssdk.core.RequestOverrideConfiguration; import software.amazon.awssdk.core.SdkStandardLogger; import software.amazon.awssdk.core.client.config.SdkClientOption; +import software.amazon.awssdk.core.exception.ApiCallTimeoutException; import software.amazon.awssdk.core.exception.ResetException; import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.core.exception.SdkException; import software.amazon.awssdk.core.internal.Response; +import software.amazon.awssdk.core.internal.client.config.SdkClientConfiguration; import software.amazon.awssdk.core.internal.http.HttpClientDependencies; import software.amazon.awssdk.core.internal.http.RequestExecutionContext; import software.amazon.awssdk.core.internal.http.pipeline.RequestPipeline; +import software.amazon.awssdk.core.internal.http.timers.TimeoutTracker; import software.amazon.awssdk.core.internal.retry.RetryHandler; import software.amazon.awssdk.core.internal.util.CapacityManager; import software.amazon.awssdk.core.internal.util.ClockSkewUtil; @@ -42,6 +48,7 @@ import software.amazon.awssdk.core.retry.RetryUtils; import software.amazon.awssdk.http.SdkHttpFullRequest; import software.amazon.awssdk.http.async.SdkHttpResponseHandler; +import software.amazon.awssdk.utils.OptionalUtils; /** * Wrapper around the pipeline for a single request to provide retry functionality. @@ -53,21 +60,23 @@ public final class AsyncRetryableStage implements RequestPipeline>> requestPipeline; - private final ScheduledExecutorService retrySubmitter; + private final ScheduledExecutorService scheduledExecutor; private final SdkHttpResponseHandler responseHandler; private final HttpClientDependencies dependencies; private final CapacityManager retryCapacity; private final RetryPolicy retryPolicy; + private final SdkClientConfiguration clientConfig; public AsyncRetryableStage(SdkHttpResponseHandler responseHandler, HttpClientDependencies dependencies, RequestPipeline>> requestPipeline) { this.responseHandler = responseHandler; this.dependencies = dependencies; - this.retrySubmitter = dependencies.clientConfiguration().option(SdkClientOption.ASYNC_RETRY_EXECUTOR_SERVICE); + this.scheduledExecutor = dependencies.clientConfiguration().option(SdkClientOption.SCHEDULED_EXECUTOR_SERVICE); this.retryPolicy = dependencies.clientConfiguration().option(SdkClientOption.RETRY_POLICY); this.retryCapacity = dependencies.retryCapacity(); this.requestPipeline = requestPipeline; + this.clientConfig = dependencies.clientConfiguration(); } public CompletableFuture> execute(SdkHttpFullRequest request, RequestExecutionContext context) throws @@ -112,6 +121,14 @@ private RetryExecutor(SdkHttpFullRequest request, RequestExecutionContext contex public CompletableFuture> execute() throws Exception { CompletableFuture> future = new CompletableFuture<>(); + + long apiCallTimeoutInMillis = getApiCallTimeoutInMillis(context.requestConfig()); + TimeoutTracker timeoutTracker = timeCompletableFuture(future, + scheduledExecutor, + ApiCallTimeoutException.create(apiCallTimeoutInMillis), + apiCallTimeoutInMillis); + context.apiCallTimeoutTracker(timeoutTracker); + execute(future); return future; } @@ -172,7 +189,7 @@ private void executeRetry(CompletableFuture> future) { SdkStandardLogger.REQUEST_LOGGER.debug(() -> "Retryable error detected, will retry in " + delay.toMillis() + "ms," + " attempt number " + retriesAttempted); - retrySubmitter.schedule(() -> { + scheduledExecutor.schedule(() -> { execute(future); return null; }, delay.toMillis(), TimeUnit.MILLISECONDS); @@ -231,4 +248,13 @@ private int readLimit() { return RequestOption.DEFAULT_STREAM_BUFFER_SIZE; } } + + private long getApiCallTimeoutInMillis(RequestOverrideConfiguration requestConfig) { + return OptionalUtils.firstPresent( + requestConfig.apiCallTimeout(), + () -> clientConfig.option(SdkClientOption.API_CALL_TIMEOUT)) + .map(Duration::toMillis) + .orElse(0L); + + } } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ClientExecutionTimedStage.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ClientExecutionTimedStage.java deleted file mode 100644 index 733a1ac36579..000000000000 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ClientExecutionTimedStage.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright 2010-2018 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.core.internal.http.pipeline.stages; - -import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely; - -import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.core.RequestOverrideConfiguration; -import software.amazon.awssdk.core.SdkRequest; -import software.amazon.awssdk.core.exception.AbortedException; -import software.amazon.awssdk.core.exception.ClientExecutionTimeoutException; -import software.amazon.awssdk.core.exception.SdkClientException; -import software.amazon.awssdk.core.exception.SdkInterruptedException; -import software.amazon.awssdk.core.internal.Response; -import software.amazon.awssdk.core.internal.client.config.SdkClientConfiguration; -import software.amazon.awssdk.core.internal.http.HttpClientDependencies; -import software.amazon.awssdk.core.internal.http.RequestExecutionContext; -import software.amazon.awssdk.core.internal.http.pipeline.RequestPipeline; -import software.amazon.awssdk.core.internal.http.pipeline.RequestToResponsePipeline; -import software.amazon.awssdk.core.internal.http.timers.client.ClientExecutionAbortTrackerTask; -import software.amazon.awssdk.core.internal.http.timers.client.ClientExecutionTimer; -import software.amazon.awssdk.http.SdkHttpFullRequest; - -/** - * Wrapper around a {@link RequestPipeline} to manage the client execution timeout feature. - */ -@SdkInternalApi -public class ClientExecutionTimedStage implements RequestToResponsePipeline { - - private final RequestPipeline> wrapped; - private final ClientExecutionTimer clientExecutionTimer; - - public ClientExecutionTimedStage(HttpClientDependencies dependencies, - RequestPipeline> wrapped) { - this.wrapped = wrapped; - this.clientExecutionTimer = dependencies.clientExecutionTimer(); - } - - @Override - public Response execute(SdkHttpFullRequest request, RequestExecutionContext context) throws Exception { - try { - return executeWithTimer(request, context); - } catch (Exception e) { - throw translatePipelineException(context, e); - } - } - - /** - * Start and end client execution timer around the execution of the request. It's important - * that the client execution task is canceled before the InterruptedException is handled by - * {@link #wrapped#execute(SdkHttpFullRequest)} so the interrupt status doesn't leak out to the callers code - */ - private Response executeWithTimer(SdkHttpFullRequest request, RequestExecutionContext context) throws Exception { - ClientExecutionAbortTrackerTask task = - clientExecutionTimer.startTimer(getClientExecutionTimeoutInMillis(context.requestConfig())); - try { - context.clientExecutionTrackerTask(task); - return wrapped.execute(request, context); - } finally { - context.clientExecutionTrackerTask().cancelTask(); - } - } - - /** - * Take the given exception thrown from the wrapped pipeline and return a more appropriate - * timeout related exception based on its type and the the execution status. - * - * @param context The execution context. - * @param e The exception thrown from the inner pipeline. - * @return The translated exception. - */ - private Exception translatePipelineException(RequestExecutionContext context, Exception e) { - if (e instanceof InterruptedException) { - return handleInterruptedException(context, (InterruptedException) e); - } - - // InterruptedException was not rethrown and instead the interrupted flag was set - if (Thread.currentThread().isInterrupted() && context.clientExecutionTrackerTask().hasTimeoutExpired()) { - Thread.interrupted(); - return ClientExecutionTimeoutException.builder().build(); - } - - return e; - } - - /** - * Determine if an interrupted exception is caused by the client execution timer - * interrupting the current thread or some other task interrupting the thread for another - * purpose. - * - * @return {@link ClientExecutionTimeoutException} if the {@link InterruptedException} was - * caused by the {@link ClientExecutionTimer}. Otherwise re-interrupts the current thread - * and returns a {@link SdkClientException} wrapping an {@link InterruptedException} - */ - private RuntimeException handleInterruptedException(RequestExecutionContext context, InterruptedException e) { - if (e instanceof SdkInterruptedException) { - ((SdkInterruptedException) e).getResponseStream().ifPresent(r -> invokeSafely(r::close)); - } - if (context.clientExecutionTrackerTask().hasTimeoutExpired()) { - // Clear the interrupt status - Thread.interrupted(); - return ClientExecutionTimeoutException.builder().build(); - } else { - Thread.currentThread().interrupt(); - return AbortedException.builder().cause(e).build(); - } - } - - /** - * Gets the correct client execution timeout taking into account precedence of the - * configuration in {@link SdkRequest} versus {@link SdkClientConfiguration}. - * - * @param requestConfig Current request configuration - * @return Client Execution timeout value or 0 if none is set - */ - private long getClientExecutionTimeoutInMillis(RequestOverrideConfiguration requestConfig) { - return 0; - } -} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/MakeAsyncHttpRequestStage.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/MakeAsyncHttpRequestStage.java index 85779c737e9c..951b035ffbf7 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/MakeAsyncHttpRequestStage.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/MakeAsyncHttpRequestStage.java @@ -15,16 +15,21 @@ package software.amazon.awssdk.core.internal.http.pipeline.stages; +import static software.amazon.awssdk.core.internal.http.timers.TimerUtils.timeCompletableFuture; + import java.nio.ByteBuffer; +import java.time.Duration; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledExecutorService; import org.reactivestreams.Publisher; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.RequestOverrideConfiguration; +import software.amazon.awssdk.core.SdkStandardLogger; import software.amazon.awssdk.core.client.config.SdkAdvancedAsyncClientOption; import software.amazon.awssdk.core.client.config.SdkClientOption; +import software.amazon.awssdk.core.exception.ApiCallAttemptTimeoutException; import software.amazon.awssdk.core.exception.SdkException; import software.amazon.awssdk.core.internal.Response; import software.amazon.awssdk.core.internal.http.HttpClientDependencies; @@ -32,15 +37,19 @@ import software.amazon.awssdk.core.internal.http.RequestExecutionContext; import software.amazon.awssdk.core.internal.http.async.SimpleRequestProvider; import software.amazon.awssdk.core.internal.http.pipeline.RequestPipeline; -import software.amazon.awssdk.http.HttpStatusFamily; +import software.amazon.awssdk.core.internal.http.timers.TimeoutTracker; +import software.amazon.awssdk.http.Abortable; import software.amazon.awssdk.http.SdkHttpFullRequest; import software.amazon.awssdk.http.SdkHttpFullResponse; import software.amazon.awssdk.http.SdkHttpMethod; import software.amazon.awssdk.http.SdkHttpResponse; import software.amazon.awssdk.http.SdkRequestContext; +import software.amazon.awssdk.http.async.AbortableRunnable; import software.amazon.awssdk.http.async.SdkAsyncHttpClient; import software.amazon.awssdk.http.async.SdkHttpRequestProvider; import software.amazon.awssdk.http.async.SdkHttpResponseHandler; +import software.amazon.awssdk.utils.Logger; +import software.amazon.awssdk.utils.OptionalUtils; /** * Delegate to the HTTP implementation to make an HTTP request and receive the response. @@ -49,12 +58,14 @@ public final class MakeAsyncHttpRequestStage implements RequestPipeline>> { - private static final Logger log = LoggerFactory.getLogger(MakeAsyncHttpRequestStage.class); + private static final Logger log = Logger.loggerFor(MakeAsyncHttpRequestStage.class); private final SdkAsyncHttpClient sdkAsyncHttpClient; private final SdkHttpResponseHandler responseHandler; private final SdkHttpResponseHandler errorResponseHandler; private final Executor futureCompletionExecutor; + private final ScheduledExecutorService timeoutExecutor; + private final Duration apiCallAttemptTimeout; public MakeAsyncHttpRequestStage(SdkHttpResponseHandler responseHandler, SdkHttpResponseHandler errorResponseHandler, @@ -64,6 +75,8 @@ public MakeAsyncHttpRequestStage(SdkHttpResponseHandler responseHandler this.futureCompletionExecutor = dependencies.clientConfiguration().option(SdkAdvancedAsyncClientOption.FUTURE_COMPLETION_EXECUTOR); this.sdkAsyncHttpClient = dependencies.clientConfiguration().option(SdkClientOption.ASYNC_HTTP_CLIENT); + this.apiCallAttemptTimeout = dependencies.clientConfiguration().option(SdkClientOption.API_CALL_ATTEMPT_TIMEOUT); + this.timeoutExecutor = dependencies.clientConfiguration().option(SdkClientOption.SCHEDULED_EXECUTOR_SERVICE); } /** @@ -77,7 +90,10 @@ public CompletableFuture> execute(SdkHttpFullRequest request, private CompletableFuture> executeHttpRequest(SdkHttpFullRequest request, RequestExecutionContext context) throws Exception { - Completable completable = new Completable(); + + long timeout = apiCallAttemptTimeoutInMillis(context.requestConfig()); + Completable completable = new Completable(timeout); + SdkHttpResponseHandler> handler = new ResponseHandler(completable); SdkHttpRequestProvider requestProvider = context.requestProvider() == null @@ -86,13 +102,19 @@ private CompletableFuture> executeHttpRequest(SdkHttpFullReque // Set content length if it hasn't been set already. SdkHttpFullRequest requestWithContentLength = getRequestWithContentLength(request, requestProvider); - sdkAsyncHttpClient.prepareRequest(requestWithContentLength, SdkRequestContext.builder().build(), - requestProvider, - handler) - .run(); + AbortableRunnable abortableRunnable = sdkAsyncHttpClient.prepareRequest(requestWithContentLength, SdkRequestContext + .builder().build(), + requestProvider, + handler); + + // Set the abortable so that the abortable request can be aborted after timeout if timeout is enabled + completable.abortable(abortableRunnable); - // TODO client execution timer - // context.clientExecutionTrackerTask().setCurrentHttpRequest(requestCallable); + if (context.apiCallTimeoutTracker() != null && context.apiCallTimeoutTracker().isEnabled()) { + context.apiCallTimeoutTracker().abortable(abortableRunnable); + } + + abortableRunnable.run(); return completable.completableFuture; } @@ -132,10 +154,12 @@ private ResponseHandler(Completable completable) { @Override public void headersReceived(SdkHttpResponse response) { - if (HttpStatusFamily.of(response.statusCode()) == HttpStatusFamily.SUCCESSFUL) { + if (response.isSuccessful()) { + SdkStandardLogger.REQUEST_LOGGER.debug(() -> "Received successful response: " + response.statusCode()); isSuccess = true; responseHandler.headersReceived(response); } else { + SdkStandardLogger.REQUEST_LOGGER.debug(() -> "Received error response: " + response.statusCode()); errorResponseHandler.headersReceived(response); } this.response = response; @@ -188,8 +212,21 @@ private Response handleResponse(SdkHttpFullResponse httpResponse) { */ private class Completable { private final CompletableFuture> completableFuture = new CompletableFuture<>(); + private TimeoutTracker timeoutTracker; - public void complete(Response result) { + Completable(long timeoutInMills) { + timeoutTracker = timeCompletableFuture(completableFuture, timeoutExecutor, + ApiCallAttemptTimeoutException.create(timeoutInMills), + timeoutInMills); + } + + void abortable(Abortable abortable) { + if (timeoutTracker != null) { + timeoutTracker.abortable(abortable); + } + } + + void complete(Response result) { try { futureCompletionExecutor.execute(() -> completableFuture.complete(result)); } catch (RejectedExecutionException e) { @@ -197,7 +234,7 @@ public void complete(Response result) { } } - public void completeExceptionally(Throwable exception) { + void completeExceptionally(Throwable exception) { try { futureCompletionExecutor.execute(() -> completableFuture.completeExceptionally(exception)); } catch (RejectedExecutionException e) { @@ -214,4 +251,11 @@ private RejectedExecutionException explainRejection(RejectedExecutionException e "performed on the async execution thread.", e); } } + + private long apiCallAttemptTimeoutInMillis(RequestOverrideConfiguration requestConfig) { + return OptionalUtils.firstPresent( + requestConfig.apiCallAttemptTimeout(), () -> apiCallAttemptTimeout) + .map(Duration::toMillis) + .orElse(0L); + } } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/TimerExceptionHandlingStage.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/TimerExceptionHandlingStage.java deleted file mode 100644 index 389d608cf224..000000000000 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/TimerExceptionHandlingStage.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2010-2018 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.core.internal.http.pipeline.stages; - -import java.io.IOException; -import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.core.internal.Response; -import software.amazon.awssdk.core.internal.http.RequestExecutionContext; -import software.amazon.awssdk.core.internal.http.pipeline.RequestPipeline; -import software.amazon.awssdk.core.internal.http.pipeline.RequestToResponsePipeline; -import software.amazon.awssdk.http.SdkHttpFullRequest; - -/** - * Translates an {@link IOException} to an {@link InterruptedException} if that IOException was caused by the - * {@link software.amazon.awssdk.core.internal.http.timers.client.ClientExecutionTimer}. This is important for consistent handling - * of timeouts in {@link ClientExecutionTimedStage}. - */ -@SdkInternalApi -public class TimerExceptionHandlingStage implements RequestToResponsePipeline { - - private final RequestPipeline> requestPipeline; - - public TimerExceptionHandlingStage(RequestPipeline> requestPipeline) { - this.requestPipeline = requestPipeline; - } - - public Response execute(SdkHttpFullRequest request, RequestExecutionContext context) throws Exception { - try { - return requestPipeline.execute(request, context); - } catch (Exception e) { - if (isTimeoutCausedException(context, e)) { - throw new InterruptedException(); - } - throw e; - } - } - - /** - * Detects if the exception thrown was triggered by the execution timeout. - * - * @param context {@link RequestExecutionContext} object. - * @param e Exception thrown by request pipeline. - * @return True if the exception was caused by the execution timeout, false if not. - */ - private boolean isTimeoutCausedException(RequestExecutionContext context, Exception e) { - return isIoException(e) && context.clientExecutionTrackerTask().hasTimeoutExpired(); - } - - /** - * Detects if this exception is an {@link IOException} or was caused by an {@link IOException}. Will unwrap the exception - * until an {@link IOException} is found or the cause it empty. - * - * @param e Exception to test. - * @return True if exception was caused by an {@link IOException}, false otherwise. - */ - private boolean isIoException(Exception e) { - Throwable cur = e; - while (cur != null) { - if (cur instanceof IOException) { - return true; - } - cur = cur.getCause(); - } - return false; - } -} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/client/ClientExecutionAbortTrackerTaskImpl.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/ApiCallTimeoutTracker.java similarity index 50% rename from core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/client/ClientExecutionAbortTrackerTaskImpl.java rename to core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/ApiCallTimeoutTracker.java index ac6e21ee5477..aaafcb728634 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/client/ClientExecutionAbortTrackerTaskImpl.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/ApiCallTimeoutTracker.java @@ -13,47 +13,45 @@ * permissions and limitations under the License. */ -package software.amazon.awssdk.core.internal.http.timers.client; +package software.amazon.awssdk.core.internal.http.timers; -import java.util.concurrent.Future; import java.util.concurrent.ScheduledFuture; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.http.Abortable; import software.amazon.awssdk.utils.Validate; /** - * Keeps track of the scheduled {@link ClientExecutionAbortTask} and the associated {@link Future} + * Api Call Timeout Tracker to track the {@link TimeoutTask} and the {@link ScheduledFuture}. */ @SdkInternalApi -public class ClientExecutionAbortTrackerTaskImpl implements ClientExecutionAbortTrackerTask { +public class ApiCallTimeoutTracker implements TimeoutTracker { + + private final TimeoutTask timeoutTask; - private final ClientExecutionAbortTask task; private final ScheduledFuture future; - public ClientExecutionAbortTrackerTaskImpl(final ClientExecutionAbortTask task, final ScheduledFuture future) { - this.task = Validate.paramNotNull(task, "task"); - this.future = Validate.paramNotNull(future, "future"); + public ApiCallTimeoutTracker(TimeoutTask timeout, ScheduledFuture future) { + this.timeoutTask = Validate.paramNotNull(timeout, "timeoutTask"); + this.future = Validate.paramNotNull(future, "scheduledFuture"); } @Override - public void setCurrentHttpRequest(Abortable newRequest) { - task.setCurrentHttpRequest(newRequest); + public boolean hasExecuted() { + return timeoutTask.hasExecuted(); } @Override - public boolean hasTimeoutExpired() { - return task.hasClientExecutionAborted(); + public boolean isEnabled() { + return timeoutTask.isEnabled(); } @Override - public boolean isEnabled() { - return task.isEnabled(); + public void cancel() { + future.cancel(false); } @Override - public void cancelTask() { - // Ensure task is canceled even if it's running as we don't want the Thread to be - // interrupted in the caller's code - future.cancel(false); + public void abortable(Abortable abortable) { + timeoutTask.abortable(abortable); } } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/AsyncTimeoutTask.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/AsyncTimeoutTask.java new file mode 100644 index 000000000000..7b08135f7501 --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/AsyncTimeoutTask.java @@ -0,0 +1,75 @@ +/* + * Copyright 2010-2018 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.core.internal.http.timers; + +import java.util.concurrent.CompletableFuture; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.http.Abortable; +import software.amazon.awssdk.utils.Logger; +import software.amazon.awssdk.utils.Validate; + +/** + * Asynchronous implementations of {@link TimeoutTask} to be scheduled to fail + * the {@link CompletableFuture} and abort the asynchronous requests. + */ +@SdkInternalApi +public class AsyncTimeoutTask implements TimeoutTask { + private static final Logger log = Logger.loggerFor(AsyncTimeoutTask.class); + private final SdkException exception; + private volatile boolean hasExecuted; + + private final CompletableFuture completableFuture; + private Abortable abortable; + + /** + * Constructs a new {@link AsyncTimeoutTask}. + * + * @param completableFuture the {@link CompletableFuture} to fail + * @param exception the exception to thrown + */ + public AsyncTimeoutTask(CompletableFuture completableFuture, SdkException exception) { + this.completableFuture = Validate.paramNotNull(completableFuture, "completableFuture"); + this.exception = Validate.paramNotNull(exception, "exception"); + } + + @Override + public void abortable(Abortable abortable) { + this.abortable = abortable; + } + + @Override + public void run() { + hasExecuted = true; + if (!completableFuture.isDone()) { + completableFuture.completeExceptionally(exception); + } + + if (abortable != null) { + abortable.abort(); + } + } + + @Override + public boolean hasExecuted() { + return hasExecuted; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/client/NoOpClientExecutionAbortTrackerTask.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/NoOpTimeoutTracker.java similarity index 59% rename from core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/client/NoOpClientExecutionAbortTrackerTask.java rename to core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/NoOpTimeoutTracker.java index 22815297bc9d..aa61ba530e79 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/client/NoOpClientExecutionAbortTrackerTask.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/NoOpTimeoutTracker.java @@ -13,30 +13,24 @@ * permissions and limitations under the License. */ -package software.amazon.awssdk.core.internal.http.timers.client; +package software.amazon.awssdk.core.internal.http.timers; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.http.Abortable; /** - * Dummy implementation of {@link ClientExecutionAbortTrackerTask} used when the timer is disabled - * for a request + * A no op implementation of {@link TimeoutTracker}. */ @SdkInternalApi -public final class NoOpClientExecutionAbortTrackerTask implements ClientExecutionAbortTrackerTask { +public final class NoOpTimeoutTracker implements TimeoutTracker { - public static final NoOpClientExecutionAbortTrackerTask INSTANCE = new NoOpClientExecutionAbortTrackerTask(); + public static final NoOpTimeoutTracker INSTANCE = new NoOpTimeoutTracker(); - // Singleton - private NoOpClientExecutionAbortTrackerTask() { + private NoOpTimeoutTracker() { } @Override - public void setCurrentHttpRequest(Abortable newRequest) { - } - - @Override - public boolean hasTimeoutExpired() { + public boolean hasExecuted() { return false; } @@ -46,7 +40,12 @@ public boolean isEnabled() { } @Override - public void cancelTask() { + public void cancel() { + } + @Override + public void abortable(Abortable abortable) { + + } } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/client/ClientExecutionAbortTrackerTask.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/TimeoutTask.java similarity index 66% rename from core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/client/ClientExecutionAbortTrackerTask.java rename to core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/TimeoutTask.java index 06d34cefd20d..0b60789bd77c 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/client/ClientExecutionAbortTrackerTask.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/TimeoutTask.java @@ -13,30 +13,28 @@ * permissions and limitations under the License. */ -package software.amazon.awssdk.core.internal.http.timers.client; +package software.amazon.awssdk.core.internal.http.timers; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.http.Abortable; @SdkInternalApi -public interface ClientExecutionAbortTrackerTask { +public interface TimeoutTask extends Runnable { /** - * Client execution timer task needs to abort the current running HTTP request when executed. + * @param abortable the abortable request */ - void setCurrentHttpRequest(Abortable newRequest); + void abortable(Abortable abortable); /** - * @return True if client execution has been aborted by the timer task. False otherwise + * @return True if abortable request has been aborted by the timer task. False otherwise */ - boolean hasTimeoutExpired(); + boolean hasExecuted(); /** - * @return True if the timer task has been scheduled. False if the client execution timeout is + * @return True if the timer task has been scheduled. False if client execution timeout is * disabled for this request */ boolean isEnabled(); - void cancelTask(); - } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/TimeoutThreadPoolBuilder.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/TimeoutThreadPoolBuilder.java deleted file mode 100644 index 574b1cddbb20..000000000000 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/TimeoutThreadPoolBuilder.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2010-2018 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.core.internal.http.timers; - -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; -import software.amazon.awssdk.annotations.SdkInternalApi; - -/** - * Utility class to build the {@link ScheduledThreadPoolExecutor} for the request timeout and client - * execution timeout features - */ -@SdkInternalApi -public final class TimeoutThreadPoolBuilder { - - private TimeoutThreadPoolBuilder() { - } - - /** - * Creates a {@link ScheduledThreadPoolExecutor} with custom name for the threads. - * - * @param name the prefix to add to the thread name in ThreadFactory. - * @return The default thread pool for request timeout and client execution timeout features. - */ - public static ScheduledThreadPoolExecutor buildDefaultTimeoutThreadPool(final String name) { - ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(5, getThreadFactory(name)); - executor.setRemoveOnCancelPolicy(true); - executor.setKeepAliveTime(5, TimeUnit.SECONDS); - executor.allowCoreThreadTimeOut(true); - - return executor; - } - - private static ThreadFactory getThreadFactory(final String name) { - return new ThreadFactory() { - private int threadCount = 1; - - public Thread newThread(Runnable r) { - Thread thread = new Thread(r); - if (name != null) { - thread.setName(name + "-" + threadCount++); - } - thread.setPriority(Thread.MAX_PRIORITY); - return thread; - } - }; - } -} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/client/ClientExecutionAbortTask.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/TimeoutTracker.java similarity index 59% rename from core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/client/ClientExecutionAbortTask.java rename to core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/TimeoutTracker.java index 1b4f658cc320..ecf64ab3972f 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/client/ClientExecutionAbortTask.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/TimeoutTracker.java @@ -13,30 +13,39 @@ * permissions and limitations under the License. */ -package software.amazon.awssdk.core.internal.http.timers.client; +package software.amazon.awssdk.core.internal.http.timers; +import java.util.concurrent.ScheduledFuture; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.http.Abortable; /** - * Task to be scheduled by {@link ClientExecutionTimer} + * Tracker task to track the {@link TimeoutTask} and the {@link ScheduledFuture} that + * schedules the timeout task. */ @SdkInternalApi -public interface ClientExecutionAbortTask extends Runnable { +public interface TimeoutTracker { /** - * Client Execution timer task needs to abort the current running HTTP request when executed. + * @return True if timeout task has been executed. False otherwise */ - void setCurrentHttpRequest(Abortable newRequest); + boolean hasExecuted(); /** - * @return True if client execution has been aborted by the timer task. False otherwise + * @return True if the timer task has been scheduled. False if the timeout is + * disabled for this request */ - boolean hasClientExecutionAborted(); + boolean isEnabled(); /** - * @return True if the timer task has been scheduled. False if client execution timeout is - * disabled for this request + * cancel the {@link ScheduledFuture} */ - boolean isEnabled(); + void cancel(); + + /** + * Sets the abortable task to be aborted by {@link TimeoutTask} + * + * @param abortable the abortable task + */ + void abortable(Abortable abortable); } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/TimerUtils.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/TimerUtils.java new file mode 100644 index 000000000000..57c0a21395cf --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/TimerUtils.java @@ -0,0 +1,62 @@ +/* + * Copyright 2010-2018 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.core.internal.http.timers; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.exception.SdkClientException; + +@SdkInternalApi +public final class TimerUtils { + + private TimerUtils() { + } + + /** + * Schedule a {@link TimeoutTask} and exceptional completes a {@link CompletableFuture} with the provide exception + * if not otherwise completed before the given timeout. + * + * @param completableFuture the completableFutre to be timed + * @param timeoutExecutor the executor to execute the {@link TimeoutTask} + * @param exceptionToThrow the exception to thrown after timeout + * @param timeoutInMills the timeout in milliseconds. + * @param the type of the {@link CompletableFuture} + * @return a {@link TimeoutTracker} + */ + public static TimeoutTracker timeCompletableFuture(CompletableFuture completableFuture, + ScheduledExecutorService timeoutExecutor, + SdkClientException exceptionToThrow, + long timeoutInMills) { + if (timeoutInMills <= 0) { + return NoOpTimeoutTracker.INSTANCE; + } + + TimeoutTask timeoutTask = new AsyncTimeoutTask(completableFuture, exceptionToThrow); + + ScheduledFuture scheduledFuture = + timeoutExecutor.schedule(timeoutTask, + timeoutInMills, + TimeUnit.MILLISECONDS); + TimeoutTracker timeoutTracker = new ApiCallTimeoutTracker(timeoutTask, scheduledFuture); + + completableFuture.whenComplete((o, t) -> timeoutTracker.cancel()); + + return timeoutTracker; + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/client/ClientExecutionAbortTaskImpl.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/client/ClientExecutionAbortTaskImpl.java deleted file mode 100644 index d3aadc6482a6..000000000000 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/client/ClientExecutionAbortTaskImpl.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2010-2018 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.core.internal.http.timers.client; - -import static software.amazon.awssdk.utils.Validate.notNull; - -import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.http.Abortable; - -/** - * Implementation of {@link ClientExecutionAbortTask} that interrupts the caller thread and aborts - * any HTTP request when triggered - */ -@SdkInternalApi -public class ClientExecutionAbortTaskImpl implements ClientExecutionAbortTask { - - private final Thread thread; - private volatile boolean hasTaskExecuted; - private volatile Abortable currentRequest; - - public ClientExecutionAbortTaskImpl(Thread thread) { - this.thread = thread; - } - - @Override - public void run() { - hasTaskExecuted = true; - if (!thread.isInterrupted()) { - thread.interrupt(); - } - if (currentRequest != null) { - currentRequest.abort(); - } - } - - @Override - public void setCurrentHttpRequest(Abortable newRequest) { - this.currentRequest = notNull(newRequest, "Abortable cannot be null"); - } - - public boolean hasClientExecutionAborted() { - return hasTaskExecuted; - } - - @Override - public boolean isEnabled() { - return true; - } - -} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/client/ClientExecutionTimer.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/client/ClientExecutionTimer.java deleted file mode 100644 index c8a5d10841bc..000000000000 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/timers/client/ClientExecutionTimer.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2010-2018 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.core.internal.http.timers.client; - -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.annotations.SdkTestInternalApi; -import software.amazon.awssdk.annotations.ThreadSafe; -import software.amazon.awssdk.core.internal.http.timers.TimeoutThreadPoolBuilder; -import software.amazon.awssdk.utils.SdkAutoCloseable; - -/** - * Represents a timer to enforce a timeout on the total client execution time. That is the time - * spent executing request handlers, any HTTP request including retries, unmarshalling, etc. - */ -// DO NOT override finalize(). The shutdown() method is called from AmazonHttpClient#shutdown() -// which is called from it's finalize() method. Since finalize methods can be be called in any -// order and even concurrently, we need to rely on AmazonHttpClient to call our shutdown() method. -@SdkInternalApi -@ThreadSafe -public class ClientExecutionTimer implements SdkAutoCloseable { - - private static final String THREAD_NAME_PREFIX = "AwsSdkClientExecutionTimerThread"; - - private volatile ScheduledThreadPoolExecutor executor; - - /** - * Start the timer with the specified timeout and return a object that can be used to track the - * state of the timer and cancel it if need be. - * - * @param clientExecutionTimeoutMillis - * A positive value here enables the timer, a non-positive value disables it and - * returns a dummy tracker task - * @return Implementation of {@link ClientExecutionAbortTrackerTaskImpl} to query the state of - * the task, provide it with up to date context, and cancel it if appropriate - */ - public ClientExecutionAbortTrackerTask startTimer(long clientExecutionTimeoutMillis) { - if (isTimeoutDisabled(clientExecutionTimeoutMillis)) { - return NoOpClientExecutionAbortTrackerTask.INSTANCE; - } else if (executor == null) { - initializeExecutor(); - } - return scheduleTimerTask(clientExecutionTimeoutMillis); - } - - /** - * Executor is lazily initialized as it's not compatible with Java 6 - */ - private synchronized void initializeExecutor() { - if (executor == null) { - executor = TimeoutThreadPoolBuilder.buildDefaultTimeoutThreadPool(THREAD_NAME_PREFIX); - } - } - - /** - * This method is current exposed for testing purposes - * - * @return The underlying {@link ScheduledThreadPoolExecutor} - */ - @SdkTestInternalApi - public ScheduledThreadPoolExecutor getExecutor() { - return this.executor; - } - - /** - * Shutdown the underlying {@link ScheduledThreadPoolExecutor}. Should be invoked when - * the client handler is shut down. - */ - @Override - public void close() { - if (executor != null) { - executor.shutdown(); - } - } - - private ClientExecutionAbortTrackerTask scheduleTimerTask(long clientExecutionTimeoutMillis) { - ClientExecutionAbortTask timerTask = new ClientExecutionAbortTaskImpl(Thread.currentThread()); - ScheduledFuture timerTaskFuture = executor.schedule(timerTask, clientExecutionTimeoutMillis, - TimeUnit.MILLISECONDS); - return new ClientExecutionAbortTrackerTaskImpl(timerTask, timerTaskFuture); - } - - private boolean isTimeoutDisabled(long clientExecutionTimeoutMillis) { - return clientExecutionTimeoutMillis <= 0; - } - -} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/retry/SdkDefaultRetrySetting.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/retry/SdkDefaultRetrySetting.java index 0db0edc8af70..55bd52cc14f4 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/retry/SdkDefaultRetrySetting.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/retry/SdkDefaultRetrySetting.java @@ -22,6 +22,7 @@ import java.util.HashSet; import java.util.Set; import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.exception.ApiCallAttemptTimeoutException; import software.amazon.awssdk.core.exception.RetryableException; import software.amazon.awssdk.http.HttpStatusCode; @@ -64,6 +65,7 @@ public final class SdkDefaultRetrySetting { Set> retryableExceptions = new HashSet<>(); retryableExceptions.add(RetryableException.class); retryableExceptions.add(IOException.class); + retryableExceptions.add(ApiCallAttemptTimeoutException.class); RETRYABLE_EXCEPTIONS = unmodifiableSet(retryableExceptions); } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ClientExecutionTimedStageTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ClientExecutionTimedStageTest.java deleted file mode 100644 index c4340208b93a..000000000000 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ClientExecutionTimedStageTest.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2010-2018 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.core.internal.http.pipeline.stages; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import org.junit.Ignore; -import org.junit.Test; -import software.amazon.awssdk.annotations.ReviewBeforeRelease; -import software.amazon.awssdk.core.SdkRequest; -import software.amazon.awssdk.core.SdkRequestOverrideConfiguration; -import software.amazon.awssdk.core.exception.AbortedException; -import software.amazon.awssdk.core.http.ExecutionContext; -import software.amazon.awssdk.core.http.NoopTestRequest; -import software.amazon.awssdk.core.internal.Response; -import software.amazon.awssdk.core.internal.client.config.SdkClientConfiguration; -import software.amazon.awssdk.core.internal.http.HttpClientDependencies; -import software.amazon.awssdk.core.internal.http.RequestExecutionContext; -import software.amazon.awssdk.core.internal.http.pipeline.RequestPipeline; -import software.amazon.awssdk.core.internal.http.timers.client.ClientExecutionTimer; -import software.amazon.awssdk.core.internal.util.CapacityManager; -import software.amazon.awssdk.http.SdkHttpFullRequest; - -/** - * Tests for {@link ClientExecutionTimedStage}. - */ -@Ignore -@ReviewBeforeRelease("Fix this test once clientExecutionTimedStage is fixed") -public class ClientExecutionTimedStageTest { - - @Test(expected = RuntimeException.class) - public void nonTimerInterruption_RuntimeExceptionThrown_interruptFlagIsPreserved() throws Exception { - nonTimerInterruption_interruptFlagIsPreserved(new RuntimeException()); - } - - @Test(expected = AbortedException.class) - public void nonTimerInterruption_InterruptedExceptionThrown_interruptFlagIsPreserved() throws Exception { - nonTimerInterruption_interruptFlagIsPreserved(new InterruptedException()); - } - - /** - * Test to ensure that if the execution *did not* expire but the - * interrupted flag is set that it's not cleared by - * ClientExecutionTimedStage because it's not an interruption by the timer. - * - * @param exceptionToThrow The exception to throw from the wrapped pipeline. - */ - private void nonTimerInterruption_interruptFlagIsPreserved(final Exception exceptionToThrow) throws Exception { - RequestPipeline> wrapped = - (RequestPipeline>) mock(RequestPipeline.class); - ClientExecutionTimedStage stage = new ClientExecutionTimedStage<>(HttpClientDependencies.builder() - .clientExecutionTimer(new ClientExecutionTimer()) - .clientConfiguration(mock(SdkClientConfiguration.class)) - .capacityManager(mock(CapacityManager.class)) - .build(), - wrapped); - - when(wrapped.execute(any(SdkHttpFullRequest.class), any(RequestExecutionContext.class))).thenAnswer(invocationOnMock -> { - Thread.currentThread().interrupt(); - throw exceptionToThrow; - }); - - SdkRequest originalRequest = NoopTestRequest.builder() - .overrideConfiguration(SdkRequestOverrideConfiguration.builder() - .build()) - .build(); - - - try { - stage.execute(mock(SdkHttpFullRequest.class), RequestExecutionContext.builder() - .executionContext(mock(ExecutionContext.class)) - .originalRequest(originalRequest) - .build()); - } finally { - assertThat(Thread.interrupted()); - } - } -} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/MakeAsyncHttpRequestStageTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/MakeAsyncHttpRequestStageTest.java new file mode 100644 index 000000000000..e52005416717 --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/MakeAsyncHttpRequestStageTest.java @@ -0,0 +1,110 @@ +/* + * Copyright 2010-2018 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.core.internal.http.pipeline.stages; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static software.amazon.awssdk.core.client.config.SdkClientOption.API_CALL_ATTEMPT_TIMEOUT; +import static software.amazon.awssdk.core.client.config.SdkClientOption.ASYNC_HTTP_CLIENT; +import static software.amazon.awssdk.core.client.config.SdkClientOption.SCHEDULED_EXECUTOR_SERVICE; +import static software.amazon.awssdk.core.internal.util.AsyncResponseHandlerTestUtils.noOpResponseHandler; + +import java.time.Duration; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import software.amazon.awssdk.core.http.ExecutionContext; +import software.amazon.awssdk.core.http.NoopTestRequest; +import software.amazon.awssdk.core.internal.client.config.SdkClientConfiguration; +import software.amazon.awssdk.core.internal.http.HttpClientDependencies; +import software.amazon.awssdk.core.internal.http.RequestExecutionContext; +import software.amazon.awssdk.core.internal.http.timers.ClientExecutionAndRequestTimerTestUtils; +import software.amazon.awssdk.core.internal.util.CapacityManager; +import software.amazon.awssdk.http.async.AbortableRunnable; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import utils.ValidSdkObjects; + +@RunWith(MockitoJUnitRunner.class) +public class MakeAsyncHttpRequestStageTest { + + @Mock + private SdkAsyncHttpClient sdkAsyncHttpClient; + + @Mock + private ScheduledExecutorService timeoutExecutor; + + @Mock + private AbortableRunnable abortableRunnable; + + @Mock + private ScheduledFuture future; + + private MakeAsyncHttpRequestStage stage; + + @Before + public void setup() { + when(sdkAsyncHttpClient.prepareRequest(any(), any(), any(), any())).thenReturn(abortableRunnable); + when(timeoutExecutor.schedule(any(Runnable.class), anyLong(), any(TimeUnit.class))).thenReturn(future); + } + + @Test + public void apiCallAttemptTimeoutEnabled_shouldInvokeExecutor() throws Exception { + stage = new MakeAsyncHttpRequestStage(noOpResponseHandler(), noOpResponseHandler(), + clientDependencies(Duration.ofMillis(1000))); + stage.execute(ValidSdkObjects.sdkHttpFullRequest().build(), requestContext()); + + verify(timeoutExecutor, times(1)).schedule(any(Runnable.class), anyLong(), any(TimeUnit.class)); + } + + @Test + public void apiCallAttemptTimeoutNotEnabled_shouldNotInvokeExecutor() throws Exception { + stage = new MakeAsyncHttpRequestStage(noOpResponseHandler(), noOpResponseHandler(), clientDependencies(null)); + stage.execute(ValidSdkObjects.sdkHttpFullRequest().build(), requestContext()); + + verify(timeoutExecutor, never()).schedule(any(Runnable.class), anyLong(), any(TimeUnit.class)); + } + + private HttpClientDependencies clientDependencies(Duration timeout) { + SdkClientConfiguration configuration = SdkClientConfiguration.builder() + .option(ASYNC_HTTP_CLIENT, sdkAsyncHttpClient) + .option(SCHEDULED_EXECUTOR_SERVICE, timeoutExecutor) + .option(API_CALL_ATTEMPT_TIMEOUT, timeout) + .build(); + + + return HttpClientDependencies.builder() + .clientConfiguration(configuration) + .capacityManager(new CapacityManager(2)) + .build(); + } + + private RequestExecutionContext requestContext() { + ExecutionContext executionContext = ClientExecutionAndRequestTimerTestUtils.executionContext(ValidSdkObjects.sdkHttpFullRequest().build()); + return RequestExecutionContext.builder() + .executionContext(executionContext) + .originalRequest(NoopTestRequest.builder().build()) + .build(); + } +} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/MoveParametersToBodyStageTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/MoveParametersToBodyStageTest.java index badf71ba3cc7..7d6182c56269 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/MoveParametersToBodyStageTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/MoveParametersToBodyStageTest.java @@ -64,6 +64,7 @@ public void postWithContentIsUnaltered() throws Exception { SdkHttpFullRequest.Builder mutableRequest = ValidSdkObjects.sdkHttpFullRequest() .content(content) .method(SdkHttpMethod.POST) + .clearHeaders() .putRawQueryParameter("key", singletonList("value")); SdkHttpFullRequest output = sut.execute(mutableRequest, requestContext(mutableRequest)).build(); @@ -77,6 +78,7 @@ public void postWithContentIsUnaltered() throws Exception { public void onlyAlterRequestsIfParamsArePresent() throws Exception { SdkHttpFullRequest.Builder mutableRequest = ValidSdkObjects.sdkHttpFullRequest() .content(null) + .clearHeaders() .method(SdkHttpMethod.POST); SdkHttpFullRequest output = sut.execute(mutableRequest, requestContext(mutableRequest)).build(); @@ -90,6 +92,7 @@ private void nonPostRequestsUnaltered(SdkHttpMethod method) { SdkHttpFullRequest.Builder mutableRequest = ValidSdkObjects.sdkHttpFullRequest() .content(null) .method(method) + .clearHeaders() .putRawQueryParameter("key", singletonList("value")); SdkHttpFullRequest output = sut.execute(mutableRequest, requestContext(mutableRequest)).build(); diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/AsyncHttpClientApiCallTimeoutTests.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/AsyncHttpClientApiCallTimeoutTests.java new file mode 100644 index 000000000000..c2841c9748b0 --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/AsyncHttpClientApiCallTimeoutTests.java @@ -0,0 +1,192 @@ +/* + * Copyright 2010-2018 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.core.internal.http.timers; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static software.amazon.awssdk.core.internal.http.timers.TimeoutTestConstants.API_CALL_TIMEOUT; +import static software.amazon.awssdk.core.internal.http.timers.TimeoutTestConstants.SLOW_REQUEST_HANDLER_TIMEOUT; +import static software.amazon.awssdk.core.internal.util.AsyncResponseHandlerTestUtils.noOpResponseHandler; +import static software.amazon.awssdk.core.internal.util.AsyncResponseHandlerTestUtils.superSlowResponseHandler; +import static utils.HttpTestUtils.testAsyncClientBuilder; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import java.io.ByteArrayInputStream; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.CompletableFuture; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import software.amazon.awssdk.core.exception.ApiCallAttemptTimeoutException; +import software.amazon.awssdk.core.exception.ApiCallTimeoutException; +import software.amazon.awssdk.core.http.ExecutionContext; +import software.amazon.awssdk.core.http.NoopTestRequest; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; +import software.amazon.awssdk.core.internal.http.AmazonAsyncHttpClient; +import software.amazon.awssdk.core.internal.http.request.SlowExecutionInterceptor; +import software.amazon.awssdk.core.internal.interceptor.ExecutionInterceptorChain; +import software.amazon.awssdk.core.internal.interceptor.InterceptorContext; +import software.amazon.awssdk.core.retry.RetryPolicy; +import software.amazon.awssdk.core.signer.NoOpSigner; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import utils.ValidSdkObjects; + + +public class AsyncHttpClientApiCallTimeoutTests { + + @Rule + public WireMockRule wireMock = new WireMockRule(0); + + private AmazonAsyncHttpClient httpClient; + + @Before + public void setup() { + httpClient = testAsyncClientBuilder() + .retryPolicy(RetryPolicy.none()) + .apiCallTimeout(API_CALL_TIMEOUT) + .build(); + } + + @Test + public void errorResponse_SlowErrorResponseHandler_ThrowsApiCallTimeoutException() { + stubFor(get(anyUrl()) + .willReturn(aResponse().withStatus(500).withBody("{}"))); + + ExecutionContext executionContext = ClientExecutionAndRequestTimerTestUtils.executionContext(null); + + CompletableFuture future = httpClient.requestExecutionBuilder() + .originalRequest(NoopTestRequest.builder().build()) + .executionContext(executionContext) + .request(generateRequest()) + .errorResponseHandler(superSlowResponseHandler(API_CALL_TIMEOUT.toMillis())) + .execute(noOpResponseHandler()); + + assertThatThrownBy(future::join).hasCauseInstanceOf(ApiCallTimeoutException.class); + } + + @Test + public void errorResponse_SlowAfterErrorRequestHandler_ThrowsApiCallTimeoutException() { + stubFor(get(anyUrl()) + .willReturn(aResponse().withStatus(500).withBody("{}"))); + ExecutionInterceptorChain interceptors = + new ExecutionInterceptorChain( + Collections.singletonList(new SlowExecutionInterceptor().onExecutionFailureWaitInSeconds(SLOW_REQUEST_HANDLER_TIMEOUT))); + + SdkHttpFullRequest request = generateRequest(); + InterceptorContext incerceptorContext = + InterceptorContext.builder() + .request(NoopTestRequest.builder().build()) + .httpRequest(request) + .build(); + + ExecutionContext executionContext = ExecutionContext.builder() + .signer(new NoOpSigner()) + .interceptorChain(interceptors) + .executionAttributes(new ExecutionAttributes()) + .interceptorContext(incerceptorContext) + .build(); + + CompletableFuture future = httpClient.requestExecutionBuilder() + .originalRequest(NoopTestRequest.builder().build()) + .request(request) + .errorResponseHandler(noOpResponseHandler()) + .executionContext(executionContext) + .execute(noOpResponseHandler()); + + assertThatThrownBy(future::join).hasCauseInstanceOf(ApiCallTimeoutException.class); + } + + @Test + public void successfulResponse_SlowBeforeRequestRequestHandler_ThrowsApiCallTimeoutException() { + stubFor(get(anyUrl()) + .willReturn(aResponse().withStatus(200).withBody("{}"))); + ExecutionInterceptor interceptor = + new SlowExecutionInterceptor().beforeTransmissionWaitInSeconds(SLOW_REQUEST_HANDLER_TIMEOUT); + + CompletableFuture future = requestBuilder().executionContext(withInterceptors(interceptor)) + .execute(noOpResponseHandler()); + assertThatThrownBy(future::join).hasCauseInstanceOf(ApiCallTimeoutException.class); + } + + @Test + public void successfulResponse_SlowAfterResponseRequestHandler_ThrowsApiCallTimeoutException() { + stubFor(get(anyUrl()) + .willReturn(aResponse().withStatus(200).withBody("{}"))); + ExecutionInterceptor interceptor = + new SlowExecutionInterceptor().afterTransmissionWaitInSeconds(SLOW_REQUEST_HANDLER_TIMEOUT); + CompletableFuture future = requestBuilder().executionContext(withInterceptors(interceptor)) + .execute(noOpResponseHandler()); + assertThatThrownBy(future::join).hasCauseInstanceOf(ApiCallTimeoutException.class); + } + + @Test + public void successfulResponse_SlowResponseHandler_ThrowsApiCallTimeoutException() { + stubFor(get(anyUrl()) + .willReturn(aResponse().withStatus(200).withBody("{}"))); + CompletableFuture future = requestBuilder().execute(superSlowResponseHandler(API_CALL_TIMEOUT.toMillis())); + assertThatThrownBy(future::join).hasCauseInstanceOf(ApiCallTimeoutException.class); + } + + @Test + public void slowApiAttempt_ThrowsApiCallAttemptTimeoutException() { + httpClient = testAsyncClientBuilder() + .apiCallTimeout(API_CALL_TIMEOUT) + .apiCallAttemptTimeout(Duration.ofMillis(100)) + .build(); + + stubFor(get(anyUrl()) + .willReturn(aResponse().withStatus(200).withBody("{}").withFixedDelay(200))); + CompletableFuture future = requestBuilder().execute(noOpResponseHandler()); + assertThatThrownBy(future::join).hasCauseInstanceOf(ApiCallAttemptTimeoutException.class); + } + + private AmazonAsyncHttpClient.RequestExecutionBuilder requestBuilder() { + return httpClient.requestExecutionBuilder() + .request(generateRequest()) + .originalRequest(NoopTestRequest.builder().build()) + .executionContext(ClientExecutionAndRequestTimerTestUtils.executionContext(null)); + } + + private SdkHttpFullRequest generateRequest() { + return ValidSdkObjects.sdkHttpFullRequest(wireMock.port()) + .host("localhost") + .content(new ByteArrayInputStream("test".getBytes())).build(); + } + + private ExecutionContext withInterceptors(ExecutionInterceptor... requestHandlers) { + + ExecutionInterceptorChain interceptors = + new ExecutionInterceptorChain(Arrays.asList(requestHandlers)); + + InterceptorContext incerceptorContext = + InterceptorContext.builder() + .request(NoopTestRequest.builder().build()) + .httpRequest(generateRequest()) + .build(); + return ExecutionContext.builder() + .signer(new NoOpSigner()) + .interceptorChain(interceptors) + .executionAttributes(new ExecutionAttributes()) + .interceptorContext(incerceptorContext) + .build(); + } +} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/ClientExecutionAndRequestTimerTestUtils.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/ClientExecutionAndRequestTimerTestUtils.java index 3774627bfccd..3d594c27b627 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/ClientExecutionAndRequestTimerTestUtils.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/ClientExecutionAndRequestTimerTestUtils.java @@ -16,7 +16,6 @@ package software.amazon.awssdk.core.internal.http.timers; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; import java.util.Collections; import java.util.concurrent.ScheduledThreadPoolExecutor; @@ -31,7 +30,6 @@ import software.amazon.awssdk.core.internal.http.request.EmptyHttpRequest; import software.amazon.awssdk.core.internal.http.response.ErrorDuringUnmarshallingResponseHandler; import software.amazon.awssdk.core.internal.http.response.NullErrorResponseHandler; -import software.amazon.awssdk.core.internal.http.timers.client.ClientExecutionTimer; import software.amazon.awssdk.core.internal.interceptor.ExecutionInterceptorChain; import software.amazon.awssdk.core.internal.interceptor.InterceptorContext; import software.amazon.awssdk.core.signer.NoOpSigner; @@ -48,13 +46,6 @@ public class ClientExecutionAndRequestTimerTestUtils { */ private static final int WAIT_BEFORE_ASSERT_ON_EXECUTOR = 500; - /** - * Assert that the executor backing {@link ClientExecutionTimer} was never created or used - */ - public static void assertClientExecutionTimerExecutorNotCreated(ClientExecutionTimer clientExecutionTimer) { - assertNull(clientExecutionTimer.getExecutor()); - } - /** * Waits until a little after the thread pools keep alive time and then asserts that all thre * @@ -90,11 +81,6 @@ public static void assertTimerNeverTriggered(ScheduledThreadPoolExecutor timerEx assertNumberOfTasksTriggered(timerExecutor, 0); } - public static void assertNumberOfTasksTriggered(ClientExecutionTimer clientExecutionTimer, - int expectedNumberOfTasks) { - assertNumberOfTasksTriggered(clientExecutionTimer.getExecutor(), expectedNumberOfTasks); - } - private static void assertNumberOfTasksTriggered(ScheduledThreadPoolExecutor timerExecutor, int expectedNumberOfTasks) { waitBeforeAssertOnExecutor(); @@ -153,5 +139,4 @@ public void run() { } }.start(); } - } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/TimeoutTestConstants.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/TimeoutTestConstants.java index 5b1aed53546b..e44a5fdfaeed 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/TimeoutTestConstants.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/TimeoutTestConstants.java @@ -23,8 +23,8 @@ public class TimeoutTestConstants { public static final int TEST_TIMEOUT = 25 * 1000; - public static final Duration CLIENT_EXECUTION_TIMEOUT = Duration.ofSeconds(5); - public static final int SLOW_REQUEST_HANDLER_TIMEOUT = 100; + public static final Duration API_CALL_TIMEOUT = Duration.ofSeconds(1); + public static final int SLOW_REQUEST_HANDLER_TIMEOUT = 2; /** * ScheduledThreadPoolExecutor isn't exact and can be delayed occasionally. For tests where we diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/client/MockedClientTests.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/client/MockedClientTests.java deleted file mode 100644 index efee62934647..000000000000 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/client/MockedClientTests.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2010-2018 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.core.internal.http.timers.client; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.when; - -import java.util.concurrent.ScheduledThreadPoolExecutor; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; -import software.amazon.awssdk.annotations.ReviewBeforeRelease; -import software.amazon.awssdk.core.exception.SdkClientException; -import software.amazon.awssdk.core.internal.http.AmazonSyncHttpClient; -import software.amazon.awssdk.core.internal.http.response.NullResponseHandler; -import software.amazon.awssdk.core.internal.http.timers.ClientExecutionAndRequestTimerTestUtils; -import software.amazon.awssdk.core.retry.RetryPolicy; -import software.amazon.awssdk.http.AbortableCallable; -import software.amazon.awssdk.http.SdkHttpClient; -import software.amazon.awssdk.http.SdkHttpFullResponse; -import utils.HttpTestUtils; - -/** - * These tests don't actually start up a mock server. They use a partially mocked Apache HTTP client - * to return the desired response - */ -@RunWith(MockitoJUnitRunner.class) -@Ignore -@ReviewBeforeRelease("Fix this once ExecutionTimeout is added") -public class MockedClientTests { - - @Mock - private SdkHttpClient sdkHttpClient; - - @Mock - private AbortableCallable sdkResponse; - - @Before - public void setup() throws Exception { - when(sdkHttpClient.prepareRequest(any(), any())).thenReturn(sdkResponse); - when(sdkResponse.call()).thenReturn(SdkHttpFullResponse.builder() - .statusCode(200) - .build()); - } - - @Test - public void clientExecutionTimeoutEnabled_RequestCompletesWithinTimeout_TaskCanceled() throws Exception { - AmazonSyncHttpClient httpClient = HttpTestUtils.testClientBuilder() - .httpClient(sdkHttpClient) - .retryPolicy(RetryPolicy.none()) - .build(); - - try { - ClientExecutionAndRequestTimerTestUtils - .execute(httpClient, ClientExecutionAndRequestTimerTestUtils.createMockGetRequest()); - fail("Exception expected"); - } catch (SdkClientException e) { - NullResponseHandler.assertIsUnmarshallingException(e); - } - - ScheduledThreadPoolExecutor requestTimerExecutor = httpClient.getClientExecutionTimer().getExecutor(); - ClientExecutionAndRequestTimerTestUtils.assertTimerNeverTriggered(requestTimerExecutor); - ClientExecutionAndRequestTimerTestUtils.assertCanceledTasksRemoved(requestTimerExecutor); - // Core threads should be spun up on demand. Since only one task was submitted only one - // thread should exist - assertEquals(1, requestTimerExecutor.getPoolSize()); - ClientExecutionAndRequestTimerTestUtils.assertCoreThreadsShutDownAfterBeingIdle(requestTimerExecutor); - } - -} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/util/AsyncResponseHandlerTestUtils.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/util/AsyncResponseHandlerTestUtils.java new file mode 100644 index 000000000000..17ff09b56e54 --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/util/AsyncResponseHandlerTestUtils.java @@ -0,0 +1,81 @@ +/* + * Copyright 2010-2018 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.core.internal.util; + +import org.reactivestreams.Publisher; +import software.amazon.awssdk.core.exception.SdkServiceException; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.http.async.SdkHttpResponseHandler; + +public class AsyncResponseHandlerTestUtils { + private AsyncResponseHandlerTestUtils() { + } + + public static SdkHttpResponseHandler noOpResponseHandler() { + return new SdkHttpResponseHandler() { + + @Override + public void headersReceived(SdkHttpResponse response) { + + } + + @Override + public void exceptionOccurred(Throwable throwable) { + + } + + @Override + public Object complete() { + return null; + } + + @Override + public void onStream(Publisher publisher) { + + } + }; + } + + public static SdkHttpResponseHandler superSlowResponseHandler(long sleepInMills) { + + return new SdkHttpResponseHandler() { + @Override + public void headersReceived(SdkHttpResponse response) { + + } + + @Override + public void onStream(Publisher publisher) { + + } + + @Override + public void exceptionOccurred(Throwable throwable) { + + } + + @Override + public SdkServiceException complete() { + try { + Thread.sleep(sleepInMills); + } catch (InterruptedException e) { + // ignore + } + return null; + } + }; + } +} diff --git a/core/sdk-core/src/test/java/utils/HttpTestUtils.java b/core/sdk-core/src/test/java/utils/HttpTestUtils.java index cfc911303406..5676edede70d 100644 --- a/core/sdk-core/src/test/java/utils/HttpTestUtils.java +++ b/core/sdk-core/src/test/java/utils/HttpTestUtils.java @@ -16,6 +16,7 @@ package utils; import java.net.URI; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -27,12 +28,15 @@ import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; import software.amazon.awssdk.core.client.config.SdkClientOption; import software.amazon.awssdk.core.internal.client.config.SdkClientConfiguration; +import software.amazon.awssdk.core.internal.http.AmazonAsyncHttpClient; import software.amazon.awssdk.core.internal.http.AmazonSyncHttpClient; +import software.amazon.awssdk.core.internal.http.loader.DefaultSdkAsyncHttpClientBuilder; import software.amazon.awssdk.core.internal.http.loader.DefaultSdkHttpClientBuilder; import software.amazon.awssdk.core.retry.RetryPolicy; import software.amazon.awssdk.core.signer.NoOpSigner; import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.SdkHttpConfigurationOption; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; import software.amazon.awssdk.utils.AttributeMap; public class HttpTestUtils { @@ -41,14 +45,27 @@ public static SdkHttpClient testSdkHttpClient() { AttributeMap.empty().merge(SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS)); } + public static SdkAsyncHttpClient testSdkAsyncHttpClient() { + return new DefaultSdkAsyncHttpClientBuilder().buildWithDefaults( + AttributeMap.empty().merge(SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS)); + } + public static AmazonSyncHttpClient testAmazonHttpClient() { return testClientBuilder().httpClient(testSdkHttpClient()).build(); } + public static AmazonAsyncHttpClient testAsyncHttpClient() { + return new TestAsyncClientBuilder().asyncHttpClient(testSdkAsyncHttpClient()).build(); + } + public static TestClientBuilder testClientBuilder() { return new TestClientBuilder(); } + public static TestAsyncClientBuilder testAsyncClientBuilder() { + return new TestAsyncClientBuilder(); + } + public static SdkClientConfiguration testClientConfiguration() { return SdkClientConfiguration.builder() .option(SdkClientOption.EXECUTION_INTERCEPTORS, new ArrayList<>()) @@ -59,9 +76,8 @@ public static SdkClientConfiguration testClientConfiguration() { .option(SdkAdvancedClientOption.SIGNER, new NoOpSigner()) .option(SdkAdvancedClientOption.USER_AGENT_PREFIX, "") .option(SdkAdvancedClientOption.USER_AGENT_SUFFIX, "") - .option(SdkClientOption.ASYNC_RETRY_EXECUTOR_SERVICE, Executors.newScheduledThreadPool(1)) - .option(SdkAdvancedAsyncClientOption.FUTURE_COMPLETION_EXECUTOR, - Runnable::run) + .option(SdkClientOption.SCHEDULED_EXECUTOR_SERVICE, Executors.newScheduledThreadPool(1)) + .option(SdkAdvancedAsyncClientOption.FUTURE_COMPLETION_EXECUTOR, Runnable::run) .build(); } @@ -108,4 +124,62 @@ private void configureRetryPolicy(SdkClientConfiguration.Builder builder) { } } } + + public static class TestAsyncClientBuilder { + private RetryPolicy retryPolicy; + private SdkAsyncHttpClient asyncHttpClient; + private Duration apiCallTimeout; + private Duration apiCallAttemptTimeout; + private Map additionalHeaders = new HashMap<>(); + + public TestAsyncClientBuilder retryPolicy(RetryPolicy retryPolicy) { + this.retryPolicy = retryPolicy; + return this; + } + + public TestAsyncClientBuilder asyncHttpClient(SdkAsyncHttpClient asyncHttpClient) { + this.asyncHttpClient = asyncHttpClient; + return this; + } + + public TestAsyncClientBuilder additionalHeader(String key, String value) { + this.additionalHeaders.put(key, value); + return this; + } + + public TestAsyncClientBuilder apiCallTimeout(Duration duration) { + this.apiCallTimeout = duration; + return this; + } + + public TestAsyncClientBuilder apiCallAttemptTimeout(Duration timeout) { + this.apiCallAttemptTimeout = timeout; + return this; + } + + public AmazonAsyncHttpClient build() { + SdkAsyncHttpClient asyncHttpClient = this.asyncHttpClient != null ? this.asyncHttpClient : testSdkAsyncHttpClient(); + return new AmazonAsyncHttpClient(testClientConfiguration().toBuilder() + .option(SdkClientOption.ASYNC_HTTP_CLIENT, asyncHttpClient) + .option(SdkClientOption.API_CALL_TIMEOUT, apiCallTimeout) + .option(SdkClientOption.API_CALL_ATTEMPT_TIMEOUT, apiCallAttemptTimeout) + .applyMutation(this::configureRetryPolicy) + .applyMutation(this::configureAdditionalHeaders) + .build()); + } + + private void configureAdditionalHeaders(SdkClientConfiguration.Builder builder) { + Map> headers = + this.additionalHeaders.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> Arrays.asList(e.getValue()))); + + builder.option(SdkClientOption.ADDITIONAL_HTTP_HEADERS, headers); + } + + private void configureRetryPolicy(SdkClientConfiguration.Builder builder) { + if (retryPolicy != null) { + builder.option(SdkClientOption.RETRY_POLICY, retryPolicy); + } + } + } } diff --git a/core/sdk-core/src/test/java/utils/ValidSdkObjects.java b/core/sdk-core/src/test/java/utils/ValidSdkObjects.java index 15487eb0dc42..7f767dad4939 100644 --- a/core/sdk-core/src/test/java/utils/ValidSdkObjects.java +++ b/core/sdk-core/src/test/java/utils/ValidSdkObjects.java @@ -30,16 +30,21 @@ public final class ValidSdkObjects { private ValidSdkObjects() {} public static SdkHttpFullRequest.Builder sdkHttpFullRequest() { + return sdkHttpFullRequest(80); + } + + public static SdkHttpFullRequest.Builder sdkHttpFullRequest(int port) { return SdkHttpFullRequest.builder() .protocol("http") - .host("test.com") - .port(80) + .host("localhost") + .putHeader("Host", "localhost") + .port(port) .method(SdkHttpMethod.GET); } public static Request legacyRequest() { DefaultRequest request = new DefaultRequest<>("testService"); - request.setEndpoint(URI.create("http://test.com")); + request.setEndpoint(URI.create("http://localhost")); return request; } diff --git a/core/sdk-core/src/test/resources/log4j.properties b/core/sdk-core/src/test/resources/log4j.properties index b5a85cb78144..f856d43c3228 100644 --- a/core/sdk-core/src/test/resources/log4j.properties +++ b/core/sdk-core/src/test/resources/log4j.properties @@ -27,11 +27,9 @@ log4j.appender.A1.layout.ConversionPattern=%d [%t] %-5p %c - %m%n #log4j.logger.httpclient.wire=DEBUG # HttpClient 4 Wire Logging -log4j.logger.org.apache.http.wire=INFO -log4j.logger.org.apache.http=DEBUG +# log4j.logger.org.apache.http.wire=INFO +# log4j.logger.org.apache.http=DEBUG # log4j.logger.org.apache.http.wire=DEBUG -# -# log4j.logger.com.amazonaws=DEBUG -# log4j.logger.com.amazonaws.services.s3.internal.Mimetypes=DEBUG +# log4j.logger.software.amazonaws.awssdk=DEBUG diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/RunnableRequest.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/RunnableRequest.java index f943a27a6a16..45ffb6b6c0a4 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/RunnableRequest.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/RunnableRequest.java @@ -86,6 +86,7 @@ public void run() { @Override public void abort() { + log.trace("aborting the request"); if (channel != null) { closeAndRelease(channel); } @@ -221,6 +222,7 @@ private String getMessageForTooManyAcquireOperationsError() { } private static void closeAndRelease(Channel channel) { + log.trace("closing and releasing channel {}", channel.id().asLongText()); RequestContext requestCtx = channel.attr(REQUEST_CONTEXT_KEY).get(); channel.close().addListener(ignored -> requestCtx.channelPool().release(channel)); } diff --git a/services/dynamodb/src/test/resources/log4j.properties b/services/dynamodb/src/test/resources/log4j.properties index 17a016ab91b5..2f94872fe32e 100644 --- a/services/dynamodb/src/test/resources/log4j.properties +++ b/services/dynamodb/src/test/resources/log4j.properties @@ -27,7 +27,7 @@ log4j.appender.A1.layout.ConversionPattern=%d [%t] %-5p %c - %m%n #log4j.logger.httpclient.wire=DEBUG # HttpClient 4 Wire Logging -log4j.logger.org.apache.http.wire=DEBUG +#log4j.logger.org.apache.http.wire=DEBUG #log4j.logger.org.apache.http=DEBUG #log4j.logger.org.apache.http.wire=WARN diff --git a/services/s3/src/it/java/software/amazon/awssdk/services/s3/AsyncGetObjectFaultIntegrationTest.java b/services/s3/src/it/java/software/amazon/awssdk/services/s3/AsyncGetObjectFaultIntegrationTest.java new file mode 100644 index 000000000000..e3871a6acd46 --- /dev/null +++ b/services/s3/src/it/java/software/amazon/awssdk/services/s3/AsyncGetObjectFaultIntegrationTest.java @@ -0,0 +1,124 @@ +/* + * Copyright 2010-2018 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.services.s3; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static software.amazon.awssdk.testutils.service.S3BucketUtils.temporaryBucketName; + +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.core.async.SdkPublisher; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.exception.ApiCallTimeoutException; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +public class AsyncGetObjectFaultIntegrationTest extends S3IntegrationTestBase { + + private static final String BUCKET = temporaryBucketName(AsyncGetObjectFaultIntegrationTest.class); + + private static final String KEY = "some-key"; + + private static S3AsyncClient s3ClientWithTimeout; + + @BeforeClass + public static void setupFixture() { + createBucket(BUCKET); + s3.putObject(PutObjectRequest.builder() + .bucket(BUCKET) + .key(KEY) + .build(), RequestBody.fromString("some contents")); + s3ClientWithTimeout = s3AsyncClientBuilder() + .overrideConfiguration(ClientOverrideConfiguration.builder() + .apiCallTimeout(Duration.ofSeconds(1)) + .build()) + .build(); + } + + @AfterClass + public static void tearDownFixture() { + deleteBucketAndAllContents(BUCKET); + } + + @Test + public void slowTransformer_shouldThrowApiCallTimeoutException() { + SlowResponseTransformer handler = + new SlowResponseTransformer<>(); + assertThatThrownBy(() -> s3ClientWithTimeout.getObject(getObjectRequest(), handler).join()) + .hasCauseInstanceOf(ApiCallTimeoutException.class); + assertThat(handler.currentCallCount()).isEqualTo(1); + } + + private GetObjectRequest getObjectRequest() { + return GetObjectRequest.builder() + .bucket(BUCKET) + .key(KEY) + .build(); + } + + /** + * Wrapper around a {@link AsyncResponseTransformer} that counts how many times it's been invoked. + */ + private static class SlowResponseTransformer + implements AsyncResponseTransformer> { + + private final AtomicInteger callCount = new AtomicInteger(0); + private final AsyncResponseTransformer> delegate; + + private SlowResponseTransformer() { + this.delegate = AsyncResponseTransformer.toBytes(); + } + + public int currentCallCount() { + return callCount.get(); + } + + @Override + public void responseReceived(ResponseT response) { + callCount.incrementAndGet(); + delegate.responseReceived(response); + } + + @Override + public void onStream(SdkPublisher publisher) { + delegate.onStream(publisher); + } + + @Override + public void exceptionOccurred(Throwable throwable) { + delegate.exceptionOccurred(throwable); + } + + @Override + public ResponseBytes complete() { + try { + Thread.sleep(2_000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return delegate.complete(); + } + } +} diff --git a/services/s3/src/it/java/software/amazon/awssdk/services/s3/GetObjectFaultIntegrationTest.java b/services/s3/src/it/java/software/amazon/awssdk/services/s3/GetObjectFaultIntegrationTest.java index fe7fbd6780f2..66434e38f213 100644 --- a/services/s3/src/it/java/software/amazon/awssdk/services/s3/GetObjectFaultIntegrationTest.java +++ b/services/s3/src/it/java/software/amazon/awssdk/services/s3/GetObjectFaultIntegrationTest.java @@ -27,7 +27,7 @@ import org.junit.Test; import software.amazon.awssdk.annotations.ReviewBeforeRelease; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; -import software.amazon.awssdk.core.exception.ClientExecutionTimeoutException; +import software.amazon.awssdk.core.exception.ApiCallTimeoutException; import software.amazon.awssdk.core.exception.NonRetryableException; import software.amazon.awssdk.core.exception.RetryableException; import software.amazon.awssdk.core.exception.SdkClientException; @@ -101,7 +101,7 @@ public void slowHandlerIsInterrupted() throws Exception { return null; }); assertThatThrownBy(() -> s3ClientWithTimeout.getObject(getObjectRequest(), handler)) - .isInstanceOf(ClientExecutionTimeoutException.class); + .isInstanceOf(ApiCallTimeoutException.class); assertThat(handler.currentCallCount()).isEqualTo(1); } @@ -124,13 +124,13 @@ public void slowHandlerIsInterrupted_SetsInterruptFlag() throws Exception { return null; }); assertThatThrownBy(() -> s3ClientWithTimeout.getObject(getObjectRequest(), handler)) - .isInstanceOf(ClientExecutionTimeoutException.class); + .isInstanceOf(ApiCallTimeoutException.class); assertThat(handler.currentCallCount()).isEqualTo(1); } /** * If a response handler does not preserve the interrupt status or throw an {@link InterruptedException} then - * we can't translate the exception to a {@link ClientExecutionTimeoutException}. + * we can't translate the exception to a {@link ApiCallTimeoutException}. */ @Test public void handlerSquashsInterrupt_DoesNotThrowClientTimeoutException() throws Exception { @@ -145,7 +145,7 @@ public void handlerSquashsInterrupt_DoesNotThrowClientTimeoutException() throws return null; }); assertThatThrownBy(() -> s3ClientWithTimeout.getObject(getObjectRequest(), handler)) - .isNotInstanceOf(ClientExecutionTimeoutException.class); + .isNotInstanceOf(ApiCallTimeoutException.class); assertThat(handler.currentCallCount()).isEqualTo(1); } diff --git a/services/s3/src/test/resources/log4j.properties b/services/s3/src/test/resources/log4j.properties index 7ea15a935c74..528a353a0203 100644 --- a/services/s3/src/test/resources/log4j.properties +++ b/services/s3/src/test/resources/log4j.properties @@ -30,4 +30,4 @@ log4j.appender.A1.layout.ConversionPattern=%d [%t] %-5p %c - %m%n #log4j.logger.org.apache.http.wire=DEBUG #log4j.logger.org.apache.http=DEBUG #log4j.logger.org.apache.http.wire=WARN -#log4j.logger.software.amazon.awssdk.request=DEBUG +#log4j.logger.software.amazon.awssdk=DEBUG diff --git a/test/protocol-tests-core/src/main/java/software/amazon/awssdk/protocol/wiremock/WireMockUtils.java b/test/protocol-tests-core/src/main/java/software/amazon/awssdk/protocol/wiremock/WireMockUtils.java index 215aa3a77bbc..6610e8ac6ef9 100644 --- a/test/protocol-tests-core/src/main/java/software/amazon/awssdk/protocol/wiremock/WireMockUtils.java +++ b/test/protocol-tests-core/src/main/java/software/amazon/awssdk/protocol/wiremock/WireMockUtils.java @@ -15,6 +15,8 @@ package software.amazon.awssdk.protocol.wiremock; +import static com.github.tomakehurst.wiremock.client.WireMock.anyRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; import static com.github.tomakehurst.wiremock.client.WireMock.findAll; import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; @@ -22,6 +24,7 @@ import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.http.RequestMethod; +import com.github.tomakehurst.wiremock.junit.WireMockRule; import com.github.tomakehurst.wiremock.matching.RequestPatternBuilder; import com.github.tomakehurst.wiremock.verification.LoggedRequest; import java.util.List; @@ -57,4 +60,8 @@ public static List findAllLoggedRequests() { new RequestPatternBuilder(RequestMethod.ANY, urlMatching(".*"))); return requests; } + + public static void verifyRequestCount(int expectedCount, WireMockRule wireMock) { + wireMock.verify(expectedCount, anyRequestedFor(anyUrl())); + } } diff --git a/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/retry/AsyncAwsJsonRetryTest.java b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/retry/AsyncAwsJsonRetryTest.java new file mode 100644 index 000000000000..967251e8bb58 --- /dev/null +++ b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/retry/AsyncAwsJsonRetryTest.java @@ -0,0 +1,158 @@ +/* + * Copyright 2010-2018 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.protocol.tests.retry; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import com.github.tomakehurst.wiremock.stubbing.Scenario; +import java.net.URI; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.retry.RetryPolicy; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.protocoljsonrpc.ProtocolJsonRpcAsyncClient; +import software.amazon.awssdk.services.protocoljsonrpc.model.AllTypesRequest; +import software.amazon.awssdk.services.protocoljsonrpc.model.AllTypesResponse; +import software.amazon.awssdk.services.protocoljsonrpc.model.ProtocolJsonRpcException; + +public class AsyncAwsJsonRetryTest { + + private static final String PATH = "/"; + private static final String JSON_BODY = "{\"StringMember\":\"foo\"}"; + + @Rule + public WireMockRule wireMock = new WireMockRule(0); + + private ProtocolJsonRpcAsyncClient client; + + @Before + public void setupClient() { + client = ProtocolJsonRpcAsyncClient.builder() + .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create + ("akid", "skid"))) + .region(Region.US_EAST_1) + .endpointOverride(URI.create("http://localhost:" + wireMock.port())) + .build(); + } + + @Test + public void shouldRetryOn500() { + stubFor(post(urlEqualTo(PATH)) + .inScenario("retry at 500") + .whenScenarioStateIs(Scenario.STARTED) + .willSetStateTo("first attempt") + .willReturn(aResponse() + .withStatus(500))); + + stubFor(post(urlEqualTo(PATH)) + .inScenario("retry at 500") + .whenScenarioStateIs("first attempt") + .willSetStateTo("second attempt") + .willReturn(aResponse() + .withStatus(200) + .withBody(JSON_BODY))); + + AllTypesResponse allTypesResponse = client.allTypes(AllTypesRequest.builder().build()).join(); + assertThat(allTypesResponse).isNotNull(); + } + + @Test + public void shouldRetryOnRetryableAwsErrorCode() { + stubFor(post(urlEqualTo(PATH)) + .inScenario("retry at PriorRequestNotComplete") + .whenScenarioStateIs(Scenario.STARTED) + .willSetStateTo("first attempt") + .willReturn(aResponse() + .withStatus(400) + .withHeader("x-amzn-ErrorType", "PriorRequestNotComplete") + .withBody("\"{\"__type\":\"PriorRequestNotComplete\",\"message\":\"Blah " + + "error\"}\""))); + + stubFor(post(urlEqualTo(PATH)) + .inScenario("retry at PriorRequestNotComplete") + .whenScenarioStateIs("first attempt") + .willSetStateTo("second attempt") + .willReturn(aResponse() + .withStatus(200) + .withBody(JSON_BODY))); + + AllTypesResponse allTypesResponse = client.allTypes(AllTypesRequest.builder().build()).join(); + assertThat(allTypesResponse).isNotNull(); + } + + @Test + public void shouldRetryOnAwsThrottlingErrorCode() { + stubFor(post(urlEqualTo(PATH)) + .inScenario("retry at SlowDown") + .whenScenarioStateIs(Scenario.STARTED) + .willSetStateTo("first attempt") + .willReturn(aResponse() + .withStatus(400) + .withHeader("x-amzn-ErrorType", "SlowDown") + .withBody("\"{\"__type\":\"SlowDown\",\"message\":\"Blah " + + "error\"}\""))); + + stubFor(post(urlEqualTo(PATH)) + .inScenario("retry at SlowDown") + .whenScenarioStateIs("first attempt") + .willSetStateTo("second attempt") + .willReturn(aResponse() + .withStatus(200) + .withBody(JSON_BODY))); + + AllTypesResponse allTypesResponse = client.allTypes(AllTypesRequest.builder().build()).join(); + assertThat(allTypesResponse).isNotNull(); + } + + @Test + public void retryPolicyNone_shouldNotRetry() { + stubFor(post(urlEqualTo(PATH)) + .inScenario("retry at 500") + .whenScenarioStateIs(Scenario.STARTED) + .willSetStateTo("first attempt") + .willReturn(aResponse() + .withStatus(500))); + + stubFor(post(urlEqualTo(PATH)) + .inScenario("retry at 500") + .whenScenarioStateIs("first attempt") + .willSetStateTo("second attempt") + .willReturn(aResponse() + .withStatus(200) + .withBody(JSON_BODY))); + + ProtocolJsonRpcAsyncClient clientWithNoRetry = + ProtocolJsonRpcAsyncClient.builder() + .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", + "skid"))) + .region(Region.US_EAST_1) + .endpointOverride(URI.create("http://localhost:" + wireMock.port())) + .overrideConfiguration(c -> c.retryPolicy(RetryPolicy.none())) + .build(); + + assertThatThrownBy(() -> clientWithNoRetry.allTypes(AllTypesRequest.builder().build()).join()) + .hasCauseInstanceOf(ProtocolJsonRpcException.class); + } +} diff --git a/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/timeout/AsyncApiCallAttemptsTimeoutTest.java b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/timeout/AsyncApiCallAttemptsTimeoutTest.java new file mode 100644 index 000000000000..2e4309ec4a25 --- /dev/null +++ b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/timeout/AsyncApiCallAttemptsTimeoutTest.java @@ -0,0 +1,283 @@ +/* + * Copyright 2010-2018 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.protocol.tests.timeout; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static software.amazon.awssdk.protocol.wiremock.WireMockUtils.verifyRequestCount; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import com.github.tomakehurst.wiremock.stubbing.Scenario; +import java.net.URI; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.core.async.SdkPublisher; +import software.amazon.awssdk.core.exception.ApiCallAttemptTimeoutException; +import software.amazon.awssdk.core.retry.RetryPolicy; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonAsyncClient; +import software.amazon.awssdk.services.protocolrestjson.model.AllTypesResponse; +import software.amazon.awssdk.services.protocolrestjson.model.ProtocolRestJsonException; +import software.amazon.awssdk.services.protocolrestjson.model.StreamingOutputOperationRequest; +import software.amazon.awssdk.services.protocolrestjson.model.StreamingOutputOperationResponse; +import software.amazon.awssdk.utils.builder.SdkBuilder; + +/** + * Test apiCallAttemptTimeout feature for asynchronous operations. + */ +public class AsyncApiCallAttemptsTimeoutTest { + @Rule + public WireMockRule wireMock = new WireMockRule(0); + + private static final String STREAMING_OUTPUT_PATH = "/2016-03-11/streamingOutputOperation"; + private ProtocolRestJsonAsyncClient client; + private ProtocolRestJsonAsyncClient clientWithRetry; + private static final int API_CALL_ATTEMPT_TIMEOUT = 800; + private static final int DELAY_BEFORE_API_CALL_ATTEMPT_TIMEOUT = 100; + private static final int DELAY_AFTER_API_CALL_ATTEMPT_TIMEOUT = 1000; + + @Before + public void setup() { + client = ProtocolRestJsonAsyncClient.builder() + .region(Region.US_WEST_1) + .endpointOverride(URI.create("http://localhost:" + wireMock.port())) + .credentialsProvider(() -> AwsBasicCredentials.create("akid", "skid")) + .overrideConfiguration(b -> b.apiCallAttemptTimeout(Duration.ofMillis(API_CALL_ATTEMPT_TIMEOUT)) + .retryPolicy(RetryPolicy.none())) + .build(); + + clientWithRetry = ProtocolRestJsonAsyncClient.builder() + .region(Region.US_WEST_1) + .endpointOverride(URI.create("http://localhost:" + wireMock.port())) + .credentialsProvider(() -> AwsBasicCredentials.create("akid", "skid")) + .overrideConfiguration(b -> b.apiCallAttemptTimeout(Duration.ofMillis(API_CALL_ATTEMPT_TIMEOUT)) + .retryPolicy(RetryPolicy.builder() + .numRetries(1) + .build())) + .build(); + + } + + @Test + public void nonstreamingOperation200_finishedWithinTime_shouldSucceed() throws InterruptedException { + stubFor(post(anyUrl()) + .willReturn(aResponse().withStatus(200).withBody("{}").withFixedDelay(DELAY_BEFORE_API_CALL_ATTEMPT_TIMEOUT))); + CompletableFuture allTypesResponseCompletableFuture = client.allTypes(SdkBuilder::build); + + AllTypesResponse response = allTypesResponseCompletableFuture.join(); + assertThat(response).isNotNull(); + } + + @Test + public void nonstreamingOperation200_notFinishedWithinTime_shouldTimeout() { + stubFor(post(anyUrl()) + .willReturn(aResponse().withStatus(200).withBody("{}").withFixedDelay(DELAY_AFTER_API_CALL_ATTEMPT_TIMEOUT))); + CompletableFuture allTypesResponseCompletableFuture = client.allTypes(SdkBuilder::build); + + assertThatThrownBy(allTypesResponseCompletableFuture::join).hasCauseInstanceOf(ApiCallAttemptTimeoutException.class); + } + + @Test + public void nonstreamingOperation500_finishedWithinTime_shouldNotTimeout() { + stubFor(post(anyUrl()) + .willReturn(aResponse().withStatus(500) + .withHeader("x-amzn-ErrorType", "EmptyModeledException") + .withFixedDelay(DELAY_BEFORE_API_CALL_ATTEMPT_TIMEOUT))); + CompletableFuture allTypesResponseCompletableFuture = client.allTypes(SdkBuilder::build); + + assertThatThrownBy(allTypesResponseCompletableFuture::join).hasCauseInstanceOf(ProtocolRestJsonException.class); + } + + @Test + public void nonstreamingOperation500_notFinishedWithinTime_shouldTimeout() { + stubFor(post(anyUrl()) + .willReturn(aResponse().withStatus(500).withFixedDelay(DELAY_AFTER_API_CALL_ATTEMPT_TIMEOUT))); + CompletableFuture allTypesResponseCompletableFuture = client.allTypes(SdkBuilder::build); + + assertThatThrownBy(allTypesResponseCompletableFuture::join).hasCauseInstanceOf(ApiCallAttemptTimeoutException.class); + } + + @Test + public void streamingOperation_finishedWithinTime_shouldSucceed() { + stubFor(post(urlPathEqualTo(STREAMING_OUTPUT_PATH)) + .willReturn(aResponse().withStatus(200).withBody("test").withFixedDelay(DELAY_BEFORE_API_CALL_ATTEMPT_TIMEOUT))); + + ResponseBytes response = + client.streamingOutputOperation(StreamingOutputOperationRequest.builder().build(), + AsyncResponseTransformer.toBytes()).join(); + + byte[] arrayCopy = response.asByteArray(); + assertThat(arrayCopy).containsExactly('t', 'e', 's', 't'); + } + + @Test + public void streamingOperation_notFinishedWithinTime_shouldTimeout() { + stubFor(post(urlPathEqualTo(STREAMING_OUTPUT_PATH)) + .willReturn(aResponse().withStatus(200).withBody("{}").withFixedDelay(DELAY_AFTER_API_CALL_ATTEMPT_TIMEOUT))); + + assertThatThrownBy(() -> + client.streamingOutputOperation(StreamingOutputOperationRequest.builder().build(), + AsyncResponseTransformer.toBytes()).join()) + .hasCauseInstanceOf(ApiCallAttemptTimeoutException.class); + } + + @Test + public void firstAttemptTimeout_retryFinishWithInTime_shouldNotTimeout() { + stubFor(post(anyUrl()) + .inScenario("timed out in the first attempt") + .whenScenarioStateIs(Scenario.STARTED) + .willSetStateTo("first attempt") + .willReturn(aResponse() + .withStatus(200).withFixedDelay(DELAY_AFTER_API_CALL_ATTEMPT_TIMEOUT))); + + stubFor(post(anyUrl()) + .inScenario("timed out in the first attempt") + .whenScenarioStateIs("first attempt") + .willSetStateTo("second attempt") + .willReturn(aResponse() + .withStatus(200) + .withBody("{}") + .withFixedDelay(DELAY_BEFORE_API_CALL_ATTEMPT_TIMEOUT))); + CompletableFuture allTypesResponseCompletableFuture = clientWithRetry.allTypes(SdkBuilder::build); + + AllTypesResponse response = allTypesResponseCompletableFuture.join(); + assertThat(response).isNotNull(); + verifyRequestCount(2, wireMock); + } + + @Test + public void firstAttemptTimeout_retryFinishWithInTime500_shouldNotTimeout() { + stubFor(post(anyUrl()) + .inScenario("timed out in the first attempt") + .whenScenarioStateIs(Scenario.STARTED) + .willSetStateTo("first attempt") + .willReturn(aResponse() + .withStatus(200).withFixedDelay(DELAY_AFTER_API_CALL_ATTEMPT_TIMEOUT))); + + stubFor(post(anyUrl()) + .inScenario("timed out in the first attempt") + .whenScenarioStateIs("first attempt") + .willSetStateTo("second attempt") + .willReturn(aResponse() + .withStatus(500) + .withFixedDelay(DELAY_BEFORE_API_CALL_ATTEMPT_TIMEOUT))); + CompletableFuture allTypesResponseCompletableFuture = clientWithRetry.allTypes(SdkBuilder::build); + assertThatThrownBy(allTypesResponseCompletableFuture::join).hasCauseInstanceOf(ProtocolRestJsonException.class); + verifyRequestCount(2, wireMock); + } + + @Test + public void allAttemtsNotFinishedWithinTime_shouldTimeout() { + stubFor(post(anyUrl()) + .inScenario("timed out in both attempts") + .whenScenarioStateIs(Scenario.STARTED) + .willSetStateTo("first attempt") + .willReturn(aResponse() + .withStatus(200).withFixedDelay(DELAY_AFTER_API_CALL_ATTEMPT_TIMEOUT))); + + stubFor(post(anyUrl()) + .inScenario("timed out in both attempts") + .whenScenarioStateIs("first attempt") + .willSetStateTo("second attempt") + .willReturn(aResponse() + .withStatus(200) + .withBody("{}") + .withFixedDelay(DELAY_AFTER_API_CALL_ATTEMPT_TIMEOUT))); + CompletableFuture allTypesResponseCompletableFuture = clientWithRetry.allTypes(SdkBuilder::build); + assertThatThrownBy(allTypesResponseCompletableFuture::join).hasCauseInstanceOf(ApiCallAttemptTimeoutException.class); + } + + @Test + public void increaseTimeoutInRequestOverrideConfig_shouldTakePrecedence() { + stubFor(post(anyUrl()) + .willReturn(aResponse().withStatus(200).withBody("{}").withFixedDelay(DELAY_AFTER_API_CALL_ATTEMPT_TIMEOUT))); + CompletableFuture allTypesResponseCompletableFuture = + client.allTypes(b -> b.overrideConfiguration(c -> c.apiCallAttemptTimeout( + Duration.ofMillis(DELAY_AFTER_API_CALL_ATTEMPT_TIMEOUT + 1000)))); + + AllTypesResponse response = allTypesResponseCompletableFuture.join(); + assertThat(response).isNotNull(); + } + + @Test + public void streamingOperation_slowTransformer_shouldThrowApiCallAttemptTimeoutException() { + stubFor(post(anyUrl()) + .willReturn(aResponse() + .withStatus(200).withFixedDelay(DELAY_BEFORE_API_CALL_ATTEMPT_TIMEOUT))); + + CompletableFuture> future = client + .streamingOutputOperation( + StreamingOutputOperationRequest.builder().build(), new SlowResponseTransformer<>()); + + assertThatThrownBy(future::join) + .hasCauseInstanceOf(ApiCallAttemptTimeoutException.class); + } + + + private static class SlowResponseTransformer + implements AsyncResponseTransformer> { + + private final AtomicInteger callCount = new AtomicInteger(0); + private final AsyncResponseTransformer> delegate; + + private SlowResponseTransformer() { + this.delegate = AsyncResponseTransformer.toBytes(); + } + + public int currentCallCount() { + return callCount.get(); + } + + @Override + public void responseReceived(ResponseT response) { + callCount.incrementAndGet(); + delegate.responseReceived(response); + } + + @Override + public void onStream(SdkPublisher publisher) { + delegate.onStream(publisher); + } + + @Override + public void exceptionOccurred(Throwable throwable) { + delegate.exceptionOccurred(throwable); + } + + @Override + public ResponseBytes complete() { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return delegate.complete(); + } + } +} diff --git a/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/timeout/AsyncApiCallTimeoutTest.java b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/timeout/AsyncApiCallTimeoutTest.java new file mode 100644 index 000000000000..0e05119ea962 --- /dev/null +++ b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/timeout/AsyncApiCallTimeoutTest.java @@ -0,0 +1,203 @@ +/* + * Copyright 2010-2018 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.protocol.tests.timeout; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static software.amazon.awssdk.protocol.wiremock.WireMockUtils.verifyRequestCount; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import com.github.tomakehurst.wiremock.stubbing.Scenario; +import java.net.URI; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.core.exception.ApiCallTimeoutException; +import software.amazon.awssdk.core.retry.RetryPolicy; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonAsyncClient; +import software.amazon.awssdk.services.protocolrestjson.model.AllTypesResponse; +import software.amazon.awssdk.services.protocolrestjson.model.ProtocolRestJsonException; +import software.amazon.awssdk.services.protocolrestjson.model.StreamingOutputOperationRequest; +import software.amazon.awssdk.services.protocolrestjson.model.StreamingOutputOperationResponse; +import software.amazon.awssdk.utils.builder.SdkBuilder; + +/** + * Test apiCallTimeout feature for asynchronous operations. + */ +public class AsyncApiCallTimeoutTest { + @Rule + public WireMockRule wireMock = new WireMockRule(0); + + private static final String STREAMING_OUTPUT_PATH = "/2016-03-11/streamingOutputOperation"; + private static final int TIMEOUT = 1000; + private static final int DELAY_BEFORE_TIMEOUT = 100; + private static final int DELAY_AFTER_TIMEOUT = 1200; + private ProtocolRestJsonAsyncClient client; + private ProtocolRestJsonAsyncClient clientWithRetry; + + @Before + public void setup() { + client = ProtocolRestJsonAsyncClient.builder() + .region(Region.US_WEST_1) + .endpointOverride(URI.create("http://localhost:" + wireMock.port())) + .credentialsProvider(() -> AwsBasicCredentials.create("akid", "skid")) + .overrideConfiguration(b -> b.apiCallTimeout(Duration.ofMillis(TIMEOUT)) + .retryPolicy(RetryPolicy.none())) + .build(); + + clientWithRetry = ProtocolRestJsonAsyncClient.builder() + .region(Region.US_WEST_1) + .endpointOverride(URI.create("http://localhost:" + wireMock.port())) + .credentialsProvider(() -> AwsBasicCredentials.create("akid", "skid")) + .overrideConfiguration(b -> b.apiCallTimeout(Duration.ofMillis(TIMEOUT)) + .retryPolicy(RetryPolicy.builder().numRetries(1).build())) + .build(); + } + + @Test + public void nonstreamingOperation_finishedWithinTime_shouldNotTimeout() { + stubFor(post(anyUrl()) + .willReturn(aResponse().withStatus(200).withBody("{}").withFixedDelay(DELAY_BEFORE_TIMEOUT))); + CompletableFuture allTypesResponseCompletableFuture = client.allTypes(SdkBuilder::build); + + AllTypesResponse response = allTypesResponseCompletableFuture.join(); + assertThat(response).isNotNull(); + } + + @Test + public void nonstreamingOperation_notFinishedWithinTime_shouldTimeout() { + stubFor(post(anyUrl()) + .willReturn(aResponse().withStatus(200).withBody("{}").withFixedDelay(DELAY_AFTER_TIMEOUT))); + CompletableFuture allTypesResponseCompletableFuture = client.allTypes(SdkBuilder::build); + + assertThatThrownBy(allTypesResponseCompletableFuture::join).hasCauseInstanceOf(ApiCallTimeoutException.class); + } + + @Test + public void nonstreamingOperation500_notFinishedWithinTime_shouldTimeout() { + stubFor(post(anyUrl()) + .willReturn(aResponse().withStatus(500).withFixedDelay(DELAY_AFTER_TIMEOUT))); + CompletableFuture allTypesResponseCompletableFuture = client.allTypes(SdkBuilder::build); + + assertThatThrownBy(allTypesResponseCompletableFuture::join).hasCauseInstanceOf(ApiCallTimeoutException.class); + } + + @Test + public void nonstreamingOperation500_finishedWithinTime_shouldNotTimeout() { + stubFor(post(anyUrl()) + .willReturn(aResponse().withStatus(500).withFixedDelay(DELAY_BEFORE_TIMEOUT))); + CompletableFuture allTypesResponseCompletableFuture = client.allTypes(SdkBuilder::build); + + assertThatThrownBy(allTypesResponseCompletableFuture::join).hasCauseInstanceOf(ProtocolRestJsonException.class); + } + + @Test + public void nonstreamingOperation_retrySucceeded_FinishedWithinTime_shouldNotTimeout() { + + stubFor(post(anyUrl()) + .inScenario("retry at 500") + .whenScenarioStateIs(Scenario.STARTED) + .willSetStateTo("first attempt") + .willReturn(aResponse() + .withStatus(500).withFixedDelay(DELAY_BEFORE_TIMEOUT))); + + stubFor(post(anyUrl()) + .inScenario("retry at 500") + .whenScenarioStateIs("first attempt") + .willSetStateTo("second attempt") + .willReturn(aResponse() + .withStatus(200) + .withBody("{}").withFixedDelay(DELAY_BEFORE_TIMEOUT))); + + + CompletableFuture allTypesResponseCompletableFuture = clientWithRetry.allTypes(SdkBuilder::build); + + AllTypesResponse response = allTypesResponseCompletableFuture.join(); + assertThat(response).isNotNull(); + } + + @Test + public void nonstreamingOperation_retryWouldSucceed_notFinishedWithinTime_shouldTimeout() { + + stubFor(post(anyUrl()) + .inScenario("retry at 500") + .whenScenarioStateIs(Scenario.STARTED) + .willSetStateTo("first attempt") + .willReturn(aResponse() + .withStatus(500).withFixedDelay(DELAY_BEFORE_TIMEOUT))); + + stubFor(post(anyUrl()) + .inScenario("retry at 500") + .whenScenarioStateIs("first attempt") + .willSetStateTo("second attempt") + .willReturn(aResponse() + .withStatus(200) + .withBody("{}").withFixedDelay(DELAY_AFTER_TIMEOUT))); + + + CompletableFuture allTypesResponseCompletableFuture = clientWithRetry.allTypes(SdkBuilder::build); + + assertThatThrownBy(allTypesResponseCompletableFuture::join).hasCauseInstanceOf(ApiCallTimeoutException.class); + verifyRequestCount(2, wireMock); + } + + @Test + public void streamingOperation_finishedWithinTime_shouldNotTimeout() { + stubFor(post(urlPathEqualTo(STREAMING_OUTPUT_PATH)) + .willReturn(aResponse().withStatus(200).withBody("test").withFixedDelay(DELAY_BEFORE_TIMEOUT))); + + ResponseBytes response = + client.streamingOutputOperation(StreamingOutputOperationRequest.builder().build(), + AsyncResponseTransformer.toBytes()).join(); + + byte[] arrayCopy = response.asByteArray(); + assertThat(arrayCopy).containsExactly('t', 'e', 's', 't'); + } + + @Test + public void streamingOperation_notFinishedWithinTime_shouldTimeout() { + stubFor(post(urlPathEqualTo(STREAMING_OUTPUT_PATH)) + .willReturn(aResponse().withStatus(200).withBody("{}").withFixedDelay(DELAY_AFTER_TIMEOUT))); + + assertThatThrownBy(() -> + client.streamingOutputOperation(StreamingOutputOperationRequest.builder().build(), + AsyncResponseTransformer.toBytes()).join()) + .hasRootCauseInstanceOf(ApiCallTimeoutException.class); + } + + @Test + public void increaseTimeoutInRequestOverrideConfig_shouldTakePrecedence() { + stubFor(post(anyUrl()) + .willReturn(aResponse().withStatus(200).withBody("{}").withFixedDelay(DELAY_AFTER_TIMEOUT))); + CompletableFuture allTypesResponseCompletableFuture = + client.allTypes(b -> b.overrideConfiguration(c -> c.apiCallTimeout(Duration.ofMillis(DELAY_AFTER_TIMEOUT + 1000)))); + + AllTypesResponse response = allTypesResponseCompletableFuture.join(); + assertThat(response).isNotNull(); + } + +} diff --git a/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/timeout/AsyncTimeoutTest.java b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/timeout/AsyncTimeoutTest.java new file mode 100644 index 000000000000..d42bc9c09f57 --- /dev/null +++ b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/timeout/AsyncTimeoutTest.java @@ -0,0 +1,93 @@ +/* + * Copyright 2010-2018 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.protocol.tests.timeout; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import java.net.URI; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.core.exception.ApiCallAttemptTimeoutException; +import software.amazon.awssdk.core.retry.RetryPolicy; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonAsyncClient; +import software.amazon.awssdk.services.protocolrestjson.model.AllTypesResponse; +import software.amazon.awssdk.utils.builder.SdkBuilder; + +/** + * Test timeout feature for asynchronous operations when both apiCallTimeout and apiCallAttemptTimeout are enabled. + */ +public class AsyncTimeoutTest { + @Rule + public WireMockRule wireMock = new WireMockRule(0); + + private static final int API_CALL_TIMEOUT = 1200; + private static final int API_CALL_ATTEMPT_TIMEOUT = 1000; + + private static final int DELAY_BEFORE_API_CALL_ATTEMPT_TIMEOUT = 100; + private static final int DELAY_AFTER_API_CALL_ATTEMPT_TIMEOUT = 1100; + private ProtocolRestJsonAsyncClient client; + + @Before + public void setup() { + client = ProtocolRestJsonAsyncClient.builder() + .region(Region.US_WEST_1) + .endpointOverride(URI.create("http://localhost:" + wireMock.port())) + .credentialsProvider(() -> AwsBasicCredentials.create("akid", "skid")) + .overrideConfiguration( + b -> b.apiCallTimeout(Duration.ofMillis(API_CALL_TIMEOUT)) + .apiCallAttemptTimeout(Duration.ofMillis + (API_CALL_ATTEMPT_TIMEOUT)) + .retryPolicy(RetryPolicy.none())) + .build(); + } + + @Test + public void attemptsTimeout_shouldThrowApiCallAttemptTimeoutException() { + stubFor(post(anyUrl()) + .willReturn(aResponse() + .withStatus(200) + .withBody("{}") + .withFixedDelay(DELAY_AFTER_API_CALL_ATTEMPT_TIMEOUT))); + + CompletableFuture allTypesResponseCompletableFuture = client.allTypes(SdkBuilder::build); + assertThatThrownBy(allTypesResponseCompletableFuture::join) + .hasCauseInstanceOf(ApiCallAttemptTimeoutException.class); + } + + @Test + public void attemptFinishWithinTime_shouldSucceed() { + stubFor(post(anyUrl()) + .willReturn(aResponse() + .withStatus(200) + .withBody("{}") + .withFixedDelay(DELAY_BEFORE_API_CALL_ATTEMPT_TIMEOUT))); + CompletableFuture allTypesResponseCompletableFuture = client.allTypes(SdkBuilder::build); + assertThat(allTypesResponseCompletableFuture.join()).isNotNull(); + } + + +} diff --git a/test/protocol-tests/src/test/resources/log4j.properties b/test/protocol-tests/src/test/resources/log4j.properties index 24dada98f815..43cfa2665f5f 100644 --- a/test/protocol-tests/src/test/resources/log4j.properties +++ b/test/protocol-tests/src/test/resources/log4j.properties @@ -31,4 +31,3 @@ log4j.appender.A1.layout.ConversionPattern=%d [%t] %-5p %c - %m%n # log4j.logger.org.apache.http=DEBUG # log4j.logger.org.apache.http.wire=DEBUG #log4j.logger.software.amazon.awssdk=DEBUG -#log4j.logger.software.amazon.awssdk.request=DEBUG diff --git a/utils/src/main/java/software/amazon/awssdk/utils/OptionalUtils.java b/utils/src/main/java/software/amazon/awssdk/utils/OptionalUtils.java index b79744e52566..c98ebff7be97 100644 --- a/utils/src/main/java/software/amazon/awssdk/utils/OptionalUtils.java +++ b/utils/src/main/java/software/amazon/awssdk/utils/OptionalUtils.java @@ -51,4 +51,12 @@ public static Optional firstPresent(Optional firstValue, Supplier Optional firstPresent(Optional firstValue, Supplier fallbackValue) { + if (firstValue.isPresent()) { + return firstValue; + } + + return Optional.ofNullable(fallbackValue.get()); + } } diff --git a/utils/src/main/java/software/amazon/awssdk/utils/Validate.java b/utils/src/main/java/software/amazon/awssdk/utils/Validate.java index 6818eb8b2efa..0986263639ef 100644 --- a/utils/src/main/java/software/amazon/awssdk/utils/Validate.java +++ b/utils/src/main/java/software/amazon/awssdk/utils/Validate.java @@ -630,6 +630,22 @@ public static Duration isPositive(Duration duration, String fieldName) { return duration; } + /** + * Asserts that the given duration is positive (non-negative and non-zero) or null. + * + * @param duration Number to validate + * @param fieldName Field name to display in exception message if not positive. + * @return Duration if positive or null. + */ + public static Duration isPositiveOrNull(Duration duration, String fieldName) { + if (duration == null) { + return null; + } + + return isPositive(duration, fieldName); + } + + /** * Asserts that the given duration is positive (non-negative and non-zero). *