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 super ByteBuffer> 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 super ByteBuffer> 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 super ByteBuffer> 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 super ByteBuffer> 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 super ByteBuffer> 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