diff --git a/.changes/next-release/feature-AWSCommonRuntimeClient-0306e36.json b/.changes/next-release/feature-AWSCommonRuntimeClient-0306e36.json new file mode 100644 index 000000000000..8123a7020f1b --- /dev/null +++ b/.changes/next-release/feature-AWSCommonRuntimeClient-0306e36.json @@ -0,0 +1,5 @@ +{ + "category": "AWS Common Runtime HTTP Client", + "type": "feature", + "description": "This release includes the preview release of the AWS Common Runtime HTTP client for the AWS SDK for Java v2. The code can be found in the `aws-crt-client` module." +} diff --git a/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml b/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml index 1c20f7d12d40..b12d46ab5ca5 100644 --- a/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml +++ b/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml @@ -180,4 +180,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/http-client-spi/src/main/java/software/amazon/awssdk/http/Header.java b/http-client-spi/src/main/java/software/amazon/awssdk/http/Header.java index 10ace60280d5..4e23000e408f 100644 --- a/http-client-spi/src/main/java/software/amazon/awssdk/http/Header.java +++ b/http-client-spi/src/main/java/software/amazon/awssdk/http/Header.java @@ -33,6 +33,12 @@ public final class Header { public static final String CHUNKED = "chunked"; + public static final String HOST = "Host"; + + public static final String CONNECTION = "Connection"; + + public static final String KEEP_ALIVE_VALUE = "keep-alive"; + private Header() { } diff --git a/http-clients/aws-crt-client/pom.xml b/http-clients/aws-crt-client/pom.xml new file mode 100644 index 000000000000..63ea12f91358 --- /dev/null +++ b/http-clients/aws-crt-client/pom.xml @@ -0,0 +1,208 @@ + + + + + + + http-clients + software.amazon.awssdk + 2.14.13-SNAPSHOT + + 4.0.0 + + aws-crt-client + AWS Java SDK :: HTTP Clients :: AWS Common Runtime Client + jar + ${awsjavasdk.version}-PREVIEW + + + ${project.parent.version} + 1.8 + + + + + + software.amazon.awssdk + bom-internal + ${awsjavasdk.version} + pom + import + + + + + + + + software.amazon.awssdk.crt + aws-crt + ${awscrt.version} + + + + + software.amazon.awssdk + annotations + ${awsjavasdk.version} + + + software.amazon.awssdk + http-client-spi + ${awsjavasdk.version} + + + software.amazon.awssdk + utils + ${awsjavasdk.version} + + + + + com.github.tomakehurst + wiremock + test + + + org.apache.commons + commons-lang3 + test + + + junit + junit + test + + + org.mockito + mockito-core + test + + + org.assertj + assertj-core + test + + + org.reactivestreams + reactive-streams-tck + test + + + org.slf4j + slf4j-log4j12 + test + + + log4j + log4j + test + + + software.amazon.awssdk + http-client-tests + ${awsjavasdk.version} + test + + + software.amazon.awssdk + sdk-core + ${awsjavasdk.version} + test + + + software.amazon.awssdk + regions + ${awsjavasdk.version} + test + + + software.amazon.awssdk + s3 + ${awsjavasdk.version} + test + + + software.amazon.awssdk + kms + ${awsjavasdk.version} + test + + + software.amazon.awssdk + auth + ${awsjavasdk.version} + test + + + service-test-utils + software.amazon.awssdk + ${awsjavasdk.version} + test + + + commons-codec + commons-codec + ${commons-codec.verion} + test + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven.surefire.version} + + + + junit + false + + + 1 + + + + org.apache.maven.surefire + surefire-junit47 + ${maven.surefire.version} + + + org.apache.maven.surefire + surefire-testng + ${maven.surefire.version} + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + software.amazon.awssdk.http.crt + + + + + + + diff --git a/http-clients/aws-crt-client/src/it/java/software/amazon/awssdk/http/crt/AwsCrtClientCallingPatternIntegrationTest.java b/http-clients/aws-crt-client/src/it/java/software/amazon/awssdk/http/crt/AwsCrtClientCallingPatternIntegrationTest.java new file mode 100644 index 000000000000..4d489d6c0ab5 --- /dev/null +++ b/http-clients/aws-crt-client/src/it/java/software/amazon/awssdk/http/crt/AwsCrtClientCallingPatternIntegrationTest.java @@ -0,0 +1,208 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.crt; + +import static software.amazon.awssdk.testutils.service.AwsTestBase.CREDENTIALS_PROVIDER_CHAIN; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.Assert; +import org.junit.experimental.theories.DataPoints; +import org.junit.experimental.theories.FromDataPoints; +import org.junit.experimental.theories.Theories; +import org.junit.experimental.theories.Theory; +import org.junit.runner.RunWith; +import software.amazon.awssdk.crt.CrtResource; +import software.amazon.awssdk.crt.io.EventLoopGroup; +import software.amazon.awssdk.crt.io.HostResolver; +import software.amazon.awssdk.http.SdkHttpConfigurationOption; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.kms.KmsAsyncClient; +import software.amazon.awssdk.services.kms.model.GenerateRandomRequest; +import software.amazon.awssdk.services.kms.model.GenerateRandomResponse; +import software.amazon.awssdk.utils.AttributeMap; + + +/** + * Test many possible different calling patterns that users might do, and make sure everything works. + */ +@RunWith(Theories.class) +public class AwsCrtClientCallingPatternIntegrationTest { + private final static String KEY_ALIAS = "alias/aws-sdk-java-v2-integ-test"; + private final static Region REGION = Region.US_EAST_1; + private final static int DEFAULT_KEY_SIZE = 32; + + // Success rate will currently never go above ~99% due to aws-c-http not detecting connection close headers, and KMS + // closing the connection after the 100th Request on a Http Connection. + // Tracking Issue: https://github.com/awslabs/aws-c-http/issues/106 + private static double MINIMUM_SUCCESS_RATE = 0.95; + + private boolean testWithClient(KmsAsyncClient asyncKMSClient, int numberOfRequests) { + List> futures = new ArrayList<>(); + + for (int i = 0; i < numberOfRequests; i++) { + GenerateRandomRequest request = GenerateRandomRequest.builder().numberOfBytes(DEFAULT_KEY_SIZE).build(); + CompletableFuture future = asyncKMSClient.generateRandom(request); + futures.add(future); + } + + List failures = new ArrayList<>(); + int actualNumSucceeded = 0; + for (CompletableFuture f : futures) { + try { + GenerateRandomResponse resp = f.get(5, TimeUnit.MINUTES); + if (200 == resp.sdkHttpResponse().statusCode()) { + actualNumSucceeded += 1; + } + } catch (Exception e) { + failures.add(e); + } + } + + int minimumNumSucceeded = (int)(numberOfRequests * (MINIMUM_SUCCESS_RATE)); + boolean succeeded = true; + if (actualNumSucceeded < minimumNumSucceeded) { + System.err.println("Failure Metrics: numRequests=" + numberOfRequests + ", numSucceeded=" + actualNumSucceeded); + succeeded = false; + } + + if (!succeeded) { + for(Exception e: failures) { + System.err.println(e.getMessage()); + } + failures.get(0).printStackTrace(); + } + + return succeeded; + } + + private boolean testWithNewClient(int eventLoopSize, int numberOfRequests) { + + try (SdkAsyncHttpClient newAwsCrtHttpClient = AwsCrtAsyncHttpClient.builder() + .build()) { + try (KmsAsyncClient newAsyncKMSClient = KmsAsyncClient.builder() + .region(REGION) + .httpClient(newAwsCrtHttpClient) + .credentialsProvider(CREDENTIALS_PROVIDER_CHAIN) + .build()) { + boolean succeeded = testWithClient(newAsyncKMSClient, numberOfRequests); + return succeeded; + } + } + } + + @DataPoints("EventLoop") + public static int[] eventLoopValues(){ + return new int[]{1, 4}; + } + + @DataPoints("ConnectionPool") + public static int[] connectionsValues(){ + /* Don't use 1 connection Pool of size 1, otherwise test takes too long */ + return new int[]{10, 100}; + } + + @DataPoints("NumRequests") + public static int[] requestValues(){ + return new int[]{1, 25, 250}; + } + + @DataPoints("ParallelClients") + public static int[] parallelClientValues(){ + return new int[]{1, 2, 8}; + } + + @DataPoints("SharedClient") + public static boolean[] sharedClientValue(){ + return new boolean[]{true, false}; + } + + @Theory + public void checkAllCombinations(@FromDataPoints("EventLoop") int eventLoopSize, + @FromDataPoints("ConnectionPool") int connectionPoolSize, + @FromDataPoints("NumRequests") int numberOfRequests, + @FromDataPoints("ParallelClients") int numberOfParallelClients, + @FromDataPoints("SharedClient") boolean useSharedClient) throws Exception { + + try { + + CrtResource.waitForNoResources(); + String testName = String.format("Testing with eventLoopSize %d, connectionPoolSize %d, numberOfRequests %d, " + + "numberOfParallelJavaClients %d, useSharedClient %b", eventLoopSize, connectionPoolSize, + numberOfRequests, numberOfParallelClients, useSharedClient); + System.out.println("\n" + testName); + + CountDownLatch latch = new CountDownLatch(numberOfParallelClients); + + AttributeMap attributes = AttributeMap.builder() + .put(SdkHttpConfigurationOption.MAX_CONNECTIONS, connectionPoolSize) + .build(); + + SdkAsyncHttpClient awsCrtHttpClient = AwsCrtAsyncHttpClient.builder() + .buildWithDefaults(attributes); + + KmsAsyncClient sharedAsyncKMSClient = KmsAsyncClient.builder() + .region(REGION) + .httpClient(awsCrtHttpClient) + .credentialsProvider(CREDENTIALS_PROVIDER_CHAIN) + .build(); + + final AtomicBoolean failed = new AtomicBoolean(false); + + long start = System.currentTimeMillis(); + ExecutorService pool = Executors.newCachedThreadPool(); + for (int threads = 0; threads < numberOfParallelClients; threads++) { + pool.submit(() -> { + if (useSharedClient) { + if (!testWithClient(sharedAsyncKMSClient, numberOfRequests)) { + System.err.println("Failed: " + testName); + failed.set(true); + } + } else { + if (!testWithNewClient(eventLoopSize, numberOfRequests)) { + System.err.println("Failed: " + testName); + failed.set(true); + } + } + latch.countDown(); + }); + } + + latch.await(5, TimeUnit.MINUTES); + + sharedAsyncKMSClient.close(); + awsCrtHttpClient.close(); + Assert.assertFalse(failed.get()); + + CrtResource.waitForNoResources(); + + float numSeconds = (float) ((System.currentTimeMillis() - start) / 1000.0); + String timeElapsed = String.format("%.2f sec", numSeconds); + + System.out.println("Passed: " + testName + ", Time " + timeElapsed); + } catch (Exception e) { + System.err.println(e.getMessage()); + e.printStackTrace(); + } + } +} diff --git a/http-clients/aws-crt-client/src/it/java/software/amazon/awssdk/http/crt/AwsCrtClientKmsIntegrationTest.java b/http-clients/aws-crt-client/src/it/java/software/amazon/awssdk/http/crt/AwsCrtClientKmsIntegrationTest.java new file mode 100644 index 000000000000..fc7e36803a87 --- /dev/null +++ b/http-clients/aws-crt-client/src/it/java/software/amazon/awssdk/http/crt/AwsCrtClientKmsIntegrationTest.java @@ -0,0 +1,142 @@ +package software.amazon.awssdk.http.crt; + +import static software.amazon.awssdk.testutils.service.AwsTestBase.CREDENTIALS_PROVIDER_CHAIN; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.crt.CrtResource; +import software.amazon.awssdk.crt.io.EventLoopGroup; +import software.amazon.awssdk.crt.io.HostResolver; +import software.amazon.awssdk.crt.io.TlsCipherPreference; +import software.amazon.awssdk.crt.io.TlsContextOptions; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.kms.KmsAsyncClient; +import software.amazon.awssdk.services.kms.model.CreateAliasRequest; +import software.amazon.awssdk.services.kms.model.CreateAliasResponse; +import software.amazon.awssdk.services.kms.model.CreateKeyRequest; +import software.amazon.awssdk.services.kms.model.CreateKeyResponse; +import software.amazon.awssdk.services.kms.model.DecryptRequest; +import software.amazon.awssdk.services.kms.model.DecryptResponse; +import software.amazon.awssdk.services.kms.model.DescribeKeyRequest; +import software.amazon.awssdk.services.kms.model.DescribeKeyResponse; +import software.amazon.awssdk.services.kms.model.EncryptRequest; +import software.amazon.awssdk.services.kms.model.EncryptResponse; + + +public class AwsCrtClientKmsIntegrationTest { + private static String KEY_ALIAS = "alias/aws-sdk-java-v2-integ-test"; + private static Region REGION = Region.US_EAST_1; + private static List awsCrtHttpClients = new ArrayList<>(); + private static EventLoopGroup eventLoopGroup; + private static HostResolver hostResolver; + + @Before + public void setup() { + CrtResource.waitForNoResources(); + + // Create an Http Client for each TLS Cipher Preference supported on the current platform + for (TlsCipherPreference pref: TlsCipherPreference.values()) { + if (!TlsContextOptions.isCipherPreferenceSupported(pref)) { + continue; + } + + int numThreads = 1; + eventLoopGroup = new EventLoopGroup(numThreads); + hostResolver = new HostResolver(eventLoopGroup); + + SdkAsyncHttpClient awsCrtHttpClient = AwsCrtAsyncHttpClient.builder() + .build(); + + awsCrtHttpClients.add(awsCrtHttpClient); + } + } + + + @After + public void tearDown() { + hostResolver.close(); + eventLoopGroup.close(); + CrtResource.waitForNoResources(); + } + + private boolean doesKeyExist(KmsAsyncClient kms, String keyAlias) { + try { + DescribeKeyRequest req = DescribeKeyRequest.builder().keyId(keyAlias).build(); + DescribeKeyResponse resp = kms.describeKey(req).get(); + Assert.assertEquals(200, resp.sdkHttpResponse().statusCode()); + return resp.sdkHttpResponse().isSuccessful(); + } catch (Exception e) { + return false; + } + } + + private void createKeyAlias(KmsAsyncClient kms, String keyId, String keyAlias) throws Exception { + CreateAliasRequest req = CreateAliasRequest.builder().aliasName(keyAlias).targetKeyId(keyId).build(); + CreateAliasResponse resp = kms.createAlias(req).get(); + Assert.assertEquals(200, resp.sdkHttpResponse().statusCode()); + } + + private String createKey(KmsAsyncClient kms) throws Exception { + CreateKeyRequest req = CreateKeyRequest.builder().build(); + CreateKeyResponse resp = kms.createKey(req).get(); + Assert.assertEquals(200, resp.sdkHttpResponse().statusCode()); + return resp.keyMetadata().keyId(); + } + + private void createKeyIfNotExists(KmsAsyncClient kms, String keyAlias) throws Exception { + if (!doesKeyExist(kms, keyAlias)) { + String keyId = createKey(kms); + createKeyAlias(kms, keyId, KEY_ALIAS); + } + } + + private SdkBytes encrypt(KmsAsyncClient kms, String keyId, String plaintext) throws Exception { + SdkBytes bytes = SdkBytes.fromUtf8String(plaintext); + EncryptRequest req = EncryptRequest.builder().keyId(keyId).plaintext(bytes).build(); + EncryptResponse resp = kms.encrypt(req).get(); + Assert.assertEquals(200, resp.sdkHttpResponse().statusCode()); + return resp.ciphertextBlob(); + } + + private String decrypt(KmsAsyncClient kms, SdkBytes ciphertext) throws Exception { + DecryptRequest req = DecryptRequest.builder().ciphertextBlob(ciphertext).build(); + DecryptResponse resp = kms.decrypt(req).get(); + Assert.assertEquals(200, resp.sdkHttpResponse().statusCode()); + return resp.plaintext().asUtf8String(); + } + + private void testEncryptDecryptWithKms(KmsAsyncClient kms) throws Exception { + createKeyIfNotExists(kms, KEY_ALIAS); + Assert.assertTrue(doesKeyExist(kms, KEY_ALIAS)); + Assert.assertFalse(doesKeyExist(kms, "alias/does-not-exist-" + UUID.randomUUID())); + + String secret = UUID.randomUUID().toString(); + SdkBytes cipherText = encrypt(kms, KEY_ALIAS, secret); + String plainText = decrypt(kms, cipherText); + + Assert.assertEquals(plainText, secret); + } + + @Test + public void testEncryptDecryptWithKms() throws Exception { + for (SdkAsyncHttpClient awsCrtHttpClient: awsCrtHttpClients) { + KmsAsyncClient kms = KmsAsyncClient.builder() + .region(REGION) + .httpClient(awsCrtHttpClient) + .credentialsProvider(CREDENTIALS_PROVIDER_CHAIN) + .build(); + + testEncryptDecryptWithKms(kms); + + kms.close(); + awsCrtHttpClient.close(); + } + } +} diff --git a/http-clients/aws-crt-client/src/it/java/software/amazon/awssdk/http/crt/AwsCrtClientS3IntegrationTest.java b/http-clients/aws-crt-client/src/it/java/software/amazon/awssdk/http/crt/AwsCrtClientS3IntegrationTest.java new file mode 100644 index 000000000000..03862022e26c --- /dev/null +++ b/http-clients/aws-crt-client/src/it/java/software/amazon/awssdk/http/crt/AwsCrtClientS3IntegrationTest.java @@ -0,0 +1,108 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.crt; + +import static org.apache.commons.codec.digest.DigestUtils.sha256Hex; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.crt.CrtResource; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; + + +public class AwsCrtClientS3IntegrationTest { + /** + * The name of the bucket created, used, and deleted by these tests. + */ + private static String BUCKET_NAME = "aws-crt-test-stuff"; + + private static String LARGE_FILE = "http_test_doc.txt"; + private static String SMALL_FILE = "random_32_byte.data"; + private static String LARGE_FILE_SHA256 = "C7FDB5314B9742467B16BD5EA2F8012190B5E2C44A005F7984F89AAB58219534"; + private static int NUM_REQUESTS = 1000; + + private static Region REGION = Region.US_EAST_1; + + private static SdkAsyncHttpClient crtClient; + + private static S3AsyncClient s3; + + @BeforeClass + public static void setup() { + CrtResource.waitForNoResources(); + + crtClient = AwsCrtAsyncHttpClient.create(); + + s3 = S3AsyncClient.builder() + .region(REGION) + .httpClient(crtClient) + .credentialsProvider(AnonymousCredentialsProvider.create()) // File is publicly readable + .build(); + } + + @AfterClass + public static void tearDown() { + s3.close(); + crtClient.close(); + CrtResource.waitForNoResources(); + } + + @Test + public void testDownloadFromS3() throws Exception { + GetObjectRequest s3Request = GetObjectRequest.builder() + .bucket(BUCKET_NAME) + .key(LARGE_FILE) + .build(); + + byte[] responseBody = s3.getObject(s3Request, AsyncResponseTransformer.toBytes()).get(120, TimeUnit.SECONDS).asByteArray(); + + assertThat(sha256Hex(responseBody).toUpperCase()).isEqualTo(LARGE_FILE_SHA256); + } + + @Test + public void testParallelDownloadFromS3() throws Exception { + List> > requestFutures = new ArrayList<>(); + + for (int i = 0; i < NUM_REQUESTS; i++) { + GetObjectRequest s3Request = GetObjectRequest.builder() + .bucket(BUCKET_NAME) + .key(SMALL_FILE) + .build(); + CompletableFuture> requestFuture = s3.getObject(s3Request, AsyncResponseTransformer.toBytes()); + requestFutures.add(requestFuture); + } + + for(CompletableFuture> f: requestFutures) { + f.join(); + Assert.assertEquals(32, f.get().asByteArray().length); + } + } + +} diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClient.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClient.java new file mode 100644 index 000000000000..ca20f32895e3 --- /dev/null +++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClient.java @@ -0,0 +1,423 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.crt; + +import static software.amazon.awssdk.utils.Validate.paramNotNull; + +import java.net.URI; +import java.time.Duration; +import java.util.LinkedList; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; +import software.amazon.awssdk.annotations.SdkPreviewApi; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.crt.CrtResource; +import software.amazon.awssdk.crt.http.HttpClientConnectionManager; +import software.amazon.awssdk.crt.http.HttpClientConnectionManagerOptions; +import software.amazon.awssdk.crt.http.HttpMonitoringOptions; +import software.amazon.awssdk.crt.http.HttpProxyOptions; +import software.amazon.awssdk.crt.io.ClientBootstrap; +import software.amazon.awssdk.crt.io.SocketOptions; +import software.amazon.awssdk.crt.io.TlsCipherPreference; +import software.amazon.awssdk.crt.io.TlsContext; +import software.amazon.awssdk.crt.io.TlsContextOptions; +import software.amazon.awssdk.http.SdkHttpConfigurationOption; +import software.amazon.awssdk.http.async.AsyncExecuteRequest; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.http.crt.internal.CrtRequestContext; +import software.amazon.awssdk.http.crt.internal.CrtRequestExecutor; +import software.amazon.awssdk.utils.AttributeMap; +import software.amazon.awssdk.utils.IoUtils; +import software.amazon.awssdk.utils.Logger; +import software.amazon.awssdk.utils.Validate; + +/** + * An implementation of {@link SdkAsyncHttpClient} that uses the AWS Common Runtime (CRT) Http Client to communicate with + * Http Web Services. This client is asynchronous and uses non-blocking IO. + * + *

This can be created via {@link #builder()}

+ * + * NOTE: This is a Preview API and is subject to change so it should not be used in production. + */ +@SdkPublicApi +@SdkPreviewApi +public final class AwsCrtAsyncHttpClient implements SdkAsyncHttpClient { + private static final Logger log = Logger.loggerFor(AwsCrtAsyncHttpClient.class); + + private static final String AWS_COMMON_RUNTIME = "AwsCommonRuntime"; + private static final int DEFAULT_STREAM_WINDOW_SIZE = 16 * 1024 * 1024; // 16 MB + + private final Map connectionPools = new ConcurrentHashMap<>(); + private final LinkedList ownedSubResources = new LinkedList<>(); + private final ClientBootstrap bootstrap; + private final SocketOptions socketOptions; + private final TlsContext tlsContext; + private final HttpProxyOptions proxyOptions; + private final HttpMonitoringOptions monitoringOptions; + private final long maxConnectionIdleInMilliseconds; + private final int readBufferSize; + private final int maxConnectionsPerEndpoint; + private boolean isClosed = false; + + private AwsCrtAsyncHttpClient(DefaultBuilder builder, AttributeMap config) { + int maxConns = config.get(SdkHttpConfigurationOption.MAX_CONNECTIONS); + + Validate.isPositive(maxConns, "maxConns"); + Validate.notNull(builder.cipherPreference, "cipherPreference"); + Validate.isPositive(builder.readBufferSize, "readBufferSize"); + + try (ClientBootstrap clientBootstrap = new ClientBootstrap(null, null); + SocketOptions clientSocketOptions = new SocketOptions(); + TlsContextOptions clientTlsContextOptions = TlsContextOptions.createDefaultClient() // NOSONAR + .withCipherPreference(builder.cipherPreference) + .withVerifyPeer(!config.get(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES)); + TlsContext clientTlsContext = new TlsContext(clientTlsContextOptions)) { + + this.bootstrap = registerOwnedResource(clientBootstrap); + this.socketOptions = registerOwnedResource(clientSocketOptions); + this.tlsContext = registerOwnedResource(clientTlsContext); + this.readBufferSize = builder.readBufferSize; + this.maxConnectionsPerEndpoint = maxConns; + this.monitoringOptions = revolveHttpMonitoringOptions(builder.connectionHealthChecksConfiguration); + this.maxConnectionIdleInMilliseconds = config.get(SdkHttpConfigurationOption.CONNECTION_MAX_IDLE_TIMEOUT).toMillis(); + this.proxyOptions = buildProxyOptions(builder.proxyConfiguration); + } + } + + private HttpMonitoringOptions revolveHttpMonitoringOptions(ConnectionHealthChecksConfiguration config) { + if (config == null) { + return null; + } + + HttpMonitoringOptions httpMonitoringOptions = new HttpMonitoringOptions(); + httpMonitoringOptions.setMinThroughputBytesPerSecond(config.minThroughputInBytesPerSecond()); + int seconds = (int) config.allowableThroughputFailureInterval().getSeconds(); + httpMonitoringOptions.setAllowableThroughputFailureIntervalSeconds(seconds); + return httpMonitoringOptions; + } + + private HttpProxyOptions buildProxyOptions(ProxyConfiguration proxyConfiguration) { + if (proxyConfiguration == null) { + return null; + } + + HttpProxyOptions clientProxyOptions = new HttpProxyOptions(); + + clientProxyOptions.setHost(proxyConfiguration.host()); + clientProxyOptions.setPort(proxyConfiguration.port()); + + if ("https".equalsIgnoreCase(proxyConfiguration.scheme())) { + clientProxyOptions.setTlsContext(tlsContext); + } + + if (proxyConfiguration.username() != null && proxyConfiguration.password() != null) { + clientProxyOptions.setAuthorizationUsername(proxyConfiguration.username()); + clientProxyOptions.setAuthorizationPassword(proxyConfiguration.password()); + clientProxyOptions.setAuthorizationType(HttpProxyOptions.HttpProxyAuthorizationType.Basic); + } else { + clientProxyOptions.setAuthorizationType(HttpProxyOptions.HttpProxyAuthorizationType.None); + } + + return clientProxyOptions; + } + + /** + * Marks a Native CrtResource as owned by the current Java Object. + * + * @param subresource The Resource to own. + * @param The CrtResource Type + * @return The CrtResource passed in + */ + private T registerOwnedResource(T subresource) { + if (subresource != null) { + subresource.addRef(); + ownedSubResources.push(subresource); + } + return subresource; + } + + public static Builder builder() { + return new DefaultBuilder(); + } + + /** + * Create a {@link AwsCrtAsyncHttpClient} client with the default configuration + * + * @return an {@link SdkAsyncHttpClient} + */ + public static SdkAsyncHttpClient create() { + return new DefaultBuilder().build(); + } + + @Override + public String clientName() { + return AWS_COMMON_RUNTIME; + } + + private HttpClientConnectionManager createConnectionPool(URI uri) { + log.debug(() -> "Creating ConnectionPool for: URI:" + uri + ", MaxConns: " + maxConnectionsPerEndpoint); + + HttpClientConnectionManagerOptions options = new HttpClientConnectionManagerOptions() + .withClientBootstrap(bootstrap) + .withSocketOptions(socketOptions) + .withTlsContext(tlsContext) + .withUri(uri) + .withWindowSize(readBufferSize) + .withMaxConnections(maxConnectionsPerEndpoint) + .withManualWindowManagement(true) + .withProxyOptions(proxyOptions) + .withMonitoringOptions(monitoringOptions) + .withMaxConnectionIdleInMilliseconds(maxConnectionIdleInMilliseconds); + + return HttpClientConnectionManager.create(options); + } + + /* + * Callers of this function MUST account for the addRef() on the pool before returning. + * Every execution path consuming the return value must guarantee an associated close(). + * Currently this function is only used by execute(), which guarantees a matching close + * via the try-with-resources block. + * + * This guarantees that a returned pool will not get closed (by closing the http client) during + * the time it takes to submit a request to the pool. Acquisition requests submitted to the pool will + * be properly failed if the http client is closed before the acquisition completes. + * + * This additional complexity means we only have to keep a lock for the scope of this function, as opposed to + * the scope of calling execute(). This function will almost always just be a hash lookup and the return of an + * existing pool. If we add all of execute() to the scope, we include, at minimum a JNI call to the native + * pool implementation. + */ + private HttpClientConnectionManager getOrCreateConnectionPool(URI uri) { + synchronized (this) { + if (isClosed) { + throw new IllegalStateException("Client is closed. No more requests can be made with this client."); + } + + HttpClientConnectionManager connPool = connectionPools.computeIfAbsent(uri, this::createConnectionPool); + connPool.addRef(); + return connPool; + } + } + + @Override + public CompletableFuture execute(AsyncExecuteRequest asyncRequest) { + + paramNotNull(asyncRequest, "asyncRequest"); + paramNotNull(asyncRequest.request(), "SdkHttpRequest"); + paramNotNull(asyncRequest.requestContentPublisher(), "RequestContentPublisher"); + paramNotNull(asyncRequest.responseHandler(), "ResponseHandler"); + + /* + * See the note on getOrCreateConnectionPool() + * + * In particular, this returns a ref-counted object and calling getOrCreateConnectionPool + * increments the ref count by one. We add a try-with-resources to release our ref + * once we have successfully submitted a request. In this way, we avoid a race condition + * when close/shutdown is called from another thread while this function is executing (ie. + * we have a pool and no one can destroy it underneath us until we've finished submitting the + * request) + */ + try (HttpClientConnectionManager crtConnPool = getOrCreateConnectionPool(asyncRequest.request().getUri())) { + CrtRequestContext context = CrtRequestContext.builder() + .crtConnPool(crtConnPool) + .readBufferSize(readBufferSize) + .request(asyncRequest) + .build(); + + return new CrtRequestExecutor().execute(context); + } + } + + @Override + public void close() { + synchronized (this) { + + if (isClosed) { + return; + } + + connectionPools.values().forEach(pool -> IoUtils.closeQuietly(pool, log.logger())); + ownedSubResources.forEach(r -> IoUtils.closeQuietly(r, log.logger())); + ownedSubResources.clear(); + + isClosed = true; + } + } + + /** + * Builder that allows configuration of the AWS CRT HTTP implementation. + */ + public interface Builder extends SdkAsyncHttpClient.Builder { + + /** + * The Maximum number of allowed concurrent requests. For HTTP/1.1 this is the same as max connections. + * @param maxConcurrency maximum concurrency per endpoint + * @return The builder of the method chaining. + */ + Builder maxConcurrency(int maxConcurrency); + + /** + * The AWS CRT TlsCipherPreference to use for this Client + * @param tlsCipherPreference The AWS Common Runtime TlsCipherPreference + * @return The builder of the method chaining. + */ + Builder tlsCipherPreference(TlsCipherPreference tlsCipherPreference); + + /** + * Configures the number of unread bytes that can be buffered in the + * client before we stop reading from the underlying TCP socket and wait for the Subscriber + * to read more data. + * + * @param readBufferSize The number of bytes that can be buffered + * @return The builder of the method chaining. + */ + Builder readBufferSize(int readBufferSize); + + /** + * Sets the http proxy configuration to use for this client. + * @param proxyConfiguration The http proxy configuration to use + * @return The builder of the method chaining. + */ + Builder proxyConfiguration(ProxyConfiguration proxyConfiguration); + + /** + * Sets the http proxy configuration to use for this client. + * + * @param proxyConfigurationBuilderConsumer The consumer of the proxy configuration builder object. + * @return the builder for method chaining. + */ + Builder proxyConfiguration(Consumer proxyConfigurationBuilderConsumer); + + /** + * Configure the health checks for for all connections established by this client. + * + *

+ * eg: you can set a throughput threshold for the a connection to be considered healthy. + * If the connection falls below this threshold for a configurable amount of time, + * then the connection is considered unhealthy and will be shut down. + * + * @param healthChecksConfiguration The health checks config to use + * @return The builder of the method chaining. + */ + Builder connectionHealthChecksConfiguration(ConnectionHealthChecksConfiguration healthChecksConfiguration); + + /** + * A convenience method to configure the health checks for for all connections established by this client. + * + *

+ * eg: you can set a throughput threshold for the a connection to be considered healthy. + * If the connection falls below this threshold for a configurable amount of time, + * then the connection is considered unhealthy and will be shut down. + * + * @param healthChecksConfigurationBuilder The health checks config builder to use + * @return The builder of the method chaining. + * @see #connectionHealthChecksConfiguration(ConnectionHealthChecksConfiguration) + */ + Builder connectionHealthChecksConfiguration(Consumer + healthChecksConfigurationBuilder); + + /** + * Configure the maximum amount of time that a connection should be allowed to remain open while idle. + */ + Builder connectionMaxIdleTime(Duration connectionMaxIdleTime); + } + + /** + * Factory that allows more advanced configuration of the AWS CRT HTTP implementation. Use {@link #builder()} to + * configure and construct an immutable instance of the factory. + */ + private static final class DefaultBuilder implements Builder { + private final AttributeMap.Builder standardOptions = AttributeMap.builder(); + private TlsCipherPreference cipherPreference = TlsCipherPreference.TLS_CIPHER_SYSTEM_DEFAULT; + private int readBufferSize = DEFAULT_STREAM_WINDOW_SIZE; + private ProxyConfiguration proxyConfiguration; + private ConnectionHealthChecksConfiguration connectionHealthChecksConfiguration; + + private DefaultBuilder() { + } + + @Override + public SdkAsyncHttpClient build() { + return new AwsCrtAsyncHttpClient(this, standardOptions.build() + .merge(SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS)); + } + + @Override + public SdkAsyncHttpClient buildWithDefaults(AttributeMap serviceDefaults) { + return new AwsCrtAsyncHttpClient(this, standardOptions.build() + .merge(serviceDefaults) + .merge(SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS)); + } + + @Override + public Builder maxConcurrency(int maxConcurrency) { + Validate.isPositive(maxConcurrency, "maxConcurrency"); + standardOptions.put(SdkHttpConfigurationOption.MAX_CONNECTIONS, maxConcurrency); + return this; + } + + @Override + public Builder tlsCipherPreference(TlsCipherPreference tlsCipherPreference) { + Validate.notNull(tlsCipherPreference, "cipherPreference"); + Validate.isTrue(TlsContextOptions.isCipherPreferenceSupported(tlsCipherPreference), + "TlsCipherPreference not supported on current Platform"); + this.cipherPreference = tlsCipherPreference; + return this; + } + + @Override + public Builder readBufferSize(int readBufferSize) { + Validate.isPositive(readBufferSize, "readBufferSize"); + this.readBufferSize = readBufferSize; + return this; + } + + @Override + public Builder proxyConfiguration(ProxyConfiguration proxyConfiguration) { + this.proxyConfiguration = proxyConfiguration; + return this; + } + + @Override + public Builder connectionHealthChecksConfiguration(ConnectionHealthChecksConfiguration monitoringOptions) { + this.connectionHealthChecksConfiguration = monitoringOptions; + return this; + } + + @Override + public Builder connectionHealthChecksConfiguration(Consumer + configurationBuilder) { + ConnectionHealthChecksConfiguration.Builder builder = ConnectionHealthChecksConfiguration.builder(); + configurationBuilder.accept(builder); + return connectionHealthChecksConfiguration(builder.build()); + } + + @Override + public Builder connectionMaxIdleTime(Duration connectionMaxIdleTime) { + standardOptions.put(SdkHttpConfigurationOption.CONNECTION_MAX_IDLE_TIMEOUT, connectionMaxIdleTime); + return this; + } + + @Override + public Builder proxyConfiguration(Consumer proxyConfigurationBuilderConsumer) { + ProxyConfiguration.Builder builder = ProxyConfiguration.builder(); + proxyConfigurationBuilderConsumer.accept(builder); + return proxyConfiguration(builder.build()); + } + } +} diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtSdkHttpService.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtSdkHttpService.java new file mode 100644 index 000000000000..cf0a609497b4 --- /dev/null +++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtSdkHttpService.java @@ -0,0 +1,36 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.crt; + +import software.amazon.awssdk.annotations.SdkPreviewApi; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.http.async.SdkAsyncHttpService; + +/** + * Service binding for the AWS common runtime HTTP client implementation. Allows SDK to pick this up automatically from the + * classpath. + * + * NOTE: This is a Preview API and is subject to change so it should not be used in production. + */ +@SdkPublicApi +@SdkPreviewApi +public class AwsCrtSdkHttpService implements SdkAsyncHttpService { + @Override + public SdkAsyncHttpClient.Builder createAsyncHttpClientFactory() { + return AwsCrtAsyncHttpClient.builder(); + } +} diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/ConnectionHealthChecksConfiguration.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/ConnectionHealthChecksConfiguration.java new file mode 100644 index 000000000000..f8b14366cdfa --- /dev/null +++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/ConnectionHealthChecksConfiguration.java @@ -0,0 +1,118 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.crt; + +import java.time.Duration; +import software.amazon.awssdk.annotations.SdkPreviewApi; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.utils.Validate; + +/** + * Configuration that defines health checks for for all connections established by + * the{@link ConnectionHealthChecksConfiguration}. + * + * NOTE: This is a Preview API and is subject to change so it should not be used in production. + */ +@SdkPublicApi +@SdkPreviewApi +public final class ConnectionHealthChecksConfiguration { + private final long minThroughputInBytesPerSecond; + private final Duration allowableThroughputFailureInterval; + + private ConnectionHealthChecksConfiguration(DefaultConnectionHealthChecksConfigurationBuilder builder) { + this.minThroughputInBytesPerSecond = Validate.paramNotNull(builder.minThroughputInBytesPerSecond, + "minThroughputInBytesPerSecond"); + this.allowableThroughputFailureInterval = Validate.isPositive(builder.allowableThroughputFailureIntervalSeconds, + "allowableThroughputFailureIntervalSeconds"); + } + + /** + * @return the minimum amount of throughput, in bytes per second, for a connection to be considered healthy. + */ + public long minThroughputInBytesPerSecond() { + return minThroughputInBytesPerSecond; + } + + /** + * @return How long a connection is allowed to be unhealthy before getting shut down. + */ + public Duration allowableThroughputFailureInterval() { + return allowableThroughputFailureInterval; + } + + public static Builder builder() { + return new DefaultConnectionHealthChecksConfigurationBuilder(); + } + + /** + * A builder for {@link ConnectionHealthChecksConfiguration}. + * + *

All implementations of this interface are mutable and not thread safe.

+ */ + public interface Builder { + + /** + * Sets a throughput threshold for connections. Throughput below this value will be considered unhealthy. + * + * @param minThroughputInBytesPerSecond minimum amount of throughput, in bytes per second, for a connection to be + * considered healthy. + * @return Builder + */ + Builder minThroughputInBytesPerSecond(Long minThroughputInBytesPerSecond); + + /** + * Sets how long a connection is allowed to be unhealthy before getting shut down. + * + *

+ * It only supports seconds precision + * + * @param allowableThroughputFailureIntervalSeconds How long a connection is allowed to be unhealthy + * before getting shut down. + * @return Builder + */ + Builder allowableThroughputFailureInterval(Duration allowableThroughputFailureIntervalSeconds); + + ConnectionHealthChecksConfiguration build(); + } + + /** + * An SDK-internal implementation of {@link Builder}. + */ + private static final class DefaultConnectionHealthChecksConfigurationBuilder implements Builder { + private Long minThroughputInBytesPerSecond; + private Duration allowableThroughputFailureIntervalSeconds; + + private DefaultConnectionHealthChecksConfigurationBuilder() { + } + + @Override + public Builder minThroughputInBytesPerSecond(Long minThroughputInBytesPerSecond) { + this.minThroughputInBytesPerSecond = minThroughputInBytesPerSecond; + return this; + } + + @Override + public Builder allowableThroughputFailureInterval(Duration allowableThroughputFailureIntervalSeconds) { + this.allowableThroughputFailureIntervalSeconds = allowableThroughputFailureIntervalSeconds; + return this; + } + + @Override + public ConnectionHealthChecksConfiguration build() { + return new ConnectionHealthChecksConfiguration(this); + } + } +} diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/ProxyConfiguration.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/ProxyConfiguration.java new file mode 100644 index 000000000000..ee5f4f836f01 --- /dev/null +++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/ProxyConfiguration.java @@ -0,0 +1,240 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.crt; + +import java.util.Objects; +import software.amazon.awssdk.annotations.SdkPreviewApi; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.utils.builder.CopyableBuilder; +import software.amazon.awssdk.utils.builder.ToCopyableBuilder; + + +/** + * Proxy configuration for {@link AwsCrtAsyncHttpClient}. This class is used to configure an HTTP proxy to be used by + * the {@link AwsCrtAsyncHttpClient}. + * + * @see AwsCrtAsyncHttpClient.Builder#proxyConfiguration(ProxyConfiguration) + * + * NOTE: This is a Preview API and is subject to change so it should not be used in production. + */ +@SdkPublicApi +@SdkPreviewApi +public final class ProxyConfiguration implements ToCopyableBuilder { + private final String scheme; + private final String host; + private final int port; + + private final String username; + private final String password; + + private ProxyConfiguration(BuilderImpl builder) { + this.scheme = builder.scheme; + this.host = builder.host; + this.port = builder.port; + this.username = builder.username; + this.password = builder.password; + } + + /** + * @return The proxy scheme. + */ + public String scheme() { + return scheme; + } + + /** + * @return The proxy host. + */ + public String host() { + return host; + } + + /** + * @return The proxy port. + */ + public int port() { + return port; + } + + /** + * @return Basic authentication username + */ + public String username() { + return username; + } + + /** + * @return Basic authentication password + */ + public String password() { + return password; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + ProxyConfiguration that = (ProxyConfiguration) o; + + if (port != that.port) { + return false; + } + + if (!Objects.equals(this.scheme, that.scheme)) { + return false; + } + + if (!Objects.equals(this.host, that.host)) { + return false; + } + + if (!Objects.equals(this.username, that.username)) { + return false; + } + + return Objects.equals(this.password, that.password); + } + + @Override + public int hashCode() { + int result = scheme != null ? scheme.hashCode() : 0; + result = 31 * result + (host != null ? host.hashCode() : 0); + result = 31 * result + port; + result = 31 * result + (username != null ? username.hashCode() : 0); + result = 31 * result + (password != null ? password.hashCode() : 0); + + return result; + } + + @Override + public Builder toBuilder() { + return new BuilderImpl(this); + } + + public static Builder builder() { + return new BuilderImpl(); + } + + /** + * Builder for {@link ProxyConfiguration}. + */ + public interface Builder extends CopyableBuilder { + + /** + * Set the hostname of the proxy. + * @param host The proxy host. + * @return This object for method chaining. + */ + Builder host(String host); + + /** + * Set the port that the proxy expects connections on. + * @param port The proxy port. + * @return This object for method chaining. + */ + Builder port(int port); + + /** + * The HTTP scheme to use for connecting to the proxy. Valid values are {@code http} and {@code https}. + *

+ * The client defaults to {@code http} if none is given. + * + * @param scheme The proxy scheme. + * @return This object for method chaining. + */ + Builder scheme(String scheme); + + /** + * The username to use for basic proxy authentication + *

+ * If not set, the client will not use basic authentication + * + * @param username The basic authentication username. + * @return This object for method chaining. + */ + Builder username(String username); + + /** + * The password to use for basic proxy authentication + *

+ * If not set, the client will not use basic authentication + * + * @param password The basic authentication password. + * @return This object for method chaining. + */ + Builder password(String password); + } + + private static final class BuilderImpl implements Builder { + private String scheme; + private String host; + private int port; + private String username; + private String password; + + private BuilderImpl() { + } + + private BuilderImpl(ProxyConfiguration proxyConfiguration) { + this.scheme = proxyConfiguration.scheme; + this.host = proxyConfiguration.host; + this.port = proxyConfiguration.port; + this.username = proxyConfiguration.username; + this.password = proxyConfiguration.password; + } + + @Override + public Builder scheme(String scheme) { + this.scheme = scheme; + return this; + } + + @Override + public Builder host(String host) { + this.host = host; + return this; + } + + @Override + public Builder port(int port) { + this.port = port; + return this; + } + + @Override + public Builder username(String username) { + this.username = username; + return this; + } + + @Override + public Builder password(String password) { + this.password = password; + return this; + } + + @Override + public ProxyConfiguration build() { + return new ProxyConfiguration(this); + } + } +} diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtAsyncHttpStreamAdapter.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtAsyncHttpStreamAdapter.java new file mode 100644 index 000000000000..86f51846a3c7 --- /dev/null +++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtAsyncHttpStreamAdapter.java @@ -0,0 +1,137 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.crt.internal; + +import java.nio.ByteBuffer; +import java.util.concurrent.CompletableFuture; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.crt.CRT; +import software.amazon.awssdk.crt.http.HttpClientConnection; +import software.amazon.awssdk.crt.http.HttpException; +import software.amazon.awssdk.crt.http.HttpHeader; +import software.amazon.awssdk.crt.http.HttpHeaderBlock; +import software.amazon.awssdk.crt.http.HttpRequestBodyStream; +import software.amazon.awssdk.crt.http.HttpStream; +import software.amazon.awssdk.crt.http.HttpStreamResponseHandler; +import software.amazon.awssdk.http.HttpStatusFamily; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.http.async.AsyncExecuteRequest; +import software.amazon.awssdk.utils.Logger; +import software.amazon.awssdk.utils.Validate; + +/** + * Implements the CrtHttpStreamHandler API and converts CRT callbacks into calls to SDK AsyncExecuteRequest methods + */ +@SdkInternalApi +public final class AwsCrtAsyncHttpStreamAdapter implements HttpStreamResponseHandler, HttpRequestBodyStream { + private static final Logger log = Logger.loggerFor(AwsCrtAsyncHttpStreamAdapter.class); + + private final HttpClientConnection connection; + private final CompletableFuture responseComplete; + private final AsyncExecuteRequest sdkRequest; + private final SdkHttpResponse.Builder respBuilder = SdkHttpResponse.builder(); + private final int windowSize; + private final AwsCrtRequestBodySubscriber requestBodySubscriber; + private AwsCrtResponseBodyPublisher respBodyPublisher = null; + + public AwsCrtAsyncHttpStreamAdapter(HttpClientConnection connection, CompletableFuture responseComplete, + AsyncExecuteRequest sdkRequest, int windowSize) { + this.connection = Validate.notNull(connection, "HttpConnection is null"); + this.responseComplete = Validate.notNull(responseComplete, "reqComplete Future is null"); + this.sdkRequest = Validate.notNull(sdkRequest, "AsyncExecuteRequest Future is null"); + this.windowSize = Validate.isPositive(windowSize, "windowSize is <= 0"); + this.requestBodySubscriber = new AwsCrtRequestBodySubscriber(windowSize); + + sdkRequest.requestContentPublisher().subscribe(requestBodySubscriber); + } + + private void initRespBodyPublisherIfNeeded(HttpStream stream) { + if (respBodyPublisher == null) { + respBodyPublisher = new AwsCrtResponseBodyPublisher(connection, stream, responseComplete, windowSize); + } + } + + @Override + public void onResponseHeaders(HttpStream stream, int responseStatusCode, int blockType, HttpHeader[] nextHeaders) { + initRespBodyPublisherIfNeeded(stream); + + for (HttpHeader h : nextHeaders) { + respBuilder.appendHeader(h.getName(), h.getValue()); + } + } + + @Override + public void onResponseHeadersDone(HttpStream stream, int headerType) { + if (headerType == HttpHeaderBlock.MAIN.getValue()) { + initRespBodyPublisherIfNeeded(stream); + + respBuilder.statusCode(stream.getResponseStatusCode()); + sdkRequest.responseHandler().onHeaders(respBuilder.build()); + sdkRequest.responseHandler().onStream(respBodyPublisher); + } + } + + @Override + public int onResponseBody(HttpStream stream, byte[] bodyBytesIn) { + initRespBodyPublisherIfNeeded(stream); + + respBodyPublisher.queueBuffer(bodyBytesIn); + respBodyPublisher.publishToSubscribers(); + + /* + * Intentionally zero. We manually manage the crt stream's window within the body publisher by updating with + * the exact amount we were able to push to the subcriber. + * + * See the call to stream.incrementWindow() in AwsCrtResponseBodyPublisher. + */ + return 0; + } + + @Override + public void onResponseComplete(HttpStream stream, int errorCode) { + initRespBodyPublisherIfNeeded(stream); + + if (HttpStatusFamily.of(respBuilder.statusCode()) == HttpStatusFamily.SERVER_ERROR) { + connection.shutdown(); + } + + if (errorCode == CRT.AWS_CRT_SUCCESS) { + log.debug(() -> "Response Completed Successfully"); + respBodyPublisher.setQueueComplete(); + respBodyPublisher.publishToSubscribers(); + } else { + HttpException error = new HttpException(errorCode); + log.error(() -> "Response Encountered an Error.", error); + + // Invoke Error Callback on SdkAsyncHttpResponseHandler + try { + sdkRequest.responseHandler().onError(error); + } catch (Exception e) { + log.error(() -> String.format("SdkAsyncHttpResponseHandler %s threw an exception in onError: %s", + sdkRequest.responseHandler(), e)); + } + + // Invoke Error Callback on any Subscriber's of the Response Body + respBodyPublisher.setError(error); + respBodyPublisher.publishToSubscribers(); + } + } + + @Override + public boolean sendRequestBody(ByteBuffer bodyBytesOut) { + return requestBodySubscriber.transferRequestBody(bodyBytesOut); + } +} diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtRequestBodySubscriber.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtRequestBodySubscriber.java new file mode 100644 index 000000000000..877cb474dc3c --- /dev/null +++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtRequestBodySubscriber.java @@ -0,0 +1,132 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.crt.internal; + +import static software.amazon.awssdk.crt.utils.ByteBufferUtils.transferData; + +import java.nio.ByteBuffer; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.utils.Logger; +import software.amazon.awssdk.utils.Validate; + +/** + * Implements the Subscriber API to be be callable from AwsCrtAsyncHttpStreamAdapter.sendRequestBody() + */ +@SdkInternalApi +public final class AwsCrtRequestBodySubscriber implements Subscriber { + private static final Logger log = Logger.loggerFor(AwsCrtRequestBodySubscriber.class); + + private final int windowSize; + private final Queue queuedBuffers = new ConcurrentLinkedQueue<>(); + private final AtomicLong queuedByteCount = new AtomicLong(0); + private final AtomicBoolean isComplete = new AtomicBoolean(false); + private final AtomicReference error = new AtomicReference<>(null); + + private AtomicReference subscriptionRef = new AtomicReference<>(null); + + /** + * + * @param windowSize The number bytes to be queued before we stop proactively queuing data + */ + public AwsCrtRequestBodySubscriber(int windowSize) { + Validate.isPositive(windowSize, "windowSize is <= 0"); + this.windowSize = windowSize; + } + + protected void requestDataIfNecessary() { + Subscription subscription = subscriptionRef.get(); + if (subscription == null) { + log.error(() -> "Subscription is null"); + return; + } + if (queuedByteCount.get() < windowSize) { + subscription.request(1); + } + } + + @Override + public void onSubscribe(Subscription s) { + Validate.paramNotNull(s, "s"); + + boolean wasFirstSubscription = subscriptionRef.compareAndSet(null, s); + + if (!wasFirstSubscription) { + log.error(() -> "Only one Subscription supported!"); + s.cancel(); + return; + } + + requestDataIfNecessary(); + } + + @Override + public void onNext(ByteBuffer byteBuffer) { + Validate.paramNotNull(byteBuffer, "byteBuffer"); + queuedBuffers.add(byteBuffer); + queuedByteCount.addAndGet(byteBuffer.remaining()); + requestDataIfNecessary(); + } + + @Override + public void onError(Throwable t) { + log.error(() -> "onError() received an error: " + t.getMessage()); + error.compareAndSet(null, t); + } + + @Override + public void onComplete() { + log.debug(() -> "AwsCrtRequestBodySubscriber Completed"); + isComplete.set(true); + } + + /** + * Transfers any queued data from the Request Body subscriptionRef to the output buffer + * @param out The output ByteBuffer + * @return true if Request Body is completely transferred, false otherwise + */ + public synchronized boolean transferRequestBody(ByteBuffer out) { + if (error.get() != null) { + throw new RuntimeException(error.get()); + } + + while (out.remaining() > 0 && !queuedBuffers.isEmpty()) { + ByteBuffer nextBuffer = queuedBuffers.peek(); + int amtTransferred = transferData(nextBuffer, out); + queuedByteCount.addAndGet(-amtTransferred); + + if (nextBuffer.remaining() == 0) { + queuedBuffers.remove(); + } + } + + boolean endOfStream = isComplete.get() && queuedBuffers.isEmpty(); + + if (!endOfStream) { + requestDataIfNecessary(); + } else { + log.debug(() -> "End Of RequestBody reached"); + } + + return endOfStream; + } +} diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtResponseBodyPublisher.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtResponseBodyPublisher.java new file mode 100644 index 000000000000..a72ac8c7fb14 --- /dev/null +++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtResponseBodyPublisher.java @@ -0,0 +1,333 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.crt.internal; + +import java.nio.ByteBuffer; +import java.util.Optional; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.LongUnaryOperator; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.crt.http.HttpClientConnection; +import software.amazon.awssdk.crt.http.HttpStream; +import software.amazon.awssdk.utils.Logger; +import software.amazon.awssdk.utils.Validate; + +/** + * Adapts an AWS Common Runtime Response Body stream from CrtHttpStreamHandler to a Publisher + */ +@SdkInternalApi +public final class AwsCrtResponseBodyPublisher implements Publisher { + private static final Logger log = Logger.loggerFor(AwsCrtResponseBodyPublisher.class); + private static final LongUnaryOperator DECREMENT_IF_GREATER_THAN_ZERO = x -> ((x > 0) ? (x - 1) : (x)); + + private final HttpClientConnection connection; + private final HttpStream stream; + private final CompletableFuture responseComplete; + private final AtomicLong outstandingRequests = new AtomicLong(0); + private final int windowSize; + private final AtomicBoolean isCancelled = new AtomicBoolean(false); + private final AtomicBoolean areNativeResourcesReleased = new AtomicBoolean(false); + private final AtomicBoolean isSubscriptionComplete = new AtomicBoolean(false); + private final AtomicBoolean queueComplete = new AtomicBoolean(false); + private final AtomicInteger mutualRecursionDepth = new AtomicInteger(0); + private final AtomicInteger queuedBytes = new AtomicInteger(0); + private final AtomicReference> subscriberRef = new AtomicReference<>(null); + private final Queue queuedBuffers = new ConcurrentLinkedQueue<>(); + private final AtomicReference error = new AtomicReference<>(null); + + /** + * Adapts a streaming AWS CRT Http Response Body to a Publisher + * @param stream The AWS CRT Http Stream for this Response + * @param windowSize The max allowed bytes to be queued. The sum of the sizes of all queued ByteBuffers should + * never exceed this value. + */ + public AwsCrtResponseBodyPublisher(HttpClientConnection connection, HttpStream stream, + CompletableFuture responseComplete, int windowSize) { + this.connection = Validate.notNull(connection, "HttpConnection must not be null"); + this.stream = Validate.notNull(stream, "Stream must not be null"); + this.responseComplete = Validate.notNull(responseComplete, "ResponseComplete future must not be null"); + this.windowSize = Validate.isPositive(windowSize, "windowSize must be > 0"); + } + + /** + * Method for the users consuming the Http Response Body to register a subscriber. + * @param subscriber The Subscriber to register. + */ + @Override + public void subscribe(Subscriber subscriber) { + Validate.notNull(subscriber, "Subscriber must not be null"); + + boolean wasFirstSubscriber = subscriberRef.compareAndSet(null, subscriber); + + if (!wasFirstSubscriber) { + log.error(() -> "Only one subscriber allowed"); + + // onSubscribe must be called first before onError gets called, so give it a do-nothing Subscription + subscriber.onSubscribe(new Subscription() { + @Override + public void request(long n) { + // This is a dummy implementation to allow the onError call + } + + @Override + public void cancel() { + // This is a dummy implementation to allow the onError call + } + }); + subscriber.onError(new IllegalStateException("Only one subscriber allowed")); + } else { + subscriber.onSubscribe(new AwsCrtResponseBodySubscription(this)); + } + } + + /** + * Adds a Buffer to the Queue to be published to any Subscribers + * @param buffer The Buffer to be queued. + */ + public void queueBuffer(byte[] buffer) { + Validate.notNull(buffer, "ByteBuffer must not be null"); + + if (isCancelled.get()) { + // Immediately open HttpStream's IO window so it doesn't see any IO Back-pressure. + // AFAIK there's no way to abort an in-progress HttpStream, only free it's memory by calling close() + stream.incrementWindow(buffer.length); + return; + } + + queuedBuffers.add(buffer); + int totalBytesQueued = queuedBytes.addAndGet(buffer.length); + + if (totalBytesQueued > windowSize) { + throw new IllegalStateException("Queued more than Window Size: queued=" + totalBytesQueued + + ", window=" + windowSize); + } + } + + /** + * Function called by Response Body Subscribers to request more Response Body buffers. + * @param n The number of buffers requested. + */ + protected void request(long n) { + Validate.inclusiveBetween(1, Long.MAX_VALUE, n, "request"); + + // Check for overflow of outstanding Requests, and clamp to LONG_MAX. + long outstandingReqs; + if (n > (Long.MAX_VALUE - outstandingRequests.get())) { + outstandingRequests.set(Long.MAX_VALUE); + outstandingReqs = Long.MAX_VALUE; + } else { + outstandingReqs = outstandingRequests.addAndGet(n); + } + + /* + * Since we buffer, in the case where the subscriber came in after the publication has already begun, + * go ahead and flush what we have. + */ + publishToSubscribers(); + + log.trace(() -> "Subscriber Requested more Buffers. Outstanding Requests: " + outstandingReqs); + } + + public void setError(Throwable t) { + log.error(() -> "Error processing Response Body", t); + error.compareAndSet(null, t); + } + + protected void setCancelled() { + isCancelled.set(true); + /** + * subscriberRef must set to null due to ReactiveStream Spec stating references to Subscribers must be deleted + * when onCancel() is called. + */ + subscriberRef.set(null); + } + + private synchronized void releaseNativeResources() { + boolean alreadyReleased = areNativeResourcesReleased.getAndSet(true); + + if (!alreadyReleased) { + stream.close(); + connection.close(); + } + } + + /** + * Called when the final Buffer has been queued and no more data is expected. + */ + public void setQueueComplete() { + log.trace(() -> "Response Body Publisher queue marked as completed."); + queueComplete.set(true); + // We're done with the Native Resources, release them so they can be used by another request. + releaseNativeResources(); + } + + /** + * Completes the Subscription by calling either the .onError() or .onComplete() callbacks exactly once. + */ + protected void completeSubscriptionExactlyOnce() { + boolean alreadyComplete = isSubscriptionComplete.getAndSet(true); + + if (alreadyComplete) { + return; + } + + // Subscriber may have cancelled their subscription, in which case this may be null. + Optional> subscriber = Optional.ofNullable(subscriberRef.getAndSet(null)); + + Throwable throwable = error.get(); + + // We're done with the Native Resources, release them so they can be used by another request. + releaseNativeResources(); + + // Complete the Futures + if (throwable != null) { + log.error(() -> "Error before ResponseBodyPublisher could complete: " + throwable.getMessage()); + try { + subscriber.ifPresent(s -> s.onError(throwable)); + } catch (Exception e) { + log.warn(() -> "Failed to exceptionally complete subscriber future with: " + throwable.getMessage()); + } + responseComplete.completeExceptionally(throwable); + } else { + log.debug(() -> "ResponseBodyPublisher Completed Successfully"); + try { + subscriber.ifPresent(Subscriber::onComplete); + } catch (Exception e) { + log.warn(() -> "Failed to successfully complete subscriber future"); + } + responseComplete.complete(null); + } + } + + /** + * Publishes any queued data to any Subscribers if there is data queued and there is an outstanding Subscriber + * request for more data. Will also call onError() or onComplete() callbacks if needed. + * + * This method MUST be synchronized since it can be called simultaneously from both the Native EventLoop Thread and + * the User Thread. If this method wasn't synchronized, it'd be possible for each thread to dequeue a buffer by + * calling queuedBuffers.poll(), but then have the 2nd thread call subscriber.onNext(buffer) first, resulting in the + * subscriber seeing out-of-order data. To avoid this race condition, this method must be synchronized. + */ + protected void publishToSubscribers() { + boolean shouldComplete = true; + synchronized (this) { + if (error.get() == null) { + if (isSubscriptionComplete.get() || isCancelled.get()) { + log.debug(() -> "Subscription already completed or cancelled, can't publish updates to Subscribers."); + return; + } + + if (mutualRecursionDepth.get() > 0) { + /** + * If our depth is > 0, then we already made a call to publishToSubscribers() further up the stack that + * will continue publishing to subscribers, and this call should return without completing work to avoid + * infinite recursive loop between: "subscription.request() -> subscriber.onNext() -> subscription.request()" + */ + return; + } + + int totalAmountTransferred = 0; + + while (outstandingRequests.get() > 0 && !queuedBuffers.isEmpty()) { + byte[] buffer = queuedBuffers.poll(); + outstandingRequests.getAndUpdate(DECREMENT_IF_GREATER_THAN_ZERO); + int amount = buffer.length; + publishWithoutMutualRecursion(subscriberRef.get(), ByteBuffer.wrap(buffer)); + totalAmountTransferred += amount; + } + + if (totalAmountTransferred > 0) { + queuedBytes.addAndGet(-totalAmountTransferred); + + // We may have released the Native HttpConnection and HttpStream if they completed before the Subscriber + // has finished reading the data. + if (!areNativeResourcesReleased.get()) { + // Open HttpStream's IO window so HttpStream can keep track of IO back-pressure + // This is why it is correct to return 0 from AwsCrtAsyncHttpStreamAdapter::onResponseBody + stream.incrementWindow(totalAmountTransferred); + } + } + + shouldComplete = queueComplete.get() && queuedBuffers.isEmpty(); + } else { + shouldComplete = true; + } + } + + // Check if Complete, consider no subscriber as a completion. + if (shouldComplete) { + completeSubscriptionExactlyOnce(); + } + } + + /** + * This method is used to avoid a StackOverflow due to the potential infinite loop between + * "subscription.request() -> subscriber.onNext() -> subscription.request()" calls. We only call subscriber.onNext() + * if the recursion depth is zero, otherwise we return up to the stack frame with depth zero and continue publishing + * from there. + * @param subscriber The Subscriber to publish to. + * @param buffer The buffer to publish to the subscriber. + */ + private synchronized void publishWithoutMutualRecursion(Subscriber subscriber, ByteBuffer buffer) { + try { + /** + * Need to keep track of recursion depth between .onNext() -> .request() calls + */ + int depth = mutualRecursionDepth.getAndIncrement(); + if (depth == 0) { + subscriber.onNext(buffer); + } + } finally { + mutualRecursionDepth.decrementAndGet(); + } + } + + static class AwsCrtResponseBodySubscription implements Subscription { + private final AwsCrtResponseBodyPublisher publisher; + + AwsCrtResponseBodySubscription(AwsCrtResponseBodyPublisher publisher) { + this.publisher = publisher; + } + + @Override + public void request(long n) { + if (n <= 0) { + // Reactive Stream Spec requires us to call onError() callback instead of throwing Exception here. + publisher.setError(new IllegalArgumentException("Request is for <= 0 elements: " + n)); + publisher.publishToSubscribers(); + return; + } + + publisher.request(n); + publisher.publishToSubscribers(); + } + + @Override + public void cancel() { + publisher.setCancelled(); + } + } + +} diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/CrtRequestContext.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/CrtRequestContext.java new file mode 100644 index 000000000000..d43fd1c9fb19 --- /dev/null +++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/CrtRequestContext.java @@ -0,0 +1,77 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.crt.internal; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.crt.http.HttpClientConnectionManager; +import software.amazon.awssdk.http.async.AsyncExecuteRequest; + +@SdkInternalApi +public final class CrtRequestContext { + private final AsyncExecuteRequest request; + private final int readBufferSize; + private final HttpClientConnectionManager crtConnPool; + + private CrtRequestContext(Builder builder) { + this.request = builder.request; + this.readBufferSize = builder.readBufferSize; + this.crtConnPool = builder.crtConnPool; + } + + public static Builder builder() { + return new Builder(); + } + + public AsyncExecuteRequest sdkRequest() { + return request; + } + + public int readBufferSize() { + return readBufferSize; + } + + public HttpClientConnectionManager crtConnPool() { + return crtConnPool; + } + + public static class Builder { + private AsyncExecuteRequest request; + private int readBufferSize; + private HttpClientConnectionManager crtConnPool; + + private Builder() { + } + + public Builder request(AsyncExecuteRequest request) { + this.request = request; + return this; + } + + public Builder readBufferSize(int readBufferSize) { + this.readBufferSize = readBufferSize; + return this; + } + + public Builder crtConnPool(HttpClientConnectionManager crtConnPool) { + this.crtConnPool = crtConnPool; + return this; + } + + public CrtRequestContext build() { + return new CrtRequestContext(this); + } + } +} diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/CrtRequestExecutor.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/CrtRequestExecutor.java new file mode 100644 index 000000000000..fb6c269ca226 --- /dev/null +++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/CrtRequestExecutor.java @@ -0,0 +1,166 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.crt.internal; + +import static software.amazon.awssdk.utils.CollectionUtils.isNullOrEmpty; +import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely; + +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.crt.CrtRuntimeException; +import software.amazon.awssdk.crt.http.HttpClientConnection; +import software.amazon.awssdk.crt.http.HttpHeader; +import software.amazon.awssdk.crt.http.HttpRequest; +import software.amazon.awssdk.http.Header; +import software.amazon.awssdk.http.SdkCancellationException; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.async.AsyncExecuteRequest; +import software.amazon.awssdk.http.async.SdkAsyncHttpResponseHandler; +import software.amazon.awssdk.utils.Logger; +import software.amazon.awssdk.utils.http.SdkHttpUtils; + +@SdkInternalApi +public final class CrtRequestExecutor { + private static final Logger log = Logger.loggerFor(CrtRequestExecutor.class); + + public CompletableFuture execute(CrtRequestContext executionContext) { + CompletableFuture requestFuture = createExecutionFuture(executionContext.sdkRequest()); + + // When a Connection is ready from the Connection Pool, schedule the Request on the connection + CompletableFuture httpClientConnectionCompletableFuture = + executionContext.crtConnPool().acquireConnection(); + + httpClientConnectionCompletableFuture.whenComplete((crtConn, throwable) -> { + AsyncExecuteRequest asyncRequest = executionContext.sdkRequest(); + // If we didn't get a connection for some reason, fail the request + if (throwable != null) { + handleFailure(new IOException("An exception occurred when acquiring connection", throwable), + requestFuture, + asyncRequest.responseHandler()); + return; + } + + AwsCrtAsyncHttpStreamAdapter crtToSdkAdapter = + new AwsCrtAsyncHttpStreamAdapter(crtConn, requestFuture, asyncRequest, executionContext.readBufferSize()); + HttpRequest crtRequest = toCrtRequest(asyncRequest, crtToSdkAdapter); + // Submit the Request on this Connection + invokeSafely(() -> { + try { + crtConn.makeRequest(crtRequest, crtToSdkAdapter).activate(); + } catch (IllegalStateException | CrtRuntimeException e) { + log.debug(() -> "An exception occurred when making the request", e); + handleFailure(new IOException("An exception occurred when making the request", e), + requestFuture, + asyncRequest.responseHandler()); + + } + }); + }); + + return requestFuture; + } + + /** + * Convenience method to create the execution future and set up the cancellation logic. + * + * @return The created execution future. + */ + private CompletableFuture createExecutionFuture(AsyncExecuteRequest request) { + CompletableFuture future = new CompletableFuture<>(); + + future.whenComplete((r, t) -> { + if (t == null) { + return; + } + //TODO: Aborting request once it's supported in CRT + if (future.isCancelled()) { + request.responseHandler().onError(new SdkCancellationException("The request was cancelled")); + } + }); + + return future; + } + + private void handleFailure(Throwable cause, + CompletableFuture executeFuture, + SdkAsyncHttpResponseHandler responseHandler) { + try { + responseHandler.onError(cause); + } catch (Exception e) { + log.error(() -> String.format("SdkAsyncHttpResponseHandler %s throw an exception in onError", + responseHandler.toString()), e); + } + + executeFuture.completeExceptionally(cause); + } + + private static HttpRequest toCrtRequest(AsyncExecuteRequest asyncRequest, AwsCrtAsyncHttpStreamAdapter crtToSdkAdapter) { + URI uri = asyncRequest.request().getUri(); + SdkHttpRequest sdkRequest = asyncRequest.request(); + + String method = sdkRequest.method().name(); + String encodedPath = sdkRequest.encodedPath(); + if (encodedPath == null || encodedPath.length() == 0) { + encodedPath = "/"; + } + + String encodedQueryString = SdkHttpUtils.encodeAndFlattenQueryParameters(sdkRequest.rawQueryParameters()) + .map(value -> "?" + value) + .orElse(""); + + HttpHeader[] crtHeaderArray = asArray(createHttpHeaderList(uri, asyncRequest)); + + return new HttpRequest(method, encodedPath + encodedQueryString, crtHeaderArray, crtToSdkAdapter); + } + + private static HttpHeader[] asArray(List crtHeaderList) { + return crtHeaderList.toArray(new HttpHeader[0]); + } + + private static List createHttpHeaderList(URI uri, AsyncExecuteRequest asyncRequest) { + SdkHttpRequest sdkRequest = asyncRequest.request(); + // worst case we may add 3 more headers here + List crtHeaderList = new ArrayList<>(sdkRequest.headers().size() + 3); + + // Set Host Header if needed + if (isNullOrEmpty(sdkRequest.headers().get(Header.HOST))) { + crtHeaderList.add(new HttpHeader(Header.HOST, uri.getHost())); + } + + // Add Connection Keep Alive Header to reuse this Http Connection as long as possible + if (isNullOrEmpty(sdkRequest.headers().get(Header.CONNECTION))) { + crtHeaderList.add(new HttpHeader(Header.CONNECTION, Header.KEEP_ALIVE_VALUE)); + } + + // Set Content-Length if needed + Optional contentLength = asyncRequest.requestContentPublisher().contentLength(); + if (isNullOrEmpty(sdkRequest.headers().get(Header.CONTENT_LENGTH)) && contentLength.isPresent()) { + crtHeaderList.add(new HttpHeader(Header.CONTENT_LENGTH, Long.toString(contentLength.get()))); + } + + // Add the rest of the Headers + sdkRequest.headers().forEach((key, value) -> { + value.stream().map(val -> new HttpHeader(key, val)).forEach(crtHeaderList::add); + }); + + return crtHeaderList; + } +} diff --git a/http-clients/aws-crt-client/src/main/resources/META-INF/services/software.amazon.awssdk.http.async.SdkAsyncHttpService b/http-clients/aws-crt-client/src/main/resources/META-INF/services/software.amazon.awssdk.http.async.SdkAsyncHttpService new file mode 100644 index 000000000000..f0312a3b901d --- /dev/null +++ b/http-clients/aws-crt-client/src/main/resources/META-INF/services/software.amazon.awssdk.http.async.SdkAsyncHttpService @@ -0,0 +1,16 @@ +# +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# +# http://aws.amazon.com/apache2.0 +# +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. +# + +software.amazon.awssdk.http.crt.AwsCrtSdkHttpService diff --git a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientSpiVerificationTest.java b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientSpiVerificationTest.java new file mode 100644 index 000000000000..4c7feb9a3929 --- /dev/null +++ b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientSpiVerificationTest.java @@ -0,0 +1,281 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.crt; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.any; +import static com.github.tomakehurst.wiremock.client.WireMock.binaryEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static java.util.Collections.emptyMap; +import static org.apache.commons.codec.digest.DigestUtils.sha256Hex; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.github.tomakehurst.wiremock.http.Fault; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import java.net.URI; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.Random; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import software.amazon.awssdk.crt.CrtResource; +import software.amazon.awssdk.crt.io.EventLoopGroup; +import software.amazon.awssdk.crt.io.HostResolver; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.http.async.AsyncExecuteRequest; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.http.async.SdkAsyncHttpResponseHandler; +import software.amazon.awssdk.utils.Logger; + +public class AwsCrtHttpClientSpiVerificationTest { + private static final Logger log = Logger.loggerFor(AwsCrtHttpClientSpiVerificationTest.class); + private static final int TEST_BODY_LEN = 1024; + + @Rule + public WireMockRule mockServer = new WireMockRule(wireMockConfig() + .dynamicPort() + .dynamicHttpsPort()); + + private SdkAsyncHttpClient client; + + @Before + public void setup() throws Exception { + CrtResource.waitForNoResources(); + + client = AwsCrtAsyncHttpClient.builder() + .connectionHealthChecksConfiguration(b -> b.minThroughputInBytesPerSecond(4068L) + .allowableThroughputFailureInterval(Duration.ofSeconds(3))) + .build(); + } + + @After + public void tearDown() { + client.close(); + EventLoopGroup.closeStaticDefault(); + HostResolver.closeStaticDefault(); + CrtResource.waitForNoResources(); + } + + private byte[] generateRandomBody(int size) { + byte[] randomData = new byte[size]; + new Random().nextBytes(randomData); + return randomData; + } + + @Test + public void signalsErrorViaOnErrorAndFuture() throws InterruptedException, ExecutionException, TimeoutException { + stubFor(any(urlEqualTo("/")).willReturn(aResponse().withFault(Fault.RANDOM_DATA_THEN_CLOSE))); + + CompletableFuture errorSignaled = new CompletableFuture<>(); + + SdkAsyncHttpResponseHandler handler = new TestResponseHandler() { + @Override + public void onError(Throwable error) { + errorSignaled.complete(true); + } + }; + + SdkHttpRequest request = CrtHttpClientTestUtils.createRequest(URI.create("http://localhost:" + mockServer.port())); + + CompletableFuture executeFuture = client.execute(AsyncExecuteRequest.builder() + .request(request) + .responseHandler(handler) + .requestContentPublisher(new EmptyPublisher()) + .build()); + + assertThat(errorSignaled.get(1, TimeUnit.SECONDS)).isTrue(); + assertThatThrownBy(executeFuture::join).hasCauseInstanceOf(Exception.class); + + } + + @Test + public void callsOnStreamForEmptyResponseContent() throws Exception { + stubFor(any(urlEqualTo("/")).willReturn(aResponse().withStatus(204).withHeader("foo", "bar"))); + + CompletableFuture streamReceived = new CompletableFuture<>(); + AtomicReference response = new AtomicReference<>(null); + + SdkAsyncHttpResponseHandler handler = new TestResponseHandler() { + @Override + public void onHeaders(SdkHttpResponse headers) { + response.compareAndSet(null, headers); + } + @Override + public void onStream(Publisher stream) { + super.onStream(stream); + streamReceived.complete(true); + } + }; + + SdkHttpRequest request = CrtHttpClientTestUtils.createRequest(URI.create("http://localhost:" + mockServer.port())); + + CompletableFuture future = client.execute(AsyncExecuteRequest.builder() + .request(request) + .responseHandler(handler) + .requestContentPublisher(new EmptyPublisher()) + .build()); + + future.get(60, TimeUnit.SECONDS); + assertThat(streamReceived.get(1, TimeUnit.SECONDS)).isTrue(); + assertThat(response.get() != null).isTrue(); + assertThat(response.get().statusCode() == 204).isTrue(); + assertThat(response.get().headers().get("foo").isEmpty()).isFalse(); + } + + @Test + public void testGetRequest() throws Exception { + String path = "/testGetRequest"; + byte[] body = generateRandomBody(TEST_BODY_LEN); + String expectedBodyHash = sha256Hex(body).toUpperCase(); + stubFor(any(urlEqualTo(path)).willReturn(aResponse().withStatus(200) + .withHeader("Content-Length", Integer.toString(TEST_BODY_LEN)) + .withHeader("foo", "bar") + .withBody(body))); + + CompletableFuture streamReceived = new CompletableFuture<>(); + AtomicReference response = new AtomicReference<>(null); + Sha256BodySubscriber bodySha256Subscriber = new Sha256BodySubscriber(); + AtomicReference error = new AtomicReference<>(null); + + SdkAsyncHttpResponseHandler handler = new SdkAsyncHttpResponseHandler() { + @Override + public void onHeaders(SdkHttpResponse headers) { + response.compareAndSet(null, headers); + } + @Override + public void onStream(Publisher stream) { + stream.subscribe(bodySha256Subscriber); + streamReceived.complete(true); + } + + @Override + public void onError(Throwable t) { + error.compareAndSet(null, t); + } + }; + + URI uri = URI.create("http://localhost:" + mockServer.port()); + SdkHttpRequest request = CrtHttpClientTestUtils.createRequest(uri, path, null, SdkHttpMethod.GET, emptyMap()); + + CompletableFuture future = client.execute(AsyncExecuteRequest.builder() + .request(request) + .responseHandler(handler) + .requestContentPublisher(new EmptyPublisher()) + .build()); + + future.get(60, TimeUnit.SECONDS); + assertThat(error.get()).isNull(); + assertThat(streamReceived.get(1, TimeUnit.SECONDS)).isTrue(); + assertThat(bodySha256Subscriber.getFuture().get(60, TimeUnit.SECONDS)).isEqualTo(expectedBodyHash); + assertThat(response.get().statusCode()).isEqualTo(200); + assertThat(response.get().headers().get("foo").isEmpty()).isFalse(); + } + + + private void makePutRequest(String path, byte[] reqBody, int expectedStatus) throws Exception { + CompletableFuture streamReceived = new CompletableFuture<>(); + AtomicReference response = new AtomicReference<>(null); + AtomicReference error = new AtomicReference<>(null); + + Subscriber subscriber = CrtHttpClientTestUtils.createDummySubscriber(); + + SdkAsyncHttpResponseHandler handler = CrtHttpClientTestUtils.createTestResponseHandler(response, + streamReceived, error, subscriber); + + URI uri = URI.create("http://localhost:" + mockServer.port()); + SdkHttpRequest request = CrtHttpClientTestUtils.createRequest(uri, path, reqBody, SdkHttpMethod.PUT, emptyMap()); + + CompletableFuture future = client.execute(AsyncExecuteRequest.builder() + .request(request) + .responseHandler(handler) + .requestContentPublisher(new SdkTestHttpContentPublisher(reqBody)) + .build()); + future.get(60, TimeUnit.SECONDS); + assertThat(error.get()).isNull(); + assertThat(streamReceived.get(60, TimeUnit.SECONDS)).isTrue(); + assertThat(response.get().statusCode()).isEqualTo(expectedStatus); + } + + + @Test + public void testPutRequest() throws Exception { + String pathExpect200 = "/testPutRequest/return_200_on_exact_match"; + byte[] expectedBody = generateRandomBody(TEST_BODY_LEN); + stubFor(any(urlEqualTo(pathExpect200)).withRequestBody(binaryEqualTo(expectedBody)).willReturn(aResponse().withStatus(200))); + makePutRequest(pathExpect200, expectedBody, 200); + + String pathExpect404 = "/testPutRequest/return_404_always"; + byte[] randomBody = generateRandomBody(TEST_BODY_LEN); + stubFor(any(urlEqualTo(pathExpect404)).willReturn(aResponse().withStatus(404))); + makePutRequest(pathExpect404, randomBody, 404); + } + + + + private static class TestResponseHandler implements SdkAsyncHttpResponseHandler { + @Override + public void onHeaders(SdkHttpResponse headers) { + } + + @Override + public void onStream(Publisher stream) { + stream.subscribe(new DrainingSubscriber<>()); + } + + @Override + public void onError(Throwable error) { + } + } + + private static class DrainingSubscriber implements Subscriber { + private Subscription subscription; + + @Override + public void onSubscribe(Subscription subscription) { + this.subscription = subscription; + this.subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(T t) { + this.subscription.request(1); + } + + @Override + public void onError(Throwable throwable) { + } + + @Override + public void onComplete() { + } + } +} diff --git a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientWireMockTest.java b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientWireMockTest.java new file mode 100644 index 000000000000..8758c8212aab --- /dev/null +++ b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientWireMockTest.java @@ -0,0 +1,107 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.crt; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.any; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static software.amazon.awssdk.http.HttpTestUtils.createProvider; +import static software.amazon.awssdk.http.crt.CrtHttpClientTestUtils.createRequest; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import java.net.URI; +import java.util.concurrent.TimeUnit; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import software.amazon.awssdk.crt.CrtResource; +import software.amazon.awssdk.crt.io.EventLoopGroup; +import software.amazon.awssdk.crt.io.HostResolver; +import software.amazon.awssdk.http.RecordingNetworkTrafficListener; +import software.amazon.awssdk.http.RecordingResponseHandler; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.async.AsyncExecuteRequest; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.utils.Logger; + +public class AwsCrtHttpClientWireMockTest { + private static final Logger log = Logger.loggerFor(AwsCrtHttpClientWireMockTest.class); + private final RecordingNetworkTrafficListener wiremockTrafficListener = new RecordingNetworkTrafficListener(); + + @Rule + public WireMockRule mockServer = new WireMockRule(wireMockConfig() + .dynamicPort() + .dynamicHttpsPort() + .networkTrafficListener(wiremockTrafficListener)); + + @BeforeClass + public static void setup() { + System.setProperty("aws.crt.debugnative", "true"); + } + + @Before + public void methodSetup() { + wiremockTrafficListener.reset(); + } + + @After + public void tearDown() { + // Verify there is no resource leak. + EventLoopGroup.closeStaticDefault(); + HostResolver.closeStaticDefault(); + CrtResource.waitForNoResources(); + } + + @Test + public void closeClient_reuse_throwException() throws Exception { + SdkAsyncHttpClient client = AwsCrtAsyncHttpClient.create(); + + client.close(); + assertThatThrownBy(() -> makeSimpleRequest(client)).hasMessageContaining("is closed"); + } + + @Test + public void sharedEventLoopGroup_closeOneClient_shouldNotAffectOtherClients() throws Exception { + try (SdkAsyncHttpClient client = AwsCrtAsyncHttpClient.create()) { + makeSimpleRequest(client); + } + + try (SdkAsyncHttpClient anotherClient = AwsCrtAsyncHttpClient.create()) { + makeSimpleRequest(anotherClient); + } + } + + /** + * Make a simple async request and wait for it to finish. + * + * @param client Client to make request with. + */ + private void makeSimpleRequest(SdkAsyncHttpClient client) throws Exception { + String body = randomAlphabetic(10); + URI uri = URI.create("http://localhost:" + mockServer.port()); + stubFor(any(urlPathEqualTo("/")).willReturn(aResponse().withBody(body))); + SdkHttpRequest request = createRequest(uri); + RecordingResponseHandler recorder = new RecordingResponseHandler(); + client.execute(AsyncExecuteRequest.builder().request(request).requestContentPublisher(createProvider("")).responseHandler(recorder).build()); + recorder.completeFuture().get(5, TimeUnit.SECONDS); + } +} diff --git a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtRequestBodySubscriberReactiveStreamCompatTest.java b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtRequestBodySubscriberReactiveStreamCompatTest.java new file mode 100644 index 000000000000..d2b07542c85c --- /dev/null +++ b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtRequestBodySubscriberReactiveStreamCompatTest.java @@ -0,0 +1,66 @@ +package software.amazon.awssdk.http.crt; + +import java.nio.ByteBuffer; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.reactivestreams.tck.SubscriberWhiteboxVerification; +import org.reactivestreams.tck.TestEnvironment; +import software.amazon.awssdk.http.crt.internal.AwsCrtRequestBodySubscriber; + +public class AwsCrtRequestBodySubscriberReactiveStreamCompatTest extends SubscriberWhiteboxVerification { + private static final int DEFAULT_STREAM_WINDOW_SIZE = 16 * 1024 * 1024; // 16 MB Total Buffer size + + public AwsCrtRequestBodySubscriberReactiveStreamCompatTest() { + super(new TestEnvironment()); + } + + @Override + public Subscriber createSubscriber(WhiteboxSubscriberProbe probe) { + AwsCrtRequestBodySubscriber actualSubscriber = new AwsCrtRequestBodySubscriber(DEFAULT_STREAM_WINDOW_SIZE); + + // Pass Through calls to AwsCrtRequestBodySubscriber, but also register calls to the whitebox probe + Subscriber passthroughSubscriber = new Subscriber() { + @Override + public void onSubscribe(Subscription s) { + actualSubscriber.onSubscribe(s); + probe.registerOnSubscribe(new SubscriberPuppet() { + + @Override + public void triggerRequest(long elements) { + s.request(elements); + } + + @Override + public void signalCancel() { + s.cancel(); + } + }); + } + + @Override + public void onNext(ByteBuffer byteBuffer) { + actualSubscriber.onNext(byteBuffer); + probe.registerOnNext(byteBuffer); + } + + @Override + public void onError(Throwable t) { + actualSubscriber.onError(t); + probe.registerOnError(t); + } + + @Override + public void onComplete() { + actualSubscriber.onComplete(); + probe.registerOnComplete(); + } + }; + + return passthroughSubscriber; + } + + @Override + public ByteBuffer createElement(int element) { + return ByteBuffer.wrap(Integer.toString(element).getBytes()); + } +} diff --git a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtResponseBodyPublisherReactiveStreamCompatTest.java b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtResponseBodyPublisherReactiveStreamCompatTest.java new file mode 100644 index 000000000000..143f1e7b591b --- /dev/null +++ b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtResponseBodyPublisherReactiveStreamCompatTest.java @@ -0,0 +1,63 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.crt; + +import static org.mockito.Mockito.mock; + +import java.nio.ByteBuffer; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import org.reactivestreams.Publisher; +import org.reactivestreams.tck.PublisherVerification; +import org.reactivestreams.tck.TestEnvironment; +import software.amazon.awssdk.crt.http.HttpClientConnection; +import software.amazon.awssdk.crt.http.HttpStream; +import software.amazon.awssdk.http.crt.internal.AwsCrtResponseBodyPublisher; +import software.amazon.awssdk.utils.Logger; + +public class AwsCrtResponseBodyPublisherReactiveStreamCompatTest extends PublisherVerification { + private static final Logger log = Logger.loggerFor(AwsCrtResponseBodyPublisherReactiveStreamCompatTest.class); + + public AwsCrtResponseBodyPublisherReactiveStreamCompatTest() { + super(new TestEnvironment()); + } + + @Override + public Publisher createPublisher(long elements) { + HttpClientConnection connection = mock(HttpClientConnection.class); + HttpStream stream = mock(HttpStream.class); + AwsCrtResponseBodyPublisher bodyPublisher = new AwsCrtResponseBodyPublisher(connection, stream, new CompletableFuture<>(), Integer.MAX_VALUE); + + for (long i = 0; i < elements; i++) { + bodyPublisher.queueBuffer(UUID.randomUUID().toString().getBytes()); + } + + bodyPublisher.setQueueComplete(); + return bodyPublisher; + } + + // Some tests try to create INT_MAX elements, which causes OutOfMemory Exceptions. Lower the max allowed number of + // queued buffers to 1024. + @Override + public long maxElementsFromPublisher() { + return 1024; + } + + @Override + public Publisher createFailedPublisher() { + return null; + } +} diff --git a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/ConnectionHealthChecksConfigurationTest.java b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/ConnectionHealthChecksConfigurationTest.java new file mode 100644 index 000000000000..bc7eef8b9b14 --- /dev/null +++ b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/ConnectionHealthChecksConfigurationTest.java @@ -0,0 +1,64 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.crt; + + + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.time.Duration; +import org.junit.Test; + +public class ConnectionHealthChecksConfigurationTest { + + @Test + public void builder_allPropertiesSet() { + ConnectionHealthChecksConfiguration connectionHealthChecksConfiguration = + ConnectionHealthChecksConfiguration.builder() + .minThroughputInBytesPerSecond(123l) + .allowableThroughputFailureInterval(Duration.ofSeconds(1)) + .build(); + + assertThat(connectionHealthChecksConfiguration.minThroughputInBytesPerSecond()).isEqualTo(123); + assertThat(connectionHealthChecksConfiguration.allowableThroughputFailureInterval()).isEqualTo(Duration.ofSeconds(1)); + } + + @Test + public void builder_nullMinThroughputInBytesPerSecond_shouldThrowException() { + assertThatThrownBy(() -> + ConnectionHealthChecksConfiguration.builder() + .allowableThroughputFailureInterval(Duration.ofSeconds(1)) + .build()).hasMessageContaining("minThroughputInBytesPerSecond"); + } + + @Test + public void builder_nullAllowableThroughputFailureInterval() { + assertThatThrownBy(() -> + ConnectionHealthChecksConfiguration.builder() + .minThroughputInBytesPerSecond(1L) + .build()).hasMessageContaining("allowableThroughputFailureIntervalSeconds"); + } + + @Test + public void builder_negativeAllowableThroughputFailureInterval() { + assertThatThrownBy(() -> + ConnectionHealthChecksConfiguration.builder() + .minThroughputInBytesPerSecond(1L) + .allowableThroughputFailureInterval(Duration.ofSeconds(-1)) + .build()).hasMessageContaining("allowableThroughputFailureIntervalSeconds"); + } +} diff --git a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/CrtHttpClientTestUtils.java b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/CrtHttpClientTestUtils.java new file mode 100644 index 000000000000..d564afd596b8 --- /dev/null +++ b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/CrtHttpClientTestUtils.java @@ -0,0 +1,87 @@ +package software.amazon.awssdk.http.crt; + +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.http.async.SdkAsyncHttpResponseHandler; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; + +import static java.util.Collections.emptyMap; + +public class CrtHttpClientTestUtils { + + static Subscriber createDummySubscriber() { + return new Subscriber() { + @Override + public void onSubscribe(Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(ByteBuffer byteBuffer) { + } + + @Override + public void onError(Throwable throwable) { + } + + @Override + public void onComplete() { + } + }; + } + + static SdkAsyncHttpResponseHandler createTestResponseHandler(AtomicReference response, + CompletableFuture streamReceived, + AtomicReference error, + Subscriber subscriber) { + return new SdkAsyncHttpResponseHandler() { + @Override + public void onHeaders(SdkHttpResponse headers) { + response.compareAndSet(null, headers); + } + @Override + public void onStream(Publisher stream) { + stream.subscribe(subscriber); + streamReceived.complete(true); + } + + @Override + public void onError(Throwable t) { + error.compareAndSet(null, t); + } + }; + } + + public static SdkHttpFullRequest createRequest(URI endpoint) { + return createRequest(endpoint, "/", null, SdkHttpMethod.GET, emptyMap()); + } + + static SdkHttpFullRequest createRequest(URI endpoint, + String resourcePath, + byte[] body, + SdkHttpMethod method, + Map params) { + + String contentLength = (body == null) ? null : String.valueOf(body.length); + return SdkHttpFullRequest.builder() + .uri(endpoint) + .method(method) + .encodedPath(resourcePath) + .applyMutation(b -> params.forEach(b::putRawQueryParameter)) + .applyMutation(b -> { + b.putHeader("Host", endpoint.getHost()); + if (contentLength != null) { + b.putHeader("Content-Length", contentLength); + } + }).build(); + } +} diff --git a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/EmptyPublisher.java b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/EmptyPublisher.java new file mode 100644 index 000000000000..1e85fc43cda6 --- /dev/null +++ b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/EmptyPublisher.java @@ -0,0 +1,45 @@ +package software.amazon.awssdk.http.crt; + +import java.nio.ByteBuffer; +import java.util.Optional; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import software.amazon.awssdk.http.async.SdkHttpContentPublisher; + +public class EmptyPublisher implements SdkHttpContentPublisher { + @Override + public void subscribe(Subscriber subscriber) { + subscriber.onSubscribe(new EmptySubscription(subscriber)); + } + + @Override + public Optional contentLength() { + return Optional.of(0L); + } + + private static class EmptySubscription implements Subscription { + private final Subscriber subscriber; + private volatile boolean done; + + EmptySubscription(Subscriber subscriber) { + this.subscriber = subscriber; + } + + @Override + public void request(long l) { + if (!done) { + done = true; + if (l <= 0) { + this.subscriber.onError(new IllegalArgumentException("Demand must be positive")); + } else { + this.subscriber.onComplete(); + } + } + } + + @Override + public void cancel() { + done = true; + } + } +} diff --git a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/H1ServerBehaviorTest.java b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/H1ServerBehaviorTest.java new file mode 100644 index 000000000000..84c4f9c194b6 --- /dev/null +++ b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/H1ServerBehaviorTest.java @@ -0,0 +1,37 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.crt; + +import static software.amazon.awssdk.http.SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES; + +import software.amazon.awssdk.crt.io.EventLoopGroup; +import software.amazon.awssdk.crt.io.HostResolver; +import software.amazon.awssdk.http.SdkAsyncHttpClientH1TestSuite; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.utils.AttributeMap; + +/** + * Testing the scenario where h1 server sends 5xx errors. + */ +public class H1ServerBehaviorTest extends SdkAsyncHttpClientH1TestSuite { + + @Override + protected SdkAsyncHttpClient setupClient() { + return AwsCrtAsyncHttpClient.builder() + .buildWithDefaults(AttributeMap.builder().put(TRUST_ALL_CERTIFICATES, true).build()); + } + +} diff --git a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/ProxyConfigurationTest.java b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/ProxyConfigurationTest.java new file mode 100644 index 000000000000..3f01c7a7774d --- /dev/null +++ b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/ProxyConfigurationTest.java @@ -0,0 +1,111 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.crt; + +import static org.assertj.core.api.Assertions.assertThat; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Random; +import java.util.stream.Stream; +import org.junit.Test; + +/** + * Tests for {@link ProxyConfiguration}. + */ +public class ProxyConfigurationTest { + private static final Random RNG = new Random(); + + @Test + public void build_setsAllProperties() { + verifyAllPropertiesSet(allPropertiesSetConfig()); + } + + @Test + public void toBuilder_roundTrip_producesExactCopy() { + ProxyConfiguration original = allPropertiesSetConfig(); + + ProxyConfiguration copy = original.toBuilder().build(); + + assertThat(copy).isEqualTo(original); + } + + @Test + public void toBuilderModified_doesNotModifySource() { + ProxyConfiguration original = allPropertiesSetConfig(); + + ProxyConfiguration modified = setAllPropertiesToRandomValues(original.toBuilder()).build(); + + assertThat(original).isNotEqualTo(modified); + } + + private ProxyConfiguration allPropertiesSetConfig() { + return setAllPropertiesToRandomValues(ProxyConfiguration.builder()).build(); + } + + private ProxyConfiguration.Builder setAllPropertiesToRandomValues(ProxyConfiguration.Builder builder) { + Stream.of(builder.getClass().getDeclaredMethods()) + .filter(m -> m.getParameterCount() == 1 && m.getReturnType().equals(ProxyConfiguration.Builder.class)) + .forEach(m -> { + try { + m.setAccessible(true); + setRandomValue(builder, m); + } catch (Exception e) { + throw new RuntimeException("Could not create random proxy config", e); + } + }); + return builder; + } + + private void setRandomValue(Object o, Method setter) throws InvocationTargetException, IllegalAccessException { + Class paramClass = setter.getParameterTypes()[0]; + + if (String.class.equals(paramClass)) { + setter.invoke(o, randomString()); + } else if (int.class.equals(paramClass)) { + setter.invoke(o, RNG.nextInt()); + } else { + throw new RuntimeException("Don't know how create random value for type " + paramClass); + } + } + + private void verifyAllPropertiesSet(ProxyConfiguration cfg) { + boolean hasNullProperty = Stream.of(cfg.getClass().getDeclaredMethods()) + .filter(m -> !m.getReturnType().equals(Void.class) && m.getParameterCount() == 0) + .anyMatch(m -> { + m.setAccessible(true); + try { + return m.invoke(cfg) == null; + } catch (Exception e) { + return true; + } + }); + + if (hasNullProperty) { + throw new RuntimeException("Given configuration has unset property"); + } + } + + private String randomString() { + String alpha = "abcdefghijklmnopqrstuwxyz"; + + StringBuilder sb = new StringBuilder(16); + for (int i = 0; i < 16; ++i) { + sb.append(alpha.charAt(RNG.nextInt(16))); + } + + return sb.toString(); + } +} diff --git a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/ProxyWireMockTest.java b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/ProxyWireMockTest.java new file mode 100644 index 000000000000..1487344b099e --- /dev/null +++ b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/ProxyWireMockTest.java @@ -0,0 +1,123 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + + +package software.amazon.awssdk.http.crt; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; +import static java.util.Collections.emptyMap; +import static org.assertj.core.api.Assertions.assertThat; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.reactivestreams.Subscriber; +import software.amazon.awssdk.crt.CrtResource; +import software.amazon.awssdk.crt.io.EventLoopGroup; +import software.amazon.awssdk.crt.io.HostResolver; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.http.async.AsyncExecuteRequest; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.http.async.SdkAsyncHttpResponseHandler; + +/** + * Tests for HTTP proxy functionality in the CRT client. + */ +public class ProxyWireMockTest { + private SdkAsyncHttpClient client; + + private ProxyConfiguration proxyCfg; + + private WireMockServer mockProxy = new WireMockServer(new WireMockConfiguration() + .dynamicPort() + .dynamicHttpsPort() + .enableBrowserProxying(true)); // make the mock proxy actually forward (to the mock server for our test) + + private WireMockServer mockServer = new WireMockServer(new WireMockConfiguration() + .dynamicPort() + .dynamicHttpsPort()); + + + @Before + public void setup() { + mockProxy.start(); + mockServer.start(); + + mockServer.stubFor(get(urlMatching(".*")).willReturn(aResponse().withStatus(200).withBody("hello"))); + + proxyCfg = ProxyConfiguration.builder() + .host("localhost") + .port(mockProxy.port()) + .build(); + + client = AwsCrtAsyncHttpClient.builder() + .proxyConfiguration(proxyCfg) + .build(); + } + + @After + public void teardown() { + mockServer.stop(); + mockProxy.stop(); + client.close(); + EventLoopGroup.closeStaticDefault(); + HostResolver.closeStaticDefault(); + CrtResource.waitForNoResources(); + } + + /* + * Note the contrast between this test and the netty connect test. The CRT proxy implementation does not + * do a CONNECT call for requests using http, so by configuring the proxy mock to forward and the server mock + * to return success, we can actually create an end-to-end test. + * + * We have an outstanding request to change this behavior to match https (use a CONNECT call). Once that + * change happens, this test will break and need to be updated to be more like the netty one. + */ + @Test + public void proxyConfigured_httpGet() throws Throwable { + + CompletableFuture streamReceived = new CompletableFuture<>(); + AtomicReference response = new AtomicReference<>(null); + AtomicReference error = new AtomicReference<>(null); + + Subscriber subscriber = CrtHttpClientTestUtils.createDummySubscriber(); + + SdkAsyncHttpResponseHandler handler = CrtHttpClientTestUtils.createTestResponseHandler(response, streamReceived, error, subscriber); + + URI uri = URI.create("http://localhost:" + mockServer.port()); + SdkHttpRequest request = CrtHttpClientTestUtils.createRequest(uri, "/server/test", null, SdkHttpMethod.GET, emptyMap()); + + CompletableFuture future = client.execute(AsyncExecuteRequest.builder() + .request(request) + .responseHandler(handler) + .requestContentPublisher(new EmptyPublisher()) + .build()); + future.get(60, TimeUnit.SECONDS); + assertThat(error.get()).isNull(); + assertThat(streamReceived.get(60, TimeUnit.SECONDS)).isTrue(); + assertThat(response.get().statusCode()).isEqualTo(200); + } + +} diff --git a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/SdkTestHttpContentPublisher.java b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/SdkTestHttpContentPublisher.java new file mode 100644 index 000000000000..3ad5f08ac0c0 --- /dev/null +++ b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/SdkTestHttpContentPublisher.java @@ -0,0 +1,56 @@ +package software.amazon.awssdk.http.crt; + +import java.nio.ByteBuffer; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import software.amazon.awssdk.http.async.SdkHttpContentPublisher; + +public class SdkTestHttpContentPublisher implements SdkHttpContentPublisher { + private final byte[] body; + private final AtomicReference> subscriber = new AtomicReference<>(null); + private final AtomicBoolean complete = new AtomicBoolean(false); + + public SdkTestHttpContentPublisher(byte[] body) { + this.body = body; + } + + @Override + public void subscribe(Subscriber s) { + boolean wasFirstSubscriber = subscriber.compareAndSet(null, s); + + SdkTestHttpContentPublisher publisher = this; + + if (wasFirstSubscriber) { + s.onSubscribe(new Subscription() { + @Override + public void request(long n) { + publisher.request(n); + } + + @Override + public void cancel() { + // Do nothing + } + }); + } else { + s.onError(new RuntimeException("Only allow one subscriber")); + } + } + + protected void request(long n) { + // Send the whole body if they request >0 ByteBuffers + if (n > 0 && !complete.get()) { + complete.set(true); + subscriber.get().onNext(ByteBuffer.wrap(body)); + subscriber.get().onComplete(); + } + } + + @Override + public Optional contentLength() { + return Optional.of((long)body.length); + } +} diff --git a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/Sha256BodySubscriber.java b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/Sha256BodySubscriber.java new file mode 100644 index 000000000000..508deffcb199 --- /dev/null +++ b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/Sha256BodySubscriber.java @@ -0,0 +1,44 @@ +package software.amazon.awssdk.http.crt; + +import static org.apache.commons.codec.binary.Hex.encodeHexString; + +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.concurrent.CompletableFuture; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +public class Sha256BodySubscriber implements Subscriber { + private MessageDigest digest; + private CompletableFuture future; + + public Sha256BodySubscriber() throws NoSuchAlgorithmException { + digest = MessageDigest.getInstance("SHA-256"); + future = new CompletableFuture<>(); + } + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(ByteBuffer byteBuffer) { + digest.update(byteBuffer); + } + + @Override + public void onError(Throwable t) { + future.completeExceptionally(t); + } + + @Override + public void onComplete() { + future.complete(encodeHexString(digest.digest()).toUpperCase()); + } + + public CompletableFuture getFuture() { + return future; + } +} diff --git a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/internal/CrtRequestExecutorTest.java b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/internal/CrtRequestExecutorTest.java new file mode 100644 index 000000000000..3c10564d3811 --- /dev/null +++ b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/internal/CrtRequestExecutorTest.java @@ -0,0 +1,164 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.crt.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.http.HttpTestUtils.createProvider; +import static software.amazon.awssdk.http.crt.CrtHttpClientTestUtils.createRequest; + +import java.io.IOException; +import java.net.URI; +import java.util.concurrent.CompletableFuture; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; +import software.amazon.awssdk.crt.CrtRuntimeException; +import software.amazon.awssdk.crt.http.HttpClientConnection; +import software.amazon.awssdk.crt.http.HttpClientConnectionManager; +import software.amazon.awssdk.crt.http.HttpRequest; +import software.amazon.awssdk.http.SdkCancellationException; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.http.async.AsyncExecuteRequest; +import software.amazon.awssdk.http.async.SdkAsyncHttpResponseHandler; + +@RunWith(MockitoJUnitRunner.class) +public class CrtRequestExecutorTest { + + private CrtRequestExecutor requestExecutor; + @Mock + private HttpClientConnectionManager connectionManager; + + @Mock + private SdkAsyncHttpResponseHandler responseHandler; + + @Mock + private HttpClientConnection httpClientConnection; + + @Before + public void setup() { + requestExecutor = new CrtRequestExecutor(); + } + + @After + public void teardown() { + Mockito.reset(connectionManager, responseHandler, httpClientConnection); + } + + @Test + public void acquireConnectionThrowException_shouldInvokeOnError() { + RuntimeException exception = new RuntimeException("error"); + CrtRequestContext context = CrtRequestContext.builder() + .crtConnPool(connectionManager) + .request(AsyncExecuteRequest.builder() + .responseHandler(responseHandler) + .build()) + .build(); + CompletableFuture completableFuture = new CompletableFuture<>(); + + Mockito.when(connectionManager.acquireConnection()).thenReturn(completableFuture); + completableFuture.completeExceptionally(exception); + + CompletableFuture executeFuture = requestExecutor.execute(context); + + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Exception.class); + Mockito.verify(responseHandler).onError(argumentCaptor.capture()); + + Exception actualException = argumentCaptor.getValue(); + assertThat(actualException).hasMessageContaining("An exception occurred when acquiring connection"); + assertThat(actualException).hasCause(exception); + assertThat(executeFuture).hasFailedWithThrowableThat().hasCause(exception).isInstanceOf(IOException.class); + } + + @Test + public void makeRequestThrowException_shouldInvokeOnError() { + CrtRuntimeException exception = new CrtRuntimeException(""); + SdkHttpFullRequest request = createRequest(URI.create("http://localhost")); + CrtRequestContext context = CrtRequestContext.builder() + .readBufferSize(2000) + .crtConnPool(connectionManager) + .request(AsyncExecuteRequest.builder() + .request(request) + .requestContentPublisher(createProvider("")) + .responseHandler(responseHandler) + .build()) + .build(); + CompletableFuture completableFuture = new CompletableFuture<>(); + + Mockito.when(connectionManager.acquireConnection()).thenReturn(completableFuture); + completableFuture.complete(httpClientConnection); + + Mockito.when(httpClientConnection.makeRequest(Mockito.any(HttpRequest.class), Mockito.any(AwsCrtAsyncHttpStreamAdapter.class))) + .thenThrow(exception); + + CompletableFuture executeFuture = requestExecutor.execute(context); + + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Exception.class); + Mockito.verify(responseHandler).onError(argumentCaptor.capture()); + + Exception actualException = argumentCaptor.getValue(); + assertThat(actualException).hasMessageContaining("An exception occurred when making the request"); + assertThat(actualException).hasCause(exception); + assertThat(executeFuture).hasFailedWithThrowableThat().hasCause(exception).isInstanceOf(IOException.class); + } + + @Test + public void makeRequest_success() { + SdkHttpFullRequest request = createRequest(URI.create("http://localhost")); + CrtRequestContext context = CrtRequestContext.builder() + .readBufferSize(2000) + .crtConnPool(connectionManager) + .request(AsyncExecuteRequest.builder() + .request(request) + .requestContentPublisher(createProvider("")) + .responseHandler(responseHandler) + .build()) + .build(); + CompletableFuture completableFuture = new CompletableFuture<>(); + Mockito.when(connectionManager.acquireConnection()).thenReturn(completableFuture); + completableFuture.complete(httpClientConnection); + + CompletableFuture executeFuture = requestExecutor.execute(context); + Mockito.verifyZeroInteractions(responseHandler); + } + + @Test + public void cancelRequest_shouldInvokeOnError() { + CrtRequestContext context = CrtRequestContext.builder() + .crtConnPool(connectionManager) + .request(AsyncExecuteRequest.builder() + .responseHandler(responseHandler) + .build()) + .build(); + CompletableFuture completableFuture = new CompletableFuture<>(); + + Mockito.when(connectionManager.acquireConnection()).thenReturn(completableFuture); + + CompletableFuture executeFuture = requestExecutor.execute(context); + executeFuture.cancel(true); + + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Exception.class); + Mockito.verify(responseHandler).onError(argumentCaptor.capture()); + + Exception actualException = argumentCaptor.getValue(); + assertThat(actualException).hasMessageContaining("The request was cancelled"); + assertThat(actualException).isInstanceOf(SdkCancellationException.class); + } +} diff --git a/http-clients/aws-crt-client/src/test/resources/jetty-logging.properties b/http-clients/aws-crt-client/src/test/resources/jetty-logging.properties new file mode 100644 index 000000000000..4ee410e7fa92 --- /dev/null +++ b/http-clients/aws-crt-client/src/test/resources/jetty-logging.properties @@ -0,0 +1,18 @@ +# +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# +# http://aws.amazon.com/apache2.0 +# +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. +# + +# Set up logging implementation +org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog +org.eclipse.jetty.LEVEL=OFF diff --git a/http-clients/aws-crt-client/src/test/resources/log4j.properties b/http-clients/aws-crt-client/src/test/resources/log4j.properties new file mode 100644 index 000000000000..5a6e0a5388d9 --- /dev/null +++ b/http-clients/aws-crt-client/src/test/resources/log4j.properties @@ -0,0 +1,24 @@ +# +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# +# http://aws.amazon.com/apache2.0 +# +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. +# + +log4j.rootLogger=WARN, A1 +log4j.appender.A1=org.apache.log4j.ConsoleAppender +log4j.appender.A1.layout=org.apache.log4j.PatternLayout + +# Print the date in ISO 8601 format +log4j.appender.A1.layout.ConversionPattern=%d [%t] %-5p %c - %m%n + + + diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/Http2MetricsTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/Http2MetricsTest.java index f6da53d17caa..fe79f1e51b34 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/Http2MetricsTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/Http2MetricsTest.java @@ -40,6 +40,7 @@ import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; +import software.amazon.awssdk.http.EmptyPublisher; import software.amazon.awssdk.http.Http2Metric; import software.amazon.awssdk.http.HttpMetric; import software.amazon.awssdk.http.Protocol; diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyClientTlsAuthTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyClientTlsAuthTest.java index b9533c8527cd..936a056425f5 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyClientTlsAuthTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyClientTlsAuthTest.java @@ -31,6 +31,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import software.amazon.awssdk.http.EmptyPublisher; import software.amazon.awssdk.http.FileStoreTlsKeyManagersProvider; import software.amazon.awssdk.http.SdkHttpFullRequest; import software.amazon.awssdk.http.SdkHttpMethod; diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClientSpiVerificationTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClientSpiVerificationTest.java index 9b992cc90918..a4e4047fde13 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClientSpiVerificationTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClientSpiVerificationTest.java @@ -41,6 +41,7 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import software.amazon.awssdk.http.EmptyPublisher; import software.amazon.awssdk.http.SdkHttpConfigurationOption; import software.amazon.awssdk.http.SdkHttpFullRequest; import software.amazon.awssdk.http.SdkHttpMethod; diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/ProxyWireMockTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/ProxyWireMockTest.java index 9a0b45094eec..f797a760fdf7 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/ProxyWireMockTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/ProxyWireMockTest.java @@ -31,6 +31,7 @@ import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; +import software.amazon.awssdk.http.EmptyPublisher; import software.amazon.awssdk.http.SdkHttpFullRequest; import software.amazon.awssdk.http.SdkHttpMethod; import software.amazon.awssdk.http.async.AsyncExecuteRequest; diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/TestUtils.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/TestUtils.java deleted file mode 100644 index af007e9c6d5a..000000000000 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/TestUtils.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.http.nio.netty; - - -import io.reactivex.Flowable; -import java.nio.ByteBuffer; -import java.util.concurrent.CompletableFuture; -import org.reactivestreams.Publisher; -import software.amazon.awssdk.http.SdkHttpFullRequest; -import software.amazon.awssdk.http.SdkHttpMethod; -import software.amazon.awssdk.http.SdkHttpResponse; -import software.amazon.awssdk.http.async.AsyncExecuteRequest; -import software.amazon.awssdk.http.async.SdkAsyncHttpClient; -import software.amazon.awssdk.http.async.SdkAsyncHttpResponseHandler; - -public class TestUtils { - - public static CompletableFuture sendGetRequest(int serverPort, SdkAsyncHttpClient client) { - AsyncExecuteRequest req = AsyncExecuteRequest.builder() - .responseHandler(new SdkAsyncHttpResponseHandler() { - private SdkHttpResponse headers; - - @Override - public void onHeaders(SdkHttpResponse headers) { - this.headers = headers; - } - - @Override - public void onStream(Publisher stream) { - Flowable.fromPublisher(stream).forEach(b -> { - }); - } - - @Override - public void onError(Throwable error) { - } - }) - .request(SdkHttpFullRequest.builder() - .method(SdkHttpMethod.GET) - .protocol("https") - .host("localhost") - .port(serverPort) - .build()) - .requestContentPublisher(new EmptyPublisher()) - .build(); - - return client.execute(req); - } -} diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/fault/GoAwayTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/fault/GoAwayTest.java index f46480dc29a2..957dcaa7fc71 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/fault/GoAwayTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/fault/GoAwayTest.java @@ -65,7 +65,7 @@ import software.amazon.awssdk.http.async.AsyncExecuteRequest; import software.amazon.awssdk.http.async.SdkAsyncHttpClient; import software.amazon.awssdk.http.async.SdkAsyncHttpResponseHandler; -import software.amazon.awssdk.http.nio.netty.EmptyPublisher; +import software.amazon.awssdk.http.EmptyPublisher; import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; import software.amazon.awssdk.http.nio.netty.SdkEventLoopGroup; import software.amazon.awssdk.http.nio.netty.internal.http2.GoAwayException; diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/fault/H1ServerErrorTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/fault/H1ServerErrorTest.java index 27d886fd6fb1..4df586e5a923 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/fault/H1ServerErrorTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/fault/H1ServerErrorTest.java @@ -15,41 +15,9 @@ package software.amazon.awssdk.http.nio.netty.fault; -import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH; -import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; -import static io.netty.handler.codec.http.HttpHeaderValues.TEXT_PLAIN; -import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR; -import static io.netty.handler.codec.http.HttpResponseStatus.OK; -import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import static software.amazon.awssdk.http.SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES; -import static software.amazon.awssdk.http.nio.netty.TestUtils.sendGetRequest; -import io.netty.bootstrap.ServerBootstrap; -import io.netty.buffer.Unpooled; -import io.netty.channel.ChannelDuplexHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInitializer; -import io.netty.channel.ChannelPipeline; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.ServerSocketChannel; -import io.netty.channel.socket.SocketChannel; -import io.netty.channel.socket.nio.NioServerSocketChannel; -import io.netty.handler.codec.http.DefaultFullHttpResponse; -import io.netty.handler.codec.http.FullHttpResponse; -import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.netty.handler.codec.http.HttpServerCodec; -import io.netty.handler.codec.http.HttpVersion; -import io.netty.handler.logging.LogLevel; -import io.netty.handler.logging.LoggingHandler; -import io.netty.handler.ssl.SslContext; -import io.netty.handler.ssl.SslContextBuilder; -import io.netty.handler.ssl.util.SelfSignedCertificate; -import java.util.ArrayList; -import java.util.List; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; +import software.amazon.awssdk.http.SdkAsyncHttpClientH1TestSuite; import software.amazon.awssdk.http.Protocol; import software.amazon.awssdk.http.async.SdkAsyncHttpClient; import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; @@ -60,114 +28,13 @@ /** * Testing the scenario where h1 server sends 5xx errors. */ -public class H1ServerErrorTest { - private SdkAsyncHttpClient netty; - private Server server; - - - @Before - public void setup() throws Exception { - server = new Server(); - server.init(); - - netty = NettyNioAsyncHttpClient.builder() - .eventLoopGroup(SdkEventLoopGroup.builder().numberOfThreads(2).build()) - .protocol(Protocol.HTTP1_1) - .buildWithDefaults(AttributeMap.builder().put(TRUST_ALL_CERTIFICATES, true).build()); - } - - - @After - public void teardown() throws InterruptedException { - if (server != null) { - server.shutdown(); - } - server = null; - - if (netty != null) { - netty.close(); - } - netty = null; - } - - @Test - public void connectionReceive500_shouldNotReuseConnection() throws Exception { - server.return500OnFirstRequest = true; - - sendGetRequest(server.port(), netty).join(); - sendGetRequest(server.port(), netty).join(); - assertThat(server.channels.size()).isEqualTo(2); - } - - @Test - public void connectionReceive200_shouldReuseConnection() { - server.return500OnFirstRequest = false; - - sendGetRequest(server.port(), netty).join(); - sendGetRequest(server.port(), netty).join(); - assertThat(server.channels.size()).isEqualTo(1); - } - - private static class Server extends ChannelInitializer { - private static final byte[] CONTENT = "helloworld".getBytes(); - private ServerBootstrap bootstrap; - private ServerSocketChannel serverSock; - private List channels = new ArrayList<>(); - private final NioEventLoopGroup group = new NioEventLoopGroup(); - private SslContext sslCtx; - private boolean return500OnFirstRequest; - - public void init() throws Exception { - SelfSignedCertificate ssc = new SelfSignedCertificate(); - sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build(); - - bootstrap = new ServerBootstrap() - .channel(NioServerSocketChannel.class) - .handler(new LoggingHandler(LogLevel.DEBUG)) - .group(group) - .childHandler(this); - - serverSock = (ServerSocketChannel) bootstrap.bind(0).sync().channel(); - } - - @Override - protected void initChannel(SocketChannel ch) throws Exception { - channels.add(ch); - ChannelPipeline pipeline = ch.pipeline(); - pipeline.addLast(sslCtx.newHandler(ch.alloc())); - pipeline.addLast(new HttpServerCodec()); - pipeline.addLast(new MightReturn500ChannelHandler()); - } - - public void shutdown() throws InterruptedException { - group.shutdownGracefully().await(); - } - - public int port() { - return serverSock.localAddress().getPort(); - } - - private class MightReturn500ChannelHandler extends ChannelDuplexHandler { - - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) { - if (msg instanceof HttpRequest) { - HttpResponseStatus status; - if (ctx.channel().equals(channels.get(0)) && return500OnFirstRequest) { - status = INTERNAL_SERVER_ERROR; - } else { - status = OK; - } - - FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, - Unpooled.wrappedBuffer(CONTENT)); - - response.headers() - .set(CONTENT_TYPE, TEXT_PLAIN) - .setInt(CONTENT_LENGTH, response.content().readableBytes()); - ctx.writeAndFlush(response); - } - } - } +public class H1ServerErrorTest extends SdkAsyncHttpClientH1TestSuite { + + @Override + protected SdkAsyncHttpClient setupClient() { + return NettyNioAsyncHttpClient.builder() + .eventLoopGroup(SdkEventLoopGroup.builder().numberOfThreads(2).build()) + .protocol(Protocol.HTTP1_1) + .buildWithDefaults(AttributeMap.builder().put(TRUST_ALL_CERTIFICATES, true).build()); } } diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/fault/H2ServerErrorTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/fault/H2ServerErrorTest.java index ebd11e7a2c58..bf22b813b15e 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/fault/H2ServerErrorTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/fault/H2ServerErrorTest.java @@ -19,7 +19,7 @@ import static io.netty.handler.codec.http.HttpResponseStatus.OK; import static org.assertj.core.api.Assertions.assertThat; import static software.amazon.awssdk.http.SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES; -import static software.amazon.awssdk.http.nio.netty.TestUtils.sendGetRequest; +import static software.amazon.awssdk.http.HttpTestUtils.sendGetRequest; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/fault/PingTimeoutTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/fault/PingTimeoutTest.java index a309addf27ff..f88c5af2bfcd 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/fault/PingTimeoutTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/fault/PingTimeoutTest.java @@ -67,7 +67,7 @@ import software.amazon.awssdk.http.async.AsyncExecuteRequest; import software.amazon.awssdk.http.async.SdkAsyncHttpClient; import software.amazon.awssdk.http.async.SdkAsyncHttpResponseHandler; -import software.amazon.awssdk.http.nio.netty.EmptyPublisher; +import software.amazon.awssdk.http.EmptyPublisher; import software.amazon.awssdk.http.nio.netty.Http2Configuration; import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; import software.amazon.awssdk.http.nio.netty.internal.http2.PingFailedException; diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/fault/ServerCloseConnectionTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/fault/ServerCloseConnectionTest.java index 88e0c50ff43a..cc6fbda166b5 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/fault/ServerCloseConnectionTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/fault/ServerCloseConnectionTest.java @@ -63,7 +63,7 @@ import software.amazon.awssdk.http.async.AsyncExecuteRequest; import software.amazon.awssdk.http.async.SdkAsyncHttpClient; import software.amazon.awssdk.http.async.SdkAsyncHttpResponseHandler; -import software.amazon.awssdk.http.nio.netty.EmptyPublisher; +import software.amazon.awssdk.http.EmptyPublisher; import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; import software.amazon.awssdk.http.nio.netty.SdkEventLoopGroup; import software.amazon.awssdk.utils.AttributeMap; diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/fault/ServerNotRespondingTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/fault/ServerNotRespondingTest.java index 92d3624febf1..88eb36716106 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/fault/ServerNotRespondingTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/fault/ServerNotRespondingTest.java @@ -65,7 +65,7 @@ import software.amazon.awssdk.http.async.AsyncExecuteRequest; import software.amazon.awssdk.http.async.SdkAsyncHttpClient; import software.amazon.awssdk.http.async.SdkAsyncHttpResponseHandler; -import software.amazon.awssdk.http.nio.netty.EmptyPublisher; +import software.amazon.awssdk.http.EmptyPublisher; import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; import software.amazon.awssdk.http.nio.netty.SdkEventLoopGroup; import software.amazon.awssdk.utils.AttributeMap; diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ConnectionReaperTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ConnectionReaperTest.java index 33fb9d9b906b..0ce25a0f6ebb 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ConnectionReaperTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ConnectionReaperTest.java @@ -43,7 +43,7 @@ import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.http.async.AsyncExecuteRequest; import software.amazon.awssdk.http.async.SdkAsyncHttpClient; -import software.amazon.awssdk.http.nio.netty.EmptyPublisher; +import software.amazon.awssdk.http.EmptyPublisher; import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; import software.amazon.awssdk.http.nio.netty.RecordingResponseHandler; diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ResponseCompletionTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ResponseCompletionTest.java index 0f6f786eb9d0..56601cf8bdb3 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ResponseCompletionTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ResponseCompletionTest.java @@ -60,7 +60,7 @@ import software.amazon.awssdk.http.async.AsyncExecuteRequest; import software.amazon.awssdk.http.async.SdkAsyncHttpClient; import software.amazon.awssdk.http.async.SdkAsyncHttpResponseHandler; -import software.amazon.awssdk.http.nio.netty.EmptyPublisher; +import software.amazon.awssdk.http.EmptyPublisher; import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; import software.amazon.awssdk.http.nio.netty.SdkEventLoopGroup; import software.amazon.awssdk.utils.AttributeMap; diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/http2/WindowSizeTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/http2/WindowSizeTest.java index 7210708d7b59..e33ddfcb6e17 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/http2/WindowSizeTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/http2/WindowSizeTest.java @@ -54,7 +54,7 @@ import software.amazon.awssdk.http.async.AsyncExecuteRequest; import software.amazon.awssdk.http.async.SdkAsyncHttpClient; import software.amazon.awssdk.http.async.SdkAsyncHttpResponseHandler; -import software.amazon.awssdk.http.nio.netty.EmptyPublisher; +import software.amazon.awssdk.http.EmptyPublisher; import software.amazon.awssdk.http.nio.netty.Http2Configuration; import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; diff --git a/http-clients/pom.xml b/http-clients/pom.xml index bd3eb8fb1845..668687375a78 100644 --- a/http-clients/pom.xml +++ b/http-clients/pom.xml @@ -31,6 +31,7 @@ apache-client + aws-crt-client netty-nio-client url-connection-client diff --git a/pom.xml b/pom.xml index 6c64b09d7ce1..033d49f38e7e 100644 --- a/pom.xml +++ b/pom.xml @@ -109,6 +109,7 @@ 2.1.9 1.10 1.21 + 0.8.2 4.12 diff --git a/test/http-client-tests/pom.xml b/test/http-client-tests/pom.xml index 6e8cd6d1726b..dd10f5f547ef 100644 --- a/test/http-client-tests/pom.xml +++ b/test/http-client-tests/pom.xml @@ -48,6 +48,11 @@ http-client-spi ${awsjavasdk.version} + + software.amazon.awssdk + metrics-spi + ${awsjavasdk.version} + software.amazon.awssdk utils @@ -73,5 +78,30 @@ wiremock compile + + io.reactivex.rxjava2 + rxjava + compile + + + io.netty + netty-codec-http + + + io.netty + netty-transport + + + io.netty + netty-common + + + io.netty + netty-buffer + + + io.netty + netty-handler + diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/EmptyPublisher.java b/test/http-client-tests/src/main/java/software/amazon/awssdk/http/EmptyPublisher.java similarity index 97% rename from http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/EmptyPublisher.java rename to test/http-client-tests/src/main/java/software/amazon/awssdk/http/EmptyPublisher.java index 1f1308a2f07f..fa5728d8e6f5 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/EmptyPublisher.java +++ b/test/http-client-tests/src/main/java/software/amazon/awssdk/http/EmptyPublisher.java @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -package software.amazon.awssdk.http.nio.netty; +package software.amazon.awssdk.http; import java.nio.ByteBuffer; import java.util.Optional; diff --git a/test/http-client-tests/src/main/java/software/amazon/awssdk/http/HttpTestUtils.java b/test/http-client-tests/src/main/java/software/amazon/awssdk/http/HttpTestUtils.java index e858a6bc145f..6660d990df2e 100644 --- a/test/http-client-tests/src/main/java/software/amazon/awssdk/http/HttpTestUtils.java +++ b/test/http-client-tests/src/main/java/software/amazon/awssdk/http/HttpTestUtils.java @@ -16,11 +16,28 @@ package software.amazon.awssdk.http; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static java.nio.charset.StandardCharsets.UTF_8; +import static software.amazon.awssdk.utils.StringUtils.isBlank; import com.github.tomakehurst.wiremock.WireMockServer; +import io.reactivex.Flowable; import java.io.InputStream; import java.net.URL; +import java.nio.ByteBuffer; import java.security.KeyStore; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import software.amazon.awssdk.http.async.AsyncExecuteRequest; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.http.async.SdkAsyncHttpResponseHandler; +import software.amazon.awssdk.http.async.SdkHttpContentPublisher; public class HttpTestUtils { private HttpTestUtils() { @@ -46,4 +63,75 @@ public static KeyStore getSelfSignedKeyStore() throws Exception { return keyStore; } + + public static CompletableFuture sendGetRequest(int serverPort, SdkAsyncHttpClient client) { + AsyncExecuteRequest req = AsyncExecuteRequest.builder() + .responseHandler(new SdkAsyncHttpResponseHandler() { + private SdkHttpResponse headers; + + @Override + public void onHeaders(SdkHttpResponse headers) { + this.headers = headers; + } + + @Override + public void onStream(Publisher stream) { + Flowable.fromPublisher(stream).forEach(b -> { + }); + } + + @Override + public void onError(Throwable error) { + } + }) + .request(SdkHttpFullRequest.builder() + .method(SdkHttpMethod.GET) + .protocol("https") + .host("127.0.0.1") + .port(serverPort) + .build()) + .requestContentPublisher(new EmptyPublisher()) + .build(); + + return client.execute(req); + } + + public static SdkHttpContentPublisher createProvider(String body) { + Stream chunks = splitStringBySize(body).stream() + .map(chunk -> ByteBuffer.wrap(chunk.getBytes(UTF_8))); + return new SdkHttpContentPublisher() { + + @Override + public Optional contentLength() { + return Optional.of(Long.valueOf(body.length())); + } + + @Override + public void subscribe(Subscriber s) { + s.onSubscribe(new Subscription() { + @Override + public void request(long n) { + chunks.forEach(s::onNext); + s.onComplete(); + } + + @Override + public void cancel() { + + } + }); + } + }; + } + + public static Collection splitStringBySize(String str) { + if (isBlank(str)) { + return Collections.emptyList(); + } + ArrayList split = new ArrayList<>(); + for (int i = 0; i <= str.length() / 1000; i++) { + split.add(str.substring(i * 1000, Math.min((i + 1) * 1000, str.length()))); + } + return split; + } } diff --git a/test/http-client-tests/src/main/java/software/amazon/awssdk/http/RecordingNetworkTrafficListener.java b/test/http-client-tests/src/main/java/software/amazon/awssdk/http/RecordingNetworkTrafficListener.java new file mode 100644 index 000000000000..33766fc4242e --- /dev/null +++ b/test/http-client-tests/src/main/java/software/amazon/awssdk/http/RecordingNetworkTrafficListener.java @@ -0,0 +1,58 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http; + +import com.github.tomakehurst.wiremock.http.trafficlistener.WiremockNetworkTrafficListener; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +/** + * Simple implementation of {@link WiremockNetworkTrafficListener} to record all requests received as a string for later + * verification. + */ +public class RecordingNetworkTrafficListener implements WiremockNetworkTrafficListener { + private final StringBuilder requests = new StringBuilder(); + + + @Override + public void opened(Socket socket) { + + } + + @Override + public void incoming(Socket socket, ByteBuffer byteBuffer) { + requests.append(StandardCharsets.UTF_8.decode(byteBuffer)); + } + + @Override + public void outgoing(Socket socket, ByteBuffer byteBuffer) { + + } + + @Override + public void closed(Socket socket) { + + } + + public void reset() { + requests.setLength(0); + } + + public StringBuilder requests() { + return requests; + } +} \ No newline at end of file diff --git a/test/http-client-tests/src/main/java/software/amazon/awssdk/http/RecordingResponseHandler.java b/test/http-client-tests/src/main/java/software/amazon/awssdk/http/RecordingResponseHandler.java new file mode 100644 index 000000000000..687d6047d759 --- /dev/null +++ b/test/http-client-tests/src/main/java/software/amazon/awssdk/http/RecordingResponseHandler.java @@ -0,0 +1,85 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import org.reactivestreams.Publisher; +import software.amazon.awssdk.http.async.SdkAsyncHttpResponseHandler; +import software.amazon.awssdk.http.async.SimpleSubscriber; +import software.amazon.awssdk.metrics.MetricCollector; + +public final class RecordingResponseHandler implements SdkAsyncHttpResponseHandler { + + private final List responses = new ArrayList<>(); + private final StringBuilder bodyParts = new StringBuilder(); + private final CompletableFuture completeFuture = new CompletableFuture<>(); + private final MetricCollector collector = MetricCollector.create("test"); + + @Override + public void onHeaders(SdkHttpResponse response) { + responses.add(response); + } + + @Override + public void onStream(Publisher publisher) { + publisher.subscribe(new SimpleSubscriber(byteBuffer -> { + byte[] b = new byte[byteBuffer.remaining()]; + byteBuffer.duplicate().get(b); + bodyParts.append(new String(b, StandardCharsets.UTF_8)); + }) { + + @Override + public void onError(Throwable t) { + completeFuture.completeExceptionally(t); + } + + @Override + public void onComplete() { + completeFuture.complete(null); + } + }); + } + + @Override + public void onError(Throwable error) { + completeFuture.completeExceptionally(error); + + } + + public String fullResponseAsString() { + return bodyParts.toString(); + } + + public List responses() { + return responses; + } + + public StringBuilder bodyParts() { + return bodyParts; + } + + public CompletableFuture completeFuture() { + return completeFuture; + } + + public MetricCollector collector() { + return collector; + } +} diff --git a/test/http-client-tests/src/main/java/software/amazon/awssdk/http/SdkAsyncHttpClientH1TestSuite.java b/test/http-client-tests/src/main/java/software/amazon/awssdk/http/SdkAsyncHttpClientH1TestSuite.java new file mode 100644 index 000000000000..a17ff0887a17 --- /dev/null +++ b/test/http-client-tests/src/main/java/software/amazon/awssdk/http/SdkAsyncHttpClientH1TestSuite.java @@ -0,0 +1,190 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http; + +import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION; +import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH; +import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; +import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE; +import static io.netty.handler.codec.http.HttpHeaderValues.TEXT_PLAIN; +import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR; +import static io.netty.handler.codec.http.HttpResponseStatus.OK; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.ServerSocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; + +/** + * A set of tests validating that the functionality implemented by a {@link SdkAsyncHttpClient} for HTTP/1 requests + * + * This is used by an HTTP plugin implementation by extending this class and implementing the abstract methods to provide this + * suite with a testable HTTP client implementation. + */ +public abstract class SdkAsyncHttpClientH1TestSuite { + private Server server; + private SdkAsyncHttpClient client; + + protected abstract SdkAsyncHttpClient setupClient(); + + @Before + public void setup() throws Exception { + server = new Server(); + server.init(); + + this.client = setupClient(); + } + + @After + public void teardown() throws InterruptedException { + if (server != null) { + server.shutdown(); + } + + if (client != null) { + client.close(); + } + server = null; + } + + @Test + public void connectionReceiveServerErrorStatusShouldNotReuseConnection() { + server.return500OnFirstRequest = true; + server.closeConnection = false; + + HttpTestUtils.sendGetRequest(server.port(), client).join(); + HttpTestUtils.sendGetRequest(server.port(), client).join(); + assertThat(server.channels.size()).isEqualTo(2); + } + + @Test + public void connectionReceiveOkStatusShouldReuseConnection() { + server.return500OnFirstRequest = false; + server.closeConnection = false; + + HttpTestUtils.sendGetRequest(server.port(), client).join(); + HttpTestUtils.sendGetRequest(server.port(), client).join(); + + assertThat(server.channels.size()).isEqualTo(1); + } + + @Test + public void connectionReceiveCloseHeaderShouldNotReuseConnection() throws InterruptedException { + server.return500OnFirstRequest = false; + server.closeConnection = true; + + HttpTestUtils.sendGetRequest(server.port(), client).join(); + Thread.sleep(1000); + + HttpTestUtils.sendGetRequest(server.port(), client).join(); + assertThat(server.channels.size()).isEqualTo(2); + } + + private static class Server extends ChannelInitializer { + private static final byte[] CONTENT = "helloworld".getBytes(StandardCharsets.UTF_8); + private ServerBootstrap bootstrap; + private ServerSocketChannel serverSock; + private List channels = new ArrayList<>(); + private final NioEventLoopGroup group = new NioEventLoopGroup(); + private SslContext sslCtx; + private boolean return500OnFirstRequest; + private boolean closeConnection; + + public void init() throws Exception { + SelfSignedCertificate ssc = new SelfSignedCertificate(); + sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build(); + + bootstrap = new ServerBootstrap() + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.DEBUG)) + .group(group) + .childHandler(this); + + serverSock = (ServerSocketChannel) bootstrap.bind(0).sync().channel(); + } + + public void shutdown() throws InterruptedException { + group.shutdownGracefully().await(); + serverSock.close(); + } + + public int port() { + return serverSock.localAddress().getPort(); + } + + @Override + protected void initChannel(Channel ch) { + channels.add(ch); + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast(sslCtx.newHandler(ch.alloc())); + pipeline.addLast(new HttpServerCodec()); + pipeline.addLast(new BehaviorTestChannelHandler()); + } + + private class BehaviorTestChannelHandler extends ChannelDuplexHandler { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof HttpRequest) { + HttpResponseStatus status; + if (ctx.channel().equals(channels.get(0)) && return500OnFirstRequest) { + status = INTERNAL_SERVER_ERROR; + } else { + status = OK; + } + + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, + Unpooled.wrappedBuffer(CONTENT)); + + response.headers() + .set(CONTENT_TYPE, TEXT_PLAIN) + .setInt(CONTENT_LENGTH, response.content().readableBytes()); + + if (closeConnection) { + response.headers().set(CONNECTION, CLOSE); + } + + ctx.writeAndFlush(response); + } + } + } + } +} diff --git a/test/sdk-benchmarks/pom.xml b/test/sdk-benchmarks/pom.xml index b90436ae1949..a03ecd55632c 100755 --- a/test/sdk-benchmarks/pom.xml +++ b/test/sdk-benchmarks/pom.xml @@ -200,6 +200,12 @@ org.eclipse.jetty.http2 http2-hpack + + software.amazon.awssdk + aws-crt-client + ${awsjavasdk.version}-PREVIEW + compile + diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apicall/httpclient/async/AwsCrtClientBenchmark.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apicall/httpclient/async/AwsCrtClientBenchmark.java new file mode 100644 index 000000000000..d3ee289e4292 --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apicall/httpclient/async/AwsCrtClientBenchmark.java @@ -0,0 +1,55 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.benchmark.apicall.httpclient.async; + +import java.net.URI; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.profile.StackProfiler; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; +import software.amazon.awssdk.benchmark.utils.MockServer; + +/** + * Using aws-crt-client to test against local mock https server. + */ +@State(Scope.Benchmark) +@Warmup(iterations = 3, time = 15, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 10, timeUnit = TimeUnit.SECONDS) +@Fork(2) // To reduce difference between each run +@BenchmarkMode(Mode.Throughput) +public class AwsCrtClientBenchmark extends BaseCrtBenchmark { + + @Override + protected URI getEndpointOverride(MockServer mock) { + return mock.getHttpsUri(); + } + + public static void main(String... args) throws Exception { + Options opt = new OptionsBuilder() + .include(AwsCrtClientBenchmark.class.getSimpleName()) + .addProfiler(StackProfiler.class) + .build(); + new Runner(opt).run(); + } +} diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apicall/httpclient/async/AwsCrtClientNonTlsBenchmark.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apicall/httpclient/async/AwsCrtClientNonTlsBenchmark.java new file mode 100644 index 000000000000..7fdd4af3f9e2 --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apicall/httpclient/async/AwsCrtClientNonTlsBenchmark.java @@ -0,0 +1,55 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.benchmark.apicall.httpclient.async; + +import java.net.URI; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.profile.StackProfiler; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; +import software.amazon.awssdk.benchmark.utils.MockServer; + +/** + * Using aws-crt-client to test against local mock https server. + */ +@State(Scope.Benchmark) +@Warmup(iterations = 3, time = 15, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 10, timeUnit = TimeUnit.SECONDS) +@Fork(2) // To reduce difference between each run +@BenchmarkMode(Mode.Throughput) +public class AwsCrtClientNonTlsBenchmark extends BaseCrtBenchmark { + + @Override + protected URI getEndpointOverride(MockServer mock) { + return mock.getHttpUri(); + } + + public static void main(String... args) throws Exception { + Options opt = new OptionsBuilder() + .include(AwsCrtClientNonTlsBenchmark.class.getSimpleName()) + .addProfiler(StackProfiler.class) + .build(); + new Runner(opt).run(); + } +} diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apicall/httpclient/async/BaseCrtBenchmark.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apicall/httpclient/async/BaseCrtBenchmark.java new file mode 100644 index 000000000000..45c90c6f183f --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apicall/httpclient/async/BaseCrtBenchmark.java @@ -0,0 +1,98 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.benchmark.apicall.httpclient.async; + +import static software.amazon.awssdk.benchmark.utils.BenchmarkConstant.CONCURRENT_CALLS; +import static software.amazon.awssdk.benchmark.utils.BenchmarkUtils.awaitCountdownLatchUninterruptibly; +import static software.amazon.awssdk.benchmark.utils.BenchmarkUtils.countDownUponCompletion; + +import java.net.URI; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.OperationsPerInvocation; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.infra.Blackhole; +import software.amazon.awssdk.benchmark.apicall.httpclient.SdkHttpClientBenchmark; +import software.amazon.awssdk.benchmark.utils.MockServer; +import software.amazon.awssdk.http.SdkHttpConfigurationOption; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.http.crt.AwsCrtAsyncHttpClient; +import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonAsyncClient; +import software.amazon.awssdk.utils.AttributeMap; + +/** + * Shared code between http and https benchmarks + */ +public abstract class BaseCrtBenchmark implements SdkHttpClientBenchmark { + + private MockServer mockServer; + private SdkAsyncHttpClient sdkHttpClient; + private ProtocolRestJsonAsyncClient client; + + @Setup(Level.Trial) + public void setup() throws Exception { + mockServer = new MockServer(); + mockServer.start(); + + AttributeMap trustAllCerts = AttributeMap.builder() + .put(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES, Boolean.TRUE) + .build(); + + sdkHttpClient = AwsCrtAsyncHttpClient.builder() + .buildWithDefaults(trustAllCerts); + + client = ProtocolRestJsonAsyncClient.builder() + .endpointOverride(getEndpointOverride(mockServer)) + .httpClient(sdkHttpClient) + .build(); + + // Making sure the request actually succeeds + client.allTypes().join(); + } + + @TearDown(Level.Trial) + public void tearDown() throws Exception { + mockServer.stop(); + client.close(); + sdkHttpClient.close(); + } + + @Override + @Benchmark + @OperationsPerInvocation(CONCURRENT_CALLS) + public void concurrentApiCall(Blackhole blackhole) { + CountDownLatch countDownLatch = new CountDownLatch(CONCURRENT_CALLS); + for (int i = 0; i < CONCURRENT_CALLS; i++) { + countDownUponCompletion(blackhole, client.allTypes(), countDownLatch); + } + + awaitCountdownLatchUninterruptibly(countDownLatch, 10, TimeUnit.SECONDS); + + } + + @Override + @Benchmark + public void sequentialApiCall(Blackhole blackhole) { + CountDownLatch countDownLatch = new CountDownLatch(1); + countDownUponCompletion(blackhole, client.allTypes(), countDownLatch); + awaitCountdownLatchUninterruptibly(countDownLatch, 1, TimeUnit.SECONDS); + } + + protected abstract URI getEndpointOverride(MockServer mock); +} diff --git a/test/stability-tests/pom.xml b/test/stability-tests/pom.xml index f83def9b751d..f94ed4dd17bc 100644 --- a/test/stability-tests/pom.xml +++ b/test/stability-tests/pom.xml @@ -65,6 +65,12 @@ ${awsjavasdk.version} test + + software.amazon.awssdk + aws-crt-client + ${awsjavasdk.version}-PREVIEW + test + software.amazon.awssdk aws-core diff --git a/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/cloudwatch/CloudWatchAsyncStabilityTest.java b/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/cloudwatch/CloudWatchAsyncStabilityTest.java deleted file mode 100644 index 826bef2e49fe..000000000000 --- a/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/cloudwatch/CloudWatchAsyncStabilityTest.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.stability.tests.cloudwatch; - - -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.function.IntFunction; -import org.apache.commons.lang3.RandomUtils; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import software.amazon.awssdk.services.cloudwatch.model.MetricDatum; -import software.amazon.awssdk.stability.tests.exceptions.StabilityTestsRetryableException; -import software.amazon.awssdk.stability.tests.utils.RetryableTest; -import software.amazon.awssdk.stability.tests.utils.StabilityTestRunner; - -public class CloudWatchAsyncStabilityTest extends CloudWatchBaseStabilityTest { - private static String namespace; - - @BeforeAll - public static void setup() { - namespace = "CloudWatchAsyncStabilityTest" + System.currentTimeMillis(); - } - - @AfterAll - public static void tearDown() { - cloudWatchAsyncClient.close(); - } - - @RetryableTest(maxRetries = 3, retryableException = StabilityTestsRetryableException.class) - public void putMetrics_lowTpsLongInterval() { - List metrics = new ArrayList<>(); - for (int i = 0; i < 20 ; i++) { - metrics.add(MetricDatum.builder() - .metricName("test") - .values(RandomUtils.nextDouble(1d, 1000d)) - .build()); - } - - IntFunction> futureIntFunction = i -> - cloudWatchAsyncClient.putMetricData(b -> b.namespace(namespace) - .metricData(metrics)); - - runCloudWatchTest("putMetrics_lowTpsLongInterval", futureIntFunction); - } - - - private void runCloudWatchTest(String testName, IntFunction> futureIntFunction) { - StabilityTestRunner.newRunner() - .testName("CloudWatchAsyncStabilityTest." + testName) - .futureFactory(futureIntFunction) - .totalRuns(TOTAL_RUNS) - .requestCountPerRun(CONCURRENCY) - .delaysBetweenEachRun(Duration.ofSeconds(6)) - .run(); - } -} diff --git a/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/cloudwatch/CloudWatchBaseStabilityTest.java b/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/cloudwatch/CloudWatchBaseStabilityTest.java index d2fbfe87d8eb..df9c2a088a75 100644 --- a/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/cloudwatch/CloudWatchBaseStabilityTest.java +++ b/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/cloudwatch/CloudWatchBaseStabilityTest.java @@ -17,24 +17,51 @@ import java.time.Duration; -import software.amazon.awssdk.core.retry.RetryPolicy; -import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.IntFunction; + +import org.apache.commons.lang3.RandomUtils; import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClient; +import software.amazon.awssdk.services.cloudwatch.model.MetricDatum; +import software.amazon.awssdk.stability.tests.utils.StabilityTestRunner; import software.amazon.awssdk.testutils.service.AwsTestBase; public abstract class CloudWatchBaseStabilityTest extends AwsTestBase { protected static final int CONCURRENCY = 50; protected static final int TOTAL_RUNS = 3; - protected static CloudWatchAsyncClient cloudWatchAsyncClient = - CloudWatchAsyncClient.builder() - .httpClientBuilder(NettyNioAsyncHttpClient.builder().maxConcurrency(CONCURRENCY)) - .credentialsProvider(CREDENTIALS_PROVIDER_CHAIN) - .overrideConfiguration(b -> b - // Retry at test level - .retryPolicy(RetryPolicy.none()) - .apiCallTimeout(Duration.ofMinutes(1))) - .build(); + protected abstract CloudWatchAsyncClient getTestClient(); + protected abstract String getNamespace(); + + protected void putMetrics() { + List metrics = new ArrayList<>(); + for (int i = 0; i < 20 ; i++) { + metrics.add(MetricDatum.builder() + .metricName("test") + .values(RandomUtils.nextDouble(1d, 1000d)) + .build()); + } + + IntFunction> futureIntFunction = i -> + getTestClient().putMetricData(b -> b.namespace(getNamespace()) + .metricData(metrics)); + + runCloudWatchTest("putMetrics_lowTpsLongInterval", futureIntFunction); + } + + + private void runCloudWatchTest(String testName, IntFunction> futureIntFunction) { + StabilityTestRunner.newRunner() + .testName("CloudWatchAsyncStabilityTest." + testName) + .futureFactory(futureIntFunction) + .totalRuns(TOTAL_RUNS) + .requestCountPerRun(CONCURRENCY) + .delaysBetweenEachRun(Duration.ofSeconds(6)) + .run(); + } + } diff --git a/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/cloudwatch/CloudWatchCrtAsyncStabilityTest.java b/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/cloudwatch/CloudWatchCrtAsyncStabilityTest.java new file mode 100644 index 000000000000..ee58ce44ee6b --- /dev/null +++ b/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/cloudwatch/CloudWatchCrtAsyncStabilityTest.java @@ -0,0 +1,65 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.stability.tests.cloudwatch; + + +import java.time.Duration; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import software.amazon.awssdk.core.retry.RetryPolicy; +import software.amazon.awssdk.crt.io.EventLoopGroup; +import software.amazon.awssdk.crt.io.HostResolver; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.http.crt.AwsCrtAsyncHttpClient; +import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClient; +import software.amazon.awssdk.stability.tests.exceptions.StabilityTestsRetryableException; +import software.amazon.awssdk.stability.tests.utils.RetryableTest; + +public class CloudWatchCrtAsyncStabilityTest extends CloudWatchBaseStabilityTest { + private static String namespace; + private static CloudWatchAsyncClient cloudWatchAsyncClient; + + @Override + protected CloudWatchAsyncClient getTestClient() { return cloudWatchAsyncClient; } + + @Override + protected String getNamespace() { return namespace; } + + @BeforeAll + public static void setup() { + namespace = "CloudWatchCrtAsyncStabilityTest" + System.currentTimeMillis(); + SdkAsyncHttpClient.Builder crtClientBuilder = AwsCrtAsyncHttpClient.builder() + .connectionMaxIdleTime(Duration.ofSeconds(5)); + + cloudWatchAsyncClient = CloudWatchAsyncClient.builder() + .httpClientBuilder(crtClientBuilder) + .credentialsProvider(CREDENTIALS_PROVIDER_CHAIN) + .overrideConfiguration(b -> b.apiCallTimeout(Duration.ofMinutes(10)) + // Retry at test level + .retryPolicy(RetryPolicy.none())) + .build(); + } + + @AfterAll + public static void tearDown() { + cloudWatchAsyncClient.close(); + } + + @RetryableTest(maxRetries = 3, retryableException = StabilityTestsRetryableException.class) + public void putMetrics_lowTpsLongInterval() { + putMetrics(); + } +} diff --git a/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/cloudwatch/CloudWatchNettyAsyncStabilityTest.java b/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/cloudwatch/CloudWatchNettyAsyncStabilityTest.java new file mode 100644 index 000000000000..204fc48c8dbf --- /dev/null +++ b/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/cloudwatch/CloudWatchNettyAsyncStabilityTest.java @@ -0,0 +1,61 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.stability.tests.cloudwatch; + + +import java.time.Duration; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import software.amazon.awssdk.core.retry.RetryPolicy; +import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; +import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClient; +import software.amazon.awssdk.stability.tests.exceptions.StabilityTestsRetryableException; +import software.amazon.awssdk.stability.tests.utils.RetryableTest; + +public class CloudWatchNettyAsyncStabilityTest extends CloudWatchBaseStabilityTest { + private static String namespace; + private static CloudWatchAsyncClient cloudWatchAsyncClient; + + @Override + protected CloudWatchAsyncClient getTestClient() { return cloudWatchAsyncClient; } + + @Override + protected String getNamespace() { return namespace; } + + @BeforeAll + public static void setup() { + namespace = "CloudWatchNettyAsyncStabilityTest" + System.currentTimeMillis(); + cloudWatchAsyncClient = + CloudWatchAsyncClient.builder() + .httpClientBuilder(NettyNioAsyncHttpClient.builder().maxConcurrency(CONCURRENCY)) + .credentialsProvider(CREDENTIALS_PROVIDER_CHAIN) + .overrideConfiguration(b -> b + // Retry at test level + .retryPolicy(RetryPolicy.none()) + .apiCallTimeout(Duration.ofMinutes(1))) + .build(); + } + + @AfterAll + public static void tearDown() { + cloudWatchAsyncClient.close(); + } + + @RetryableTest(maxRetries = 3, retryableException = StabilityTestsRetryableException.class) + public void putMetrics_lowTpsLongInterval() { + putMetrics(); + } +} diff --git a/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/s3/S3AsyncStabilityTest.java b/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/s3/S3AsyncStabilityTest.java deleted file mode 100644 index 9b5b94398f7b..000000000000 --- a/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/s3/S3AsyncStabilityTest.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.stability.tests.s3; - - -import java.io.File; -import java.io.IOException; -import java.nio.file.Path; -import java.time.Duration; -import java.util.concurrent.CompletableFuture; -import java.util.function.IntFunction; -import org.apache.commons.lang3.RandomStringUtils; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import software.amazon.awssdk.core.async.AsyncRequestBody; -import software.amazon.awssdk.core.async.AsyncResponseTransformer; -import software.amazon.awssdk.stability.tests.exceptions.StabilityTestsRetryableException; -import software.amazon.awssdk.stability.tests.utils.RetryableTest; -import software.amazon.awssdk.stability.tests.utils.StabilityTestRunner; -import software.amazon.awssdk.testutils.RandomTempFile; -import software.amazon.awssdk.utils.Logger; - -public class S3AsyncStabilityTest extends S3BaseStabilityTest { - private static final Logger LOGGER = Logger.loggerFor(S3AsyncStabilityTest.class); - private static String bucketName = "s3asyncstabilitytests" + System.currentTimeMillis(); - - @BeforeAll - public static void setup() { - s3NettyClient.createBucket(b -> b.bucket(bucketName)).join(); - } - - @AfterAll - public static void cleanup() { - deleteBucketAndAllContents(bucketName); - s3NettyClient.close(); - } - - @RetryableTest(maxRetries = 3, retryableException = StabilityTestsRetryableException.class) - @Override - public void putObject_getObject_highConcurrency() { - putObject(); - getObject(); - } - - @RetryableTest(maxRetries = 3, retryableException = StabilityTestsRetryableException.class) - public void largeObject_put_get_usingFile() { - uploadLargeObjectFromFile(); - downloadLargeObjectToFile(); - } - - @RetryableTest(maxRetries = 3, retryableException = StabilityTestsRetryableException.class) - public void getBucketAcl_lowTpsLongInterval() { - IntFunction> future = i -> s3NettyClient.getBucketAcl(b -> b.bucket(bucketName)); - StabilityTestRunner.newRunner() - .testName("S3AsyncStabilityTest.getBucketAcl_lowTpsLongInterval") - .futureFactory(future) - .requestCountPerRun(10) - .totalRuns(3) - .delaysBetweenEachRun(Duration.ofSeconds(6)) - .run(); - } - - private void downloadLargeObjectToFile() { - File randomTempFile = RandomTempFile.randomUncreatedFile(); - StabilityTestRunner.newRunner() - .testName("S3AsyncStabilityTest.downloadLargeObjectToFile") - .futures(s3NettyClient.getObject(b -> b.bucket(bucketName).key(LARGE_KEY_NAME), - AsyncResponseTransformer.toFile(randomTempFile))) - .run(); - randomTempFile.delete(); - } - - private void uploadLargeObjectFromFile() { - RandomTempFile file = null; - try { - file = new RandomTempFile((long) 2e+9); - StabilityTestRunner.newRunner() - .testName("S3AsyncStabilityTest.uploadLargeObjectFromFile") - .futures(s3NettyClient.putObject(b -> b.bucket(bucketName).key(LARGE_KEY_NAME), - AsyncRequestBody.fromFile(file))) - .run(); - } catch (IOException e) { - throw new RuntimeException("fail to create test file", e); - } finally { - if (file != null) { - file.delete(); - } - } - } - - private void putObject() { - LOGGER.info(() -> "Starting to test putObject"); - byte[] bytes = RandomStringUtils.randomAlphanumeric(10_000).getBytes(); - - IntFunction> future = i -> { - String keyName = computeKeyName(i); - return s3NettyClient.putObject(b -> b.bucket(bucketName).key(keyName), - AsyncRequestBody.fromBytes(bytes)); - }; - - StabilityTestRunner.newRunner() - .testName("S3AsyncStabilityTest.putObject") - .futureFactory(future) - .requestCountPerRun(CONCURRENCY) - .totalRuns(TOTAL_RUNS) - .delaysBetweenEachRun(Duration.ofMillis(100)) - .run(); - } - - private void getObject() { - LOGGER.info(() -> "Starting to test getObject"); - IntFunction> future = i -> { - String keyName = computeKeyName(i); - Path path = RandomTempFile.randomUncreatedFile().toPath(); - return s3NettyClient.getObject(b -> b.bucket(bucketName).key(keyName), AsyncResponseTransformer.toFile(path)); - }; - - StabilityTestRunner.newRunner() - .testName("S3AsyncStabilityTest.getObject") - .futureFactory(future) - .requestCountPerRun(CONCURRENCY) - .totalRuns(TOTAL_RUNS) - .delaysBetweenEachRun(Duration.ofMillis(100)) - .run(); - } -} diff --git a/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/s3/S3BaseStabilityTest.java b/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/s3/S3BaseStabilityTest.java index 178f8394c229..2bdcd1f13c6d 100644 --- a/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/s3/S3BaseStabilityTest.java +++ b/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/s3/S3BaseStabilityTest.java @@ -15,20 +15,26 @@ package software.amazon.awssdk.stability.tests.s3; +import java.io.File; import java.io.IOException; +import java.nio.file.Path; import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; -import software.amazon.awssdk.core.retry.RetryPolicy; +import java.util.function.IntFunction; + +import org.apache.commons.lang3.RandomStringUtils; +import software.amazon.awssdk.core.async.AsyncRequestBody; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.http.apache.ApacheHttpClient; -import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; import software.amazon.awssdk.services.s3.S3AsyncClient; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.DeleteBucketRequest; import software.amazon.awssdk.services.s3.model.NoSuchBucketException; import software.amazon.awssdk.services.s3.model.NoSuchKeyException; +import software.amazon.awssdk.stability.tests.utils.StabilityTestRunner; import software.amazon.awssdk.testutils.RandomTempFile; import software.amazon.awssdk.testutils.service.AwsTestBase; import software.amazon.awssdk.utils.Logger; @@ -39,20 +45,9 @@ public abstract class S3BaseStabilityTest extends AwsTestBase { protected static final int TOTAL_RUNS = 50; protected static final String LARGE_KEY_NAME = "2GB"; - protected static S3AsyncClient s3NettyClient; protected static S3Client s3ApacheClient; static { - s3NettyClient = S3AsyncClient.builder() - .httpClientBuilder(NettyNioAsyncHttpClient.builder() - .maxConcurrency(CONCURRENCY)) - .credentialsProvider(CREDENTIALS_PROVIDER_CHAIN) - .overrideConfiguration(b -> b.apiCallTimeout(Duration.ofMinutes(10)) - // Retry at test level - .retryPolicy(RetryPolicy.none())) - .build(); - - s3ApacheClient = S3Client.builder() .httpClientBuilder(ApacheHttpClient.builder() .maxConnections(CONCURRENCY)) @@ -65,19 +60,98 @@ protected String computeKeyName(int i) { return "key_" + i; } - protected static void deleteBucketAndAllContents(String bucketName) { + protected abstract S3AsyncClient getTestClient(); + + protected abstract String getTestBucketName(); + + protected void doGetBucketAcl_lowTpsLongInterval() { + IntFunction> future = i -> getTestClient().getBucketAcl(b -> b.bucket(getTestBucketName())); + String className = this.getClass().getSimpleName(); + StabilityTestRunner.newRunner() + .testName(className + ".getBucketAcl_lowTpsLongInterval") + .futureFactory(future) + .requestCountPerRun(10) + .totalRuns(3) + .delaysBetweenEachRun(Duration.ofSeconds(6)) + .run(); + } + + + protected void downloadLargeObjectToFile() { + File randomTempFile = RandomTempFile.randomUncreatedFile(); + StabilityTestRunner.newRunner() + .testName("S3AsyncStabilityTest.downloadLargeObjectToFile") + .futures(getTestClient().getObject(b -> b.bucket(getTestBucketName()).key(LARGE_KEY_NAME), + AsyncResponseTransformer.toFile(randomTempFile))) + .run(); + randomTempFile.delete(); + } + + protected void uploadLargeObjectFromFile() { + RandomTempFile file = null; + try { + file = new RandomTempFile((long) 2e+9); + StabilityTestRunner.newRunner() + .testName("S3AsyncStabilityTest.uploadLargeObjectFromFile") + .futures(getTestClient().putObject(b -> b.bucket(getTestBucketName()).key(LARGE_KEY_NAME), + AsyncRequestBody.fromFile(file))) + .run(); + } catch (IOException e) { + throw new RuntimeException("fail to create test file", e); + } finally { + if (file != null) { + file.delete(); + } + } + } + + protected void putObject() { + byte[] bytes = RandomStringUtils.randomAlphanumeric(10_000).getBytes(); + + IntFunction> future = i -> { + String keyName = computeKeyName(i); + return getTestClient().putObject(b -> b.bucket(getTestBucketName()).key(keyName), + AsyncRequestBody.fromBytes(bytes)); + }; + + StabilityTestRunner.newRunner() + .testName("S3AsyncStabilityTest.putObject") + .futureFactory(future) + .requestCountPerRun(CONCURRENCY) + .totalRuns(TOTAL_RUNS) + .delaysBetweenEachRun(Duration.ofMillis(100)) + .run(); + } + + protected void getObject() { + IntFunction> future = i -> { + String keyName = computeKeyName(i); + Path path = RandomTempFile.randomUncreatedFile().toPath(); + return getTestClient().getObject(b -> b.bucket(getTestBucketName()).key(keyName), AsyncResponseTransformer.toFile(path)); + }; + + StabilityTestRunner.newRunner() + .testName("S3AsyncStabilityTest.getObject") + .futureFactory(future) + .requestCountPerRun(CONCURRENCY) + .totalRuns(TOTAL_RUNS) + .delaysBetweenEachRun(Duration.ofMillis(100)) + .run(); + } + + protected static void deleteBucketAndAllContents(S3AsyncClient client, String bucketName) { try { List> futures = new ArrayList<>(); - s3NettyClient.listObjectsV2Paginator(b -> b.bucket(bucketName)) - .subscribe(r -> r.contents().forEach(s -> futures.add(s3NettyClient.deleteObject(o -> o.bucket(bucketName).key(s.key()))))) + client.listObjectsV2Paginator(b -> b.bucket(bucketName)) + .subscribe(r -> r.contents().forEach(s -> futures.add(client.deleteObject(o -> o.bucket(bucketName).key(s.key()))))) .join(); CompletableFuture[] futureArray = futures.toArray(new CompletableFuture[0]); CompletableFuture.allOf(futureArray).join(); - s3NettyClient.deleteBucket(DeleteBucketRequest.builder().bucket(bucketName).build()).join(); + client.deleteBucket(DeleteBucketRequest.builder().bucket(bucketName).build()).join(); } catch (Exception e) { log.error(() -> "Failed to delete bucket: " +bucketName); } @@ -101,7 +175,4 @@ protected void verifyObjectExist(String bucketName, String keyName, long size) t } } - public abstract void putObject_getObject_highConcurrency(); - - public abstract void largeObject_put_get_usingFile(); } diff --git a/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/s3/S3CrtAsyncStabilityTest.java b/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/s3/S3CrtAsyncStabilityTest.java new file mode 100644 index 000000000000..e8d9dd7dbb10 --- /dev/null +++ b/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/s3/S3CrtAsyncStabilityTest.java @@ -0,0 +1,69 @@ +package software.amazon.awssdk.stability.tests.s3; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import software.amazon.awssdk.core.retry.RetryPolicy; +import software.amazon.awssdk.crt.io.EventLoopGroup; +import software.amazon.awssdk.crt.io.HostResolver; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.http.crt.AwsCrtAsyncHttpClient; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.stability.tests.exceptions.StabilityTestsRetryableException; +import software.amazon.awssdk.stability.tests.utils.RetryableTest; + +import java.time.Duration; + +public class S3CrtAsyncStabilityTest extends S3BaseStabilityTest { + + private static String bucketName = "s3crtasyncstabilitytests" + System.currentTimeMillis(); + + private static S3AsyncClient s3CrtClient; + + static { + int numThreads = Runtime.getRuntime().availableProcessors(); + SdkAsyncHttpClient.Builder httpClientBuilder = AwsCrtAsyncHttpClient.builder() + .connectionMaxIdleTime(Duration.ofSeconds(5)); + + s3CrtClient = S3AsyncClient.builder() + .httpClientBuilder(httpClientBuilder) + .credentialsProvider(CREDENTIALS_PROVIDER_CHAIN) + .overrideConfiguration(b -> b.apiCallTimeout(Duration.ofMinutes(10)) + // Retry at test level + .retryPolicy(RetryPolicy.none())) + .build(); + } + + @BeforeAll + public static void setup() { + s3CrtClient.createBucket(b -> b.bucket(bucketName)).join(); + } + + @AfterAll + public static void cleanup() { + deleteBucketAndAllContents(s3CrtClient, bucketName); + s3CrtClient.close(); + } + + @Override + protected S3AsyncClient getTestClient() { return s3CrtClient; } + + @Override + protected String getTestBucketName() { return bucketName; } + + @RetryableTest(maxRetries = 3, retryableException = StabilityTestsRetryableException.class) + public void putObject_getObject_highConcurrency() { + putObject(); + getObject(); + } + + @RetryableTest(maxRetries = 3, retryableException = StabilityTestsRetryableException.class) + public void largeObject_put_get_usingFile() { + uploadLargeObjectFromFile(); + downloadLargeObjectToFile(); + } + + @RetryableTest(maxRetries = 3, retryableException = StabilityTestsRetryableException.class) + public void getBucketAcl_lowTpsLongInterval_Crt() { + doGetBucketAcl_lowTpsLongInterval(); + } +} diff --git a/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/s3/S3NettyAsyncStabilityTest.java b/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/s3/S3NettyAsyncStabilityTest.java new file mode 100644 index 000000000000..54fd65ddfa39 --- /dev/null +++ b/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/s3/S3NettyAsyncStabilityTest.java @@ -0,0 +1,63 @@ +package software.amazon.awssdk.stability.tests.s3; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import software.amazon.awssdk.core.retry.RetryPolicy; +import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.stability.tests.exceptions.StabilityTestsRetryableException; +import software.amazon.awssdk.stability.tests.utils.RetryableTest; + +import java.time.Duration; + +public class S3NettyAsyncStabilityTest extends S3BaseStabilityTest { + + private static String bucketName = "s3nettyasyncstabilitytests" + System.currentTimeMillis(); + + private static S3AsyncClient s3NettyClient; + + static { + s3NettyClient = S3AsyncClient.builder() + .httpClientBuilder(NettyNioAsyncHttpClient.builder() + .maxConcurrency(CONCURRENCY)) + .credentialsProvider(CREDENTIALS_PROVIDER_CHAIN) + .overrideConfiguration(b -> b.apiCallTimeout(Duration.ofMinutes(10)) + // Retry at test level + .retryPolicy(RetryPolicy.none())) + .build(); + } + + @BeforeAll + public static void setup() { + s3NettyClient.createBucket(b -> b.bucket(bucketName)).join(); + } + + @AfterAll + public static void cleanup() { + deleteBucketAndAllContents(s3NettyClient, bucketName); + s3NettyClient.close(); + } + + @Override + protected S3AsyncClient getTestClient() { return s3NettyClient; } + + @Override + protected String getTestBucketName() { return bucketName; } + + @RetryableTest(maxRetries = 3, retryableException = StabilityTestsRetryableException.class) + public void putObject_getObject_highConcurrency() { + putObject(); + getObject(); + } + + @RetryableTest(maxRetries = 3, retryableException = StabilityTestsRetryableException.class) + public void largeObject_put_get_usingFile() { + uploadLargeObjectFromFile(); + downloadLargeObjectToFile(); + } + + @RetryableTest(maxRetries = 3, retryableException = StabilityTestsRetryableException.class) + public void getBucketAcl_lowTpsLongInterval_Netty() { + doGetBucketAcl_lowTpsLongInterval(); + } +} diff --git a/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/sqs/SqsAsyncStabilityTest.java b/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/sqs/SqsAsyncStabilityTest.java deleted file mode 100644 index a2c881e86814..000000000000 --- a/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/sqs/SqsAsyncStabilityTest.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.stability.tests.sqs; - -import java.time.Duration; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.function.IntFunction; -import java.util.stream.Collectors; -import org.apache.commons.lang3.RandomStringUtils; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import software.amazon.awssdk.services.sqs.model.CreateQueueResponse; -import software.amazon.awssdk.services.sqs.model.DeleteMessageBatchRequestEntry; -import software.amazon.awssdk.stability.tests.exceptions.StabilityTestsRetryableException; -import software.amazon.awssdk.stability.tests.utils.RetryableTest; -import software.amazon.awssdk.stability.tests.utils.StabilityTestRunner; -import software.amazon.awssdk.utils.Logger; - -public class SqsAsyncStabilityTest extends SqsBaseStabilityTest { - private static final Logger log = Logger.loggerFor(SqsAsyncStabilityTest.class); - private static String queueName; - private static String queueUrl; - - @BeforeAll - public static void setup() { - queueName = "sqsasyncstabilitytests" + System.currentTimeMillis(); - CreateQueueResponse createQueueResponse = sqsAsyncClient.createQueue(b -> b.queueName(queueName)).join(); - queueUrl = createQueueResponse.queueUrl(); - } - - @AfterAll - public static void tearDown() { - if (queueUrl != null) { - sqsAsyncClient.deleteQueue(b -> b.queueUrl(queueUrl)); - } - sqsAsyncClient.close(); - } - - @RetryableTest(maxRetries = 3, retryableException = StabilityTestsRetryableException.class) - public void sendMessage_receiveMessage() { - sendMessage(); - receiveMessage(); - } - - private void sendMessage() { - log.info(() -> String.format("Starting testing sending messages to queue %s with queueUrl %s", queueName, queueUrl)); - String messageBody = RandomStringUtils.randomAscii(1000); - IntFunction> futureIntFunction = - i -> sqsAsyncClient.sendMessage(b -> b.queueUrl(queueUrl).messageBody(messageBody)); - - runSqsTests("sendMessage", futureIntFunction); - } - - private void receiveMessage() { - log.info(() -> String.format("Starting testing receiving messages from queue %s with queueUrl %s", queueName, queueUrl)); - IntFunction> futureIntFunction = - i -> sqsAsyncClient.receiveMessage(b -> b.queueUrl(queueUrl)) - .thenApply( - r -> { - List batchRequestEntries = - r.messages().stream().map(m -> DeleteMessageBatchRequestEntry.builder().id(m.messageId()).receiptHandle(m.receiptHandle()).build()) - .collect(Collectors.toList()); - return sqsAsyncClient.deleteMessageBatch(b -> b.queueUrl(queueUrl).entries(batchRequestEntries)); - }); - runSqsTests("receiveMessage", futureIntFunction); - } - - private void runSqsTests(String testName, IntFunction> futureIntFunction) { - StabilityTestRunner.newRunner() - .testName("SqsAsyncStabilityTest." + testName) - .futureFactory(futureIntFunction) - .totalRuns(TOTAL_RUNS) - .requestCountPerRun(CONCURRENCY) - .delaysBetweenEachRun(Duration.ofMillis(100)) - .run(); - } -} diff --git a/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/sqs/SqsBaseStabilityTest.java b/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/sqs/SqsBaseStabilityTest.java index 18a52afd56e1..04bcc6180e42 100644 --- a/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/sqs/SqsBaseStabilityTest.java +++ b/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/sqs/SqsBaseStabilityTest.java @@ -16,29 +16,71 @@ package software.amazon.awssdk.stability.tests.sqs; import java.time.Duration; -import software.amazon.awssdk.http.apache.ApacheHttpClient; -import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.IntFunction; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.RandomStringUtils; import software.amazon.awssdk.services.sqs.SqsAsyncClient; -import software.amazon.awssdk.services.sqs.SqsClient; +import software.amazon.awssdk.services.sqs.model.CreateQueueResponse; +import software.amazon.awssdk.services.sqs.model.DeleteMessageBatchRequestEntry; +import software.amazon.awssdk.stability.tests.utils.StabilityTestRunner; import software.amazon.awssdk.testutils.service.AwsTestBase; +import software.amazon.awssdk.utils.Logger; public abstract class SqsBaseStabilityTest extends AwsTestBase { + private static final Logger log = Logger.loggerFor(SqsNettyAsyncStabilityTest.class); protected static final int CONCURRENCY = 100; - protected static final int TOTAL_REQUEST_NUMBER = 5000; protected static final int TOTAL_RUNS = 50; - protected static SqsAsyncClient sqsAsyncClient = SqsAsyncClient.builder() - .httpClientBuilder(NettyNioAsyncHttpClient.builder().maxConcurrency(CONCURRENCY)) - .credentialsProvider(CREDENTIALS_PROVIDER_CHAIN) - .overrideConfiguration(b -> b.apiCallTimeout(Duration.ofMinutes(1))) - .build(); - protected static SqsClient sqsClient = SqsClient.builder() - .httpClientBuilder(ApacheHttpClient.builder().maxConnections(CONCURRENCY)) - .overrideConfiguration(b -> b.apiCallTimeout(Duration.ofMinutes(1))) - .build(); + protected abstract SqsAsyncClient getTestClient(); + protected abstract String getQueueUrl(); + protected abstract String getQueueName(); + + protected static String setup(SqsAsyncClient client, String queueName) { + CreateQueueResponse createQueueResponse = client.createQueue(b -> b.queueName(queueName)).join(); + return createQueueResponse.queueUrl(); + } + + protected static void tearDown(SqsAsyncClient client, String queueUrl) { + if (queueUrl != null) { + client.deleteQueue(b -> b.queueUrl(queueUrl)); + } + } + + protected void sendMessage() { + log.info(() -> String.format("Starting testing sending messages to queue %s with queueUrl %s", getQueueName(), getQueueUrl())); + String messageBody = RandomStringUtils.randomAscii(1000); + IntFunction> futureIntFunction = + i -> getTestClient().sendMessage(b -> b.queueUrl(getQueueUrl()).messageBody(messageBody)); + + runSqsTests("sendMessage", futureIntFunction); + } + protected void receiveMessage() { + log.info(() -> String.format("Starting testing receiving messages from queue %s with queueUrl %s", getQueueName(), getQueueUrl())); + IntFunction> futureIntFunction = + i -> getTestClient().receiveMessage(b -> b.queueUrl(getQueueUrl())) + .thenApply( + r -> { + List batchRequestEntries = + r.messages().stream().map(m -> DeleteMessageBatchRequestEntry.builder().id(m.messageId()).receiptHandle(m.receiptHandle()).build()) + .collect(Collectors.toList()); + return getTestClient().deleteMessageBatch(b -> b.queueUrl(getQueueUrl()).entries(batchRequestEntries)); + }); + runSqsTests("receiveMessage", futureIntFunction); + } - public abstract void sendMessage_receiveMessage(); + private void runSqsTests(String testName, IntFunction> futureIntFunction) { + StabilityTestRunner.newRunner() + .testName("SqsAsyncStabilityTest." + testName) + .futureFactory(futureIntFunction) + .totalRuns(TOTAL_RUNS) + .requestCountPerRun(CONCURRENCY) + .delaysBetweenEachRun(Duration.ofMillis(100)) + .run(); + } } diff --git a/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/sqs/SqsCrtAsyncStabilityTest.java b/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/sqs/SqsCrtAsyncStabilityTest.java new file mode 100644 index 000000000000..20bca1557984 --- /dev/null +++ b/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/sqs/SqsCrtAsyncStabilityTest.java @@ -0,0 +1,73 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.stability.tests.sqs; + +import java.time.Duration; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import software.amazon.awssdk.core.retry.RetryPolicy; +import software.amazon.awssdk.crt.io.EventLoopGroup; +import software.amazon.awssdk.crt.io.HostResolver; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.http.crt.AwsCrtAsyncHttpClient; +import software.amazon.awssdk.services.sqs.SqsAsyncClient; +import software.amazon.awssdk.stability.tests.exceptions.StabilityTestsRetryableException; +import software.amazon.awssdk.stability.tests.utils.RetryableTest; + +public class SqsCrtAsyncStabilityTest extends SqsBaseStabilityTest { + private static String queueName; + private static String queueUrl; + + private static SqsAsyncClient sqsAsyncClient; + + @Override + protected SqsAsyncClient getTestClient() { return sqsAsyncClient; } + + @Override + protected String getQueueUrl() { return queueUrl; } + + @Override + protected String getQueueName() { return queueName; } + + @BeforeAll + public static void setup() { + SdkAsyncHttpClient.Builder crtClientBuilder = AwsCrtAsyncHttpClient.builder() + .connectionMaxIdleTime(Duration.ofSeconds(5)); + + sqsAsyncClient = SqsAsyncClient.builder() + .httpClientBuilder(crtClientBuilder) + .credentialsProvider(CREDENTIALS_PROVIDER_CHAIN) + .overrideConfiguration(b -> b.apiCallTimeout(Duration.ofMinutes(10)) + // Retry at test level + .retryPolicy(RetryPolicy.none())) + .build(); + + queueName = "sqscrtasyncstabilitytests" + System.currentTimeMillis(); + queueUrl = setup(sqsAsyncClient, queueName); + } + + @AfterAll + public static void tearDown() { + tearDown(sqsAsyncClient, queueUrl); + sqsAsyncClient.close(); + } + + @RetryableTest(maxRetries = 3, retryableException = StabilityTestsRetryableException.class) + public void sendMessage_receiveMessage() { + sendMessage(); + receiveMessage(); + } +} diff --git a/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/sqs/SqsNettyAsyncStabilityTest.java b/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/sqs/SqsNettyAsyncStabilityTest.java new file mode 100644 index 000000000000..7cdc3dee773a --- /dev/null +++ b/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/sqs/SqsNettyAsyncStabilityTest.java @@ -0,0 +1,63 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.stability.tests.sqs; + +import java.time.Duration; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; +import software.amazon.awssdk.services.sqs.SqsAsyncClient; +import software.amazon.awssdk.stability.tests.exceptions.StabilityTestsRetryableException; +import software.amazon.awssdk.stability.tests.utils.RetryableTest; + +public class SqsNettyAsyncStabilityTest extends SqsBaseStabilityTest { + private static String queueName; + private static String queueUrl; + + private static SqsAsyncClient sqsAsyncClient; + + @Override + protected SqsAsyncClient getTestClient() { return sqsAsyncClient; } + + @Override + protected String getQueueUrl() { return queueUrl; } + + @Override + protected String getQueueName() { return queueName; } + + @BeforeAll + public static void setup() { + sqsAsyncClient = SqsAsyncClient.builder() + .httpClientBuilder(NettyNioAsyncHttpClient.builder().maxConcurrency(CONCURRENCY)) + .credentialsProvider(CREDENTIALS_PROVIDER_CHAIN) + .overrideConfiguration(b -> b.apiCallTimeout(Duration.ofMinutes(1))) + .build(); + queueName = "sqsnettyasyncstabilitytests" + System.currentTimeMillis(); + queueUrl = setup(sqsAsyncClient, queueName); + } + + @AfterAll + public static void tearDown() { + tearDown(sqsAsyncClient, queueUrl); + sqsAsyncClient.close(); + } + + @RetryableTest(maxRetries = 3, retryableException = StabilityTestsRetryableException.class) + public void sendMessage_receiveMessage() { + sendMessage(); + receiveMessage(); + } +} diff --git a/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/utils/StabilityTestRunner.java b/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/utils/StabilityTestRunner.java index c657e069bd28..156fd3f90e38 100644 --- a/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/utils/StabilityTestRunner.java +++ b/test/stability-tests/src/it/java/software/amazon/awssdk/stability/tests/utils/StabilityTestRunner.java @@ -249,10 +249,10 @@ private static CompletableFuture handleException(CompletableFuture future, log.error(() -> "An exception was thrown ", t); if (cause instanceof SdkServiceException) { exceptionCounter.addServiceException(); - } else if (isIOException(cause)) { + } else if (isIOExceptionOrHasIOCause(cause)) { exceptionCounter.addIoException(); } else if (cause instanceof SdkClientException) { - if (isIOException(cause.getCause())) { + if (isIOExceptionOrHasIOCause(cause.getCause())) { exceptionCounter.addIoException(); } else { exceptionCounter.addClientException(); @@ -264,8 +264,8 @@ private static CompletableFuture handleException(CompletableFuture future, }); } - private static boolean isIOException(Throwable throwable) { - return throwable.getClass().isAssignableFrom(IOException.class); + private static boolean isIOExceptionOrHasIOCause(Throwable throwable) { + return throwable instanceof IOException || throwable.getCause() instanceof IOException; } private TestResult generateTestResult(int totalRequestNumber, String testName, ExceptionCounter exceptionCounter, diff --git a/test/tests-coverage-reporting/pom.xml b/test/tests-coverage-reporting/pom.xml index fb987f5a9e6a..bc766e12e258 100644 --- a/test/tests-coverage-reporting/pom.xml +++ b/test/tests-coverage-reporting/pom.xml @@ -102,6 +102,11 @@ software.amazon.awssdk ${awsjavasdk.version} + + aws-crt-client + software.amazon.awssdk + ${awsjavasdk.version}-PREVIEW + url-connection-client software.amazon.awssdk