From f684e40d682c4dc696291e8241377abde55ccafd Mon Sep 17 00:00:00 2001 From: Dongie Agnir Date: Tue, 17 Dec 2019 11:40:56 -0800 Subject: [PATCH] Make the initial window size for H2 configurable --- .../feature-NettyNIOHTTPClient-8901f81.json | 5 + .../http/nio/netty/Http2Configuration.java | 148 +++++++++ .../nio/netty/NettyNioAsyncHttpClient.java | 72 ++++- .../internal/AwaitCloseChannelPoolMap.java | 17 +- .../internal/ChannelPipelineInitializer.java | 5 +- .../nio/netty/Http2ConfigurationTest.java | 91 ++++++ .../netty/internal/http2/WindowSizeTest.java | 286 ++++++++++++++++++ .../amazon/awssdk/utils/Validate.java | 30 ++ .../amazon/awssdk/utils/ValidateTest.java | 54 ++++ 9 files changed, 704 insertions(+), 4 deletions(-) create mode 100644 .changes/next-release/feature-NettyNIOHTTPClient-8901f81.json create mode 100644 http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/Http2Configuration.java create mode 100644 http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/Http2ConfigurationTest.java create mode 100644 http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/http2/WindowSizeTest.java diff --git a/.changes/next-release/feature-NettyNIOHTTPClient-8901f81.json b/.changes/next-release/feature-NettyNIOHTTPClient-8901f81.json new file mode 100644 index 000000000000..73b3894218bc --- /dev/null +++ b/.changes/next-release/feature-NettyNIOHTTPClient-8901f81.json @@ -0,0 +1,5 @@ +{ + "type": "feature", + "category": "Netty NIO HTTP Client", + "description": "`SETTINGS_INITIAL_WINDOW_SIZE` is now configurable on HTTP/2 connections opened by the Netty client using `Http2Configuration#initialWindowSize(Integer)` along with `NettyNioAsyncHttpClient.Builder#http2Configuration(Http2Configuration)`. See https://tools.ietf.org/html/rfc7540#section-6.5.2 for more information." +} diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/Http2Configuration.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/Http2Configuration.java new file mode 100644 index 000000000000..ce35ba514e79 --- /dev/null +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/Http2Configuration.java @@ -0,0 +1,148 @@ +/* + * Copyright 2010-2019 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 software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.utils.Validate; +import software.amazon.awssdk.utils.builder.CopyableBuilder; +import software.amazon.awssdk.utils.builder.ToCopyableBuilder; + +/** + * Configuration specific to HTTP/2 connections. + */ +@SdkPublicApi +public final class Http2Configuration implements ToCopyableBuilder { + private final Long maxStreams; + private final Integer initialWindowSize; + + private Http2Configuration(DefaultBuilder builder) { + this.maxStreams = builder.maxStreams; + this.initialWindowSize = builder.initialWindowSize; + } + + /** + * @return The maximum number of streams to be created per HTTP/2 connection. + */ + public Long maxStreams() { + return maxStreams; + } + + /** + * @return The initial window size for an HTTP/2 stream. + */ + public Integer initialWindowSize() { + return initialWindowSize; + } + + @Override + public Builder toBuilder() { + return new DefaultBuilder(this); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + Http2Configuration that = (Http2Configuration) o; + + if (maxStreams != null ? !maxStreams.equals(that.maxStreams) : that.maxStreams != null) { + return false; + } + + return initialWindowSize != null ? initialWindowSize.equals(that.initialWindowSize) : that.initialWindowSize == null; + + } + + @Override + public int hashCode() { + int result = maxStreams != null ? maxStreams.hashCode() : 0; + result = 31 * result + (initialWindowSize != null ? initialWindowSize.hashCode() : 0); + return result; + } + + public static Builder builder() { + return new DefaultBuilder(); + } + + public interface Builder extends CopyableBuilder { + + /** + * Sets the max number of concurrent streams per connection. + * + *

Note that this cannot exceed the value of the MAX_CONCURRENT_STREAMS setting returned by the service. If it + * does the service setting is used instead.

+ * + * @param maxStreams Max concurrent HTTP/2 streams per connection. + * @return This builder for method chaining. + */ + Builder maxStreams(Long maxStreams); + + /** + * Sets initial window size of a stream. This setting is only respected when the HTTP/2 protocol is used. + * + * See https://tools.ietf.org/html/rfc7540#section-6.5.2 + * for more information about this parameter. + * + * @param initialWindowSize The initial window size of a stream. + * @return This builder for method chaining. + */ + Builder initialWindowSize(Integer initialWindowSize); + } + + private static final class DefaultBuilder implements Builder { + private Long maxStreams; + private Integer initialWindowSize; + + private DefaultBuilder() { + } + + private DefaultBuilder(Http2Configuration http2Configuration) { + this.maxStreams = http2Configuration.maxStreams; + this.initialWindowSize = http2Configuration.initialWindowSize; + } + + @Override + public Builder maxStreams(Long maxStreams) { + this.maxStreams = Validate.isPositiveOrNull(maxStreams, "maxStreams"); + return this; + } + + public void setMaxStreams(Long maxStreams) { + maxStreams(maxStreams); + } + + @Override + public Builder initialWindowSize(Integer initialWindowSize) { + this.initialWindowSize = Validate.isPositiveOrNull(initialWindowSize, "initialWindowSize"); + return this; + } + + public void setInitialWindowSize(Integer initialWindowSize) { + initialWindowSize(initialWindowSize); + } + + @Override + public Http2Configuration build() { + return new Http2Configuration(this); + } + } +} diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClient.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClient.java index 93581ce2021e..3563cb82b328 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClient.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClient.java @@ -42,6 +42,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import software.amazon.awssdk.annotations.SdkPublicApi; @@ -77,6 +78,7 @@ public final class NettyNioAsyncHttpClient implements SdkAsyncHttpClient { private static final Logger log = LoggerFactory.getLogger(NettyNioAsyncHttpClient.class); private static final long MAX_STREAMS_ALLOWED = 4294967295L; // unsigned 32-bit, 2^32 -1 + private static final int DEFAULT_INITIAL_WINDOW_SIZE = 1_048_576; // 1MiB // Override connection idle timeout for Netty http client to reduce the frequency of "server failed to complete the // response error". see https://github.com/aws/aws-sdk-java-v2/issues/1122 @@ -91,13 +93,19 @@ public final class NettyNioAsyncHttpClient implements SdkAsyncHttpClient { private NettyNioAsyncHttpClient(DefaultBuilder builder, AttributeMap serviceDefaultsMap) { this.configuration = new NettyConfiguration(serviceDefaultsMap); Protocol protocol = serviceDefaultsMap.get(SdkHttpConfigurationOption.PROTOCOL); - long maxStreams = builder.maxHttp2Streams == null ? MAX_STREAMS_ALLOWED : builder.maxHttp2Streams; this.sdkEventLoopGroup = eventLoopGroup(builder); + + Http2Configuration http2Configuration = builder.http2Configuration; + + long maxStreams = resolveMaxHttp2Streams(builder.maxHttp2Streams, http2Configuration); + int initialWindowSize = resolveInitialWindowSize(http2Configuration); + this.pools = AwaitCloseChannelPoolMap.builder() .sdkChannelOptions(builder.sdkChannelOptions) .configuration(configuration) .protocol(protocol) .maxStreams(maxStreams) + .initialWindowSize(initialWindowSize) .sdkEventLoopGroup(sdkEventLoopGroup) .sslProvider(resolveSslProvider(builder)) .proxyConfiguration(builder.proxyConfiguration) @@ -149,6 +157,25 @@ private SslProvider resolveSslProvider(DefaultBuilder builder) { return SslContext.defaultClientProvider(); } + private long resolveMaxHttp2Streams(Integer topLevelValue, Http2Configuration http2Configuration) { + if (topLevelValue != null) { + return topLevelValue; + } + + if (http2Configuration == null || http2Configuration.maxStreams() == null) { + return MAX_STREAMS_ALLOWED; + } + + return Math.min(http2Configuration.maxStreams(), MAX_STREAMS_ALLOWED); + } + + private int resolveInitialWindowSize(Http2Configuration http2Configuration) { + if (http2Configuration == null || http2Configuration.initialWindowSize() == null) { + return DEFAULT_INITIAL_WINDOW_SIZE; + } + return http2Configuration.initialWindowSize(); + } + private SdkEventLoopGroup nonManagedEventLoopGroup(SdkEventLoopGroup eventLoopGroup) { return SdkEventLoopGroup.create(new NonManagedEventLoopGroup(eventLoopGroup.eventLoopGroup()), eventLoopGroup.channelFactory()); @@ -343,6 +370,9 @@ public interface Builder extends SdkAsyncHttpClient.Builder + * Note:If {@link #maxHttp2Streams(Integer)} and {@link Http2Configuration#maxStreams()} are both set, + * the value set using {@link #maxHttp2Streams(Integer)} takes precedence. + * + * @param http2Configuration The HTTP/2 configuration object. + * @return the builder for method chaining. + */ + Builder http2Configuration(Http2Configuration http2Configuration); + + /** + * Set the HTTP/2 specific configuration for this client. + *

+ * Note:If {@link #maxHttp2Streams(Integer)} and {@link Http2Configuration#maxStreams()} are both set, + * the value set using {@link #maxHttp2Streams(Integer)} takes precedence. + * + * @param http2ConfigurationBuilderConsumer The consumer of the HTTP/2 configuration builder object. + * @return the builder for method chaining. + */ + Builder http2Configuration(Consumer http2ConfigurationBuilderConsumer); } /** @@ -394,6 +446,7 @@ private static final class DefaultBuilder implements Builder { private SdkEventLoopGroup eventLoopGroup; private SdkEventLoopGroup.Builder eventLoopGroupBuilder; private Integer maxHttp2Streams; + private Http2Configuration http2Configuration; private SslProvider sslProvider; private ProxyConfiguration proxyConfiguration; @@ -568,6 +621,23 @@ public Builder tlsKeyManagersProvider(TlsKeyManagersProvider tlsKeyManagersProvi return this; } + @Override + public Builder http2Configuration(Http2Configuration http2Configuration) { + this.http2Configuration = http2Configuration; + return this; + } + + @Override + public Builder http2Configuration(Consumer http2ConfigurationBuilderConsumer) { + Http2Configuration.Builder builder = Http2Configuration.builder(); + http2ConfigurationBuilderConsumer.accept(builder); + return http2Configuration(builder.build()); + } + + public void setHttp2Configuration(Http2Configuration http2Configuration) { + http2Configuration(http2Configuration); + } + @Override public SdkAsyncHttpClient buildWithDefaults(AttributeMap serviceDefaults) { return new NettyNioAsyncHttpClient(this, standardOptions.build() diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java index 1376eb594464..647b40a4139c 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java @@ -81,6 +81,7 @@ public void channelCreated(Channel ch) throws Exception { private final NettyConfiguration configuration; private final Protocol protocol; private final long maxStreams; + private final int initialWindowSize; private final SslProvider sslProvider; private final ProxyConfiguration proxyConfiguration; @@ -90,6 +91,7 @@ private AwaitCloseChannelPoolMap(Builder builder) { this.configuration = builder.configuration; this.protocol = builder.protocol; this.maxStreams = builder.maxStreams; + this.initialWindowSize = builder.initialWindowSize; this.sslProvider = builder.sslProvider; this.proxyConfiguration = builder.proxyConfiguration; } @@ -112,8 +114,13 @@ protected SimpleChannelPoolAwareChannelPool newPool(URI key) { AtomicReference channelPoolRef = new AtomicReference<>(); - ChannelPipelineInitializer pipelineInitializer = - new ChannelPipelineInitializer(protocol, sslContext, maxStreams, channelPoolRef, configuration, key); + ChannelPipelineInitializer pipelineInitializer = new ChannelPipelineInitializer(protocol, + sslContext, + maxStreams, + initialWindowSize, + channelPoolRef, + configuration, + key); BetterSimpleChannelPool tcpChannelPool; ChannelPool baseChannelPool; @@ -289,6 +296,7 @@ public static class Builder { private NettyConfiguration configuration; private Protocol protocol; private long maxStreams; + private int initialWindowSize; private SslProvider sslProvider; private ProxyConfiguration proxyConfiguration; @@ -320,6 +328,11 @@ public Builder maxStreams(long maxStreams) { return this; } + public Builder initialWindowSize(int initialWindowSize) { + this.initialWindowSize = initialWindowSize; + return this; + } + public Builder sslProvider(SslProvider sslProvider) { this.sslProvider = sslProvider; return this; diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ChannelPipelineInitializer.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ChannelPipelineInitializer.java index a6b6fe4e47bd..7a17716e5296 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ChannelPipelineInitializer.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ChannelPipelineInitializer.java @@ -53,6 +53,7 @@ public final class ChannelPipelineInitializer extends AbstractChannelPoolHandler private final Protocol protocol; private final SslContext sslCtx; private final long clientMaxStreams; + private final int clientInitialWindowSize; private final AtomicReference channelPoolRef; private final NettyConfiguration configuration; private final URI poolKey; @@ -60,12 +61,14 @@ public final class ChannelPipelineInitializer extends AbstractChannelPoolHandler public ChannelPipelineInitializer(Protocol protocol, SslContext sslCtx, long clientMaxStreams, + int clientInitialWindowSize, AtomicReference channelPoolRef, NettyConfiguration configuration, URI poolKey) { this.protocol = protocol; this.sslCtx = sslCtx; this.clientMaxStreams = clientMaxStreams; + this.clientInitialWindowSize = clientInitialWindowSize; this.channelPoolRef = channelPoolRef; this.configuration = configuration; this.poolKey = poolKey; @@ -125,7 +128,7 @@ private void configureHttp2(Channel ch, ChannelPipeline pipeline) { Http2FrameCodec codec = Http2FrameCodecBuilder.forClient() .headerSensitivityDetector((name, value) -> lowerCase(name.toString()).equals("authorization")) - .initialSettings(Http2Settings.defaultSettings().initialWindowSize(1_048_576)) + .initialSettings(Http2Settings.defaultSettings().initialWindowSize(clientInitialWindowSize)) .frameLogger(new Http2FrameLogger(LogLevel.DEBUG)) .build(); diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/Http2ConfigurationTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/Http2ConfigurationTest.java new file mode 100644 index 000000000000..99173491c0e0 --- /dev/null +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/Http2ConfigurationTest.java @@ -0,0 +1,91 @@ +/* + * Copyright 2010-2019 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 static org.assertj.core.api.Assertions.assertThat; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class Http2ConfigurationTest { + @Rule + public ExpectedException expected = ExpectedException.none(); + + @Test + public void builder_returnsInstance() { + assertThat(Http2Configuration.builder()).isNotNull(); + } + + @Test + public void build_buildsCorrectConfig() { + long maxStreams = 1; + int initialWindowSize = 2; + + Http2Configuration config = Http2Configuration.builder() + .maxStreams(maxStreams) + .initialWindowSize(initialWindowSize) + .build(); + + assertThat(config.maxStreams()).isEqualTo(maxStreams); + assertThat(config.initialWindowSize()).isEqualTo(initialWindowSize); + } + + @Test + public void builder_toBuilder_roundTrip() { + Http2Configuration config1 = Http2Configuration.builder() + .maxStreams(7L) + .initialWindowSize(42) + .build(); + + Http2Configuration config2 = config1.toBuilder().build(); + + assertThat(config1).isEqualTo(config2); + } + + @Test + public void builder_maxStream_nullValue_doesNotThrow() { + Http2Configuration.builder().maxStreams(null); + } + + @Test + public void builder_maxStream_negative_throws() { + expected.expect(IllegalArgumentException.class); + Http2Configuration.builder().maxStreams(-1L); + } + + @Test + public void builder_maxStream_0_throws() { + expected.expect(IllegalArgumentException.class); + Http2Configuration.builder().maxStreams(0L); + } + + @Test + public void builder_initialWindowSize_nullValue_doesNotThrow() { + Http2Configuration.builder().initialWindowSize(null); + } + + @Test + public void builder_initialWindowSize_negative_throws() { + expected.expect(IllegalArgumentException.class); + Http2Configuration.builder().initialWindowSize(-1); + } + + @Test + public void builder_initialWindowSize_0_throws() { + expected.expect(IllegalArgumentException.class); + Http2Configuration.builder().initialWindowSize(0); + } +} 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 new file mode 100644 index 000000000000..6f89ab7b5aef --- /dev/null +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/http2/WindowSizeTest.java @@ -0,0 +1,286 @@ +/* + * Copyright 2010-2019 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.internal.http2; + +import static org.assertj.core.api.Assertions.assertThat; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +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.http2.DefaultHttp2Headers; +import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame; +import io.netty.handler.codec.http2.Http2DataFrame; +import io.netty.handler.codec.http2.Http2Frame; +import io.netty.handler.codec.http2.Http2FrameCodec; +import io.netty.handler.codec.http2.Http2FrameCodecBuilder; +import io.netty.handler.codec.http2.Http2HeadersFrame; +import io.netty.handler.codec.http2.Http2Settings; +import io.netty.handler.codec.http2.Http2SettingsFrame; +import io.netty.util.ReferenceCountUtil; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.reactivestreams.Publisher; +import software.amazon.awssdk.http.Protocol; +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; +import software.amazon.awssdk.http.nio.netty.EmptyPublisher; +import software.amazon.awssdk.http.nio.netty.Http2Configuration; +import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; + +public class WindowSizeTest { + private static final int DEFAULT_INIT_WINDOW_SIZE = 1024 * 1024; + + private TestH2Server server; + private SdkAsyncHttpClient netty; + + @Rule + public ExpectedException expected = ExpectedException.none(); + + @After + public void methodTeardown() throws InterruptedException { + if (netty != null) { + netty.close(); + } + netty = null; + + if (server != null) { + server.shutdown(); + } + server = null; + } + + @Test + public void builderSetter_negativeValue_throws() { + expected.expect(IllegalArgumentException.class); + + NettyNioAsyncHttpClient.builder() + .http2Configuration(Http2Configuration.builder() + .initialWindowSize(-1) + .build()) + .build(); + } + + @Test + public void builderSetter_0Value_throws() { + expected.expect(IllegalArgumentException.class); + + NettyNioAsyncHttpClient.builder() + .http2Configuration(Http2Configuration.builder() + .initialWindowSize(0) + .build()) + .build(); + } + + @Test + public void builderSetter_explicitNullSet_usesDefaultValue() throws InterruptedException { + expectCorrectWindowSizeValueTest(null, DEFAULT_INIT_WINDOW_SIZE); + } + + @Test + public void execute_customWindowValue_valueSentInSettings() throws InterruptedException { + int windowSize = 128 * 1024 * 1024; + expectCorrectWindowSizeValueTest(windowSize, windowSize); + } + + @Test + public void execute_noExplicitValueSet_sendsDefaultValueInSettings() throws InterruptedException { + ConcurrentLinkedQueue receivedFrames = new ConcurrentLinkedQueue<>(); + + server = new TestH2Server(() -> new StreamHandler(receivedFrames)); + + server.init(); + + netty = NettyNioAsyncHttpClient.builder() + .protocol(Protocol.HTTP2) + .build(); + + AsyncExecuteRequest req = AsyncExecuteRequest.builder() + .requestContentPublisher(new EmptyPublisher()) + .request(SdkHttpFullRequest.builder() + .method(SdkHttpMethod.GET) + .protocol("http") + .host("localhost") + .port(server.port()) + .build()) + .responseHandler(new SdkAsyncHttpResponseHandler() { + @Override + public void onHeaders(SdkHttpResponse headers) { + } + + @Override + public void onStream(Publisher stream) { + } + + @Override + public void onError(Throwable error) { + } + }) + .build(); + + netty.execute(req).join(); + + List receivedSettings = receivedFrames.stream() + .filter(f -> f instanceof Http2SettingsFrame) + .map(f -> (Http2SettingsFrame) f) + .map(Http2SettingsFrame::settings) + .collect(Collectors.toList()); + + assertThat(receivedSettings.size()).isGreaterThan(0); + for (Http2Settings s : receivedSettings) { + assertThat(s.initialWindowSize()).isEqualTo(DEFAULT_INIT_WINDOW_SIZE); + } + } + + private void expectCorrectWindowSizeValueTest(Integer builderSetterValue, int settingsFrameValue) throws InterruptedException { + ConcurrentLinkedQueue receivedFrames = new ConcurrentLinkedQueue<>(); + + server = new TestH2Server(() -> new StreamHandler(receivedFrames)); + + server.init(); + + netty = NettyNioAsyncHttpClient.builder() + .protocol(Protocol.HTTP2) + .http2Configuration(Http2Configuration.builder() + .initialWindowSize(builderSetterValue) + .build()) + .build(); + + AsyncExecuteRequest req = AsyncExecuteRequest.builder() + .requestContentPublisher(new EmptyPublisher()) + .request(SdkHttpFullRequest.builder() + .method(SdkHttpMethod.GET) + .protocol("http") + .host("localhost") + .port(server.port()) + .build()) + .responseHandler(new SdkAsyncHttpResponseHandler() { + @Override + public void onHeaders(SdkHttpResponse headers) { + } + + @Override + public void onStream(Publisher stream) { + } + + @Override + public void onError(Throwable error) { + } + }) + .build(); + + netty.execute(req).join(); + + + List receivedSettings = receivedFrames.stream() + .filter(f -> f instanceof Http2SettingsFrame) + .map(f -> (Http2SettingsFrame) f) + .map(Http2SettingsFrame::settings) + .collect(Collectors.toList()); + + assertThat(receivedSettings.size()).isGreaterThan(0); + for (Http2Settings s : receivedSettings) { + assertThat(s.initialWindowSize()).isEqualTo(settingsFrameValue); + } + } + + private static final class TestH2Server extends ChannelInitializer { + private final Supplier handlerSupplier; + + private ServerBootstrap bootstrap; + private ServerSocketChannel channel; + + private TestH2Server(Supplier handlerSupplier) { + this.handlerSupplier = handlerSupplier; + } + + public void init() throws InterruptedException { + bootstrap = new ServerBootstrap() + .channel(NioServerSocketChannel.class) + .group(new NioEventLoopGroup()) + .childHandler(this) + .localAddress(0) + .childOption(ChannelOption.SO_KEEPALIVE, true); + + channel = ((ServerSocketChannel) bootstrap.bind().await().channel()); + } + + public int port() { + return channel.localAddress().getPort(); + } + + public void shutdown() throws InterruptedException { + channel.close().await(); + } + + @Override + protected void initChannel(SocketChannel ch) { + Http2FrameCodec codec = Http2FrameCodecBuilder.forServer() + .initialSettings(new Http2Settings() + .maxConcurrentStreams(5)) + .build(); + + ch.pipeline().addLast(codec); + ch.pipeline().addLast(handlerSupplier.get()); + } + } + + private static class StreamHandler extends ChannelInboundHandlerAdapter { + private final Queue receivedFrames; + + private StreamHandler(Queue receivedFrames) { + this.receivedFrames = receivedFrames; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (!(msg instanceof Http2Frame)) { + ctx.fireChannelRead(msg); + return; + } + + Http2Frame frame = (Http2Frame) msg; + receivedFrames.add(frame); + if (frame instanceof Http2DataFrame) { + Http2DataFrame dataFrame = (Http2DataFrame) frame; + if (dataFrame.isEndStream()) { + Http2HeadersFrame respHeaders = new DefaultHttp2HeadersFrame( + new DefaultHttp2Headers().status("204"), true) + .stream(dataFrame.stream()); + ctx.writeAndFlush(respHeaders); + } + } + ReferenceCountUtil.release(frame); + } + } +} diff --git a/utils/src/main/java/software/amazon/awssdk/utils/Validate.java b/utils/src/main/java/software/amazon/awssdk/utils/Validate.java index 960aa9d455ab..4220c4b9293e 100644 --- a/utils/src/main/java/software/amazon/awssdk/utils/Validate.java +++ b/utils/src/main/java/software/amazon/awssdk/utils/Validate.java @@ -643,6 +643,36 @@ public static Duration isPositiveOrNull(Duration duration, String fieldName) { return isPositive(duration, fieldName); } + /** + * Asserts that the given boxed integer is positive (non-negative and non-zero) or null. + * + * @param num Boxed integer to validate + * @param fieldName Field name to display in exception message if not positive. + * @return Duration if positive or null. + */ + public static Integer isPositiveOrNull(Integer num, String fieldName) { + if (num == null) { + return null; + } + + return isPositive(num, fieldName); + } + + /** + * Asserts that the given boxed long is positive (non-negative and non-zero) or null. + * + * @param num Boxed long to validate + * @param fieldName Field name to display in exception message if not positive. + * @return Duration if positive or null. + */ + public static Long isPositiveOrNull(Long num, String fieldName) { + if (num == null) { + return null; + } + + return isPositive(num, fieldName); + } + /** * Asserts that the given duration is positive (non-negative and non-zero). * diff --git a/utils/src/test/java/software/amazon/awssdk/utils/ValidateTest.java b/utils/src/test/java/software/amazon/awssdk/utils/ValidateTest.java index fbfc5f8c51d2..9badd7de1c26 100644 --- a/utils/src/test/java/software/amazon/awssdk/utils/ValidateTest.java +++ b/utils/src/test/java/software/amazon/awssdk/utils/ValidateTest.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.utils; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.fail; @@ -25,7 +26,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; /** * Unit tests {@link Validate}. @@ -33,6 +36,8 @@ * Adapted from https://github.com/apache/commons-lang. */ public class ValidateTest { + @Rule + public ExpectedException expected = ExpectedException.none(); //----------------------------------------------------------------------- @Test @@ -523,4 +528,53 @@ public void mutuallyExclusive_MultipleProvided_DoesNotThrow() { Validate.mutuallyExclusive("error", null, "foo", "bar"); } + @Test + public void isPositiveOrNullInteger_null_returnsNull() { + assertNull(Validate.isPositiveOrNull((Integer) null, "foo")); + } + + @Test + public void isPositiveOrNullInteger_positive_returnsInteger() { + Integer num = 42; + assertEquals(num, Validate.isPositiveOrNull(num, "foo")); + } + + @Test + public void isPositiveOrNullInteger_zero_throws() { + expected.expect(IllegalArgumentException.class); + expected.expectMessage("foo"); + Validate.isPositiveOrNull(0, "foo"); + } + + @Test + public void isPositiveOrNullInteger_negative_throws() { + expected.expect(IllegalArgumentException.class); + expected.expectMessage("foo"); + Validate.isPositiveOrNull(-1, "foo"); + } + + @Test + public void isPositiveOrNullLong_null_returnsNull() { + assertNull(Validate.isPositiveOrNull((Long) null, "foo")); + } + + @Test + public void isPositiveOrNullLong_positive_returnsInteger() { + Long num = 42L; + assertEquals(num, Validate.isPositiveOrNull(num, "foo")); + } + + @Test + public void isPositiveOrNullLong_zero_throws() { + expected.expect(IllegalArgumentException.class); + expected.expectMessage("foo"); + Validate.isPositiveOrNull(0L, "foo"); + } + + @Test + public void isPositiveOrNullLong_negative_throws() { + expected.expect(IllegalArgumentException.class); + expected.expectMessage("foo"); + Validate.isPositiveOrNull(-1L, "foo"); + } }