Skip to content

Commit

Permalink
HTTP/2 set initial settings
Browse files Browse the repository at this point in the history
Motivaiton:
There is no way to define initial values for
[http/2 settings](https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2).

Modifications:
- Enhance H2ProtocolConfigBuilder to accept Map<Character, Integer> to
  set the initial settings values.
- Set the default value of settings with bound max header list size
  and 1mb default initial flow control window.
- Increase flow control quantum to 16kb because the flow control window
  is larger. This allows streams to write an entire frame per write
  operation and increases goodput on the connection.
  • Loading branch information
Scottmitch committed Sep 3, 2022
1 parent ee34067 commit ac3ba07
Show file tree
Hide file tree
Showing 9 changed files with 350 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright © 2022 Apple Inc. and the ServiceTalk project authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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 io.servicetalk.http.api;

/**
* Utilities for <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.1">HTTP/2 Setting</a>.
*/
public final class Http2Settings {
/**
* Identifier <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">SETTINGS_HEADER_TABLE_SIZE</a>.
*/
public static final char HEADER_TABLE_SIZE = 0x1;

/**
* Identifier <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">SETTINGS_ENABLE_PUSH</a>.
*/
public static final char ENABLE_PUSH = 0x2;

/**
* Identifier <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">
* SETTINGS_MAX_CONCURRENT_STREAMS</a>.
*/
public static final char MAX_CONCURRENT_STREAMS = 0x3;

/**
* Identifier <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">
* SETTINGS_INITIAL_WINDOW_SIZE</a>.
*/
public static final char INITIAL_WINDOW_SIZE = 0x4;

/**
* Identifier <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">SETTINGS_MAX_FRAME_SIZE</a>.
*/
public static final char MAX_FRAME_SIZE = 0x5;

/**
* Identifier <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">
* SETTINGS_MAX_HEADER_LIST_SIZE</a>.
*/
public static final char MAX_HEADER_LIST_SIZE = 0x6;

private Http2Settings() {
}
}
10 changes: 10 additions & 0 deletions servicetalk-http-netty/gradle/spotbugs/main-exclusions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,14 @@
<Method name="handlerAdded"/>
<Bug pattern="THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION"/>
</Match>
<Match>
<Class name="io.servicetalk.http.netty.H2ProtocolConfigBuilder"/>
<Field name="h2Settings"/>
<Bug pattern="EI_EXPOSE_REP2"/>
</Match>
<Match>
<Class name="io.servicetalk.http.netty.Http2SettingsBuilder"/>
<Method name="build"/>
<Bug pattern="EI_EXPOSE_REP"/>
</Match>
</FindBugsFilter>
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,18 @@
import io.netty.handler.codec.http2.DefaultHttp2GoAwayFrame;
import io.netty.handler.codec.http2.Http2FrameCodecBuilder;
import io.netty.handler.codec.http2.Http2MultiplexHandler;
import io.netty.handler.codec.http2.Http2Settings;

import java.util.Map;
import java.util.function.BiPredicate;

import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
import static io.servicetalk.http.netty.H2ServerParentChannelInitializer.initFrameLogger;
import static io.servicetalk.http.netty.H2ServerParentChannelInitializer.toNettySettings;

final class H2ClientParentChannelInitializer implements ChannelInitializer {

private static final Http2Settings DEFAULT_NETTY_SETTINGS =
applyClientDefaultSettings(Http2Settings.defaultSettings());
private final H2ProtocolConfig config;

H2ClientParentChannelInitializer(final H2ProtocolConfig config) {
Expand All @@ -40,7 +44,8 @@ final class H2ClientParentChannelInitializer implements ChannelInitializer {

@Override
public void init(final Channel channel) {
final Http2FrameCodecBuilder multiplexCodecBuilder = new OptimizedHttp2FrameCodecBuilder(false)
final Http2FrameCodecBuilder multiplexCodecBuilder =
new OptimizedHttp2FrameCodecBuilder(false, config.flowControlQuantum())
// We do not want close to trigger graceful closure (go away), instead when user triggers a graceful
// close, we do the appropriate go away handling.
.decoupleCloseAndGoAway(true)
Expand All @@ -53,16 +58,20 @@ public void init(final Channel channel) {
// the user to apply their own timeout at the call site.
.gracefulShutdownTimeoutMillis(-1);

// Notify server that this client does not support server push and request it to be disabled.
multiplexCodecBuilder.initialSettings().pushEnabled(false).maxConcurrentStreams(0L);
final Map<Character, Integer> h2Settings = config.initialSettings();
if (h2Settings.isEmpty()) {
multiplexCodecBuilder.initialSettings(DEFAULT_NETTY_SETTINGS);
} else {
multiplexCodecBuilder.initialSettings(applyClientDefaultSettings(toNettySettings(h2Settings)));
}

final BiPredicate<CharSequence, CharSequence> headersSensitivityDetector =
config.headersSensitivityDetector();
multiplexCodecBuilder.headerSensitivityDetector(headersSensitivityDetector::test);

initFrameLogger(multiplexCodecBuilder, config.frameLoggerConfig());

// TODO(scott): more configuration. header validation, settings stream, etc...
// TODO(scott): more configuration. header validation, etc...

channel.pipeline().addLast(multiplexCodecBuilder.build(),
new Http2MultiplexHandler(H2PushStreamHandler.INSTANCE));
Expand All @@ -84,4 +93,9 @@ public void channelRegistered(final ChannelHandlerContext ctx) {
// Http2ConnectionHandler.processGoAwayWriteResult will close the connection after GO_AWAY is flushed
}
}

private static Http2Settings applyClientDefaultSettings(Http2Settings settings) {
// Notify server that this client does not support server push and request it to be disabled.
return settings.pushEnabled(false).maxConcurrentStreams(0L);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import io.servicetalk.logging.api.UserDataLoggerConfig;

import java.time.Duration;
import java.util.Map;
import java.util.function.BiPredicate;
import javax.annotation.Nullable;

Expand Down Expand Up @@ -60,6 +61,21 @@ default String alpnId() {
@Nullable
KeepAlivePolicy keepAlivePolicy();

/**
* Get a {@link Map} which provides a hint for the initial settings for any h2 connection. Note that some settings
* may be ignored if not supported (e.g. push promise).
* @return a {@link Map} which provides a hint for the initial settings for any h2 connection. Note that some
* settings may be ignored if not supported (e.g. push promise).
*/
Map<Character, Integer> initialSettings();

/**
* Provide a hint on the number of bytes that the flow controller will attempt to give to a stream for each
* allocation (assuming the stream has this much eligible data).
* @return number of bytes.
*/
int flowControlQuantum();

/**
* A policy for sending <a href="https://tools.ietf.org/html/rfc7540#section-6.7">PING frames</a> to the peer.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@
import io.servicetalk.logging.api.UserDataLoggerConfig;
import io.servicetalk.logging.slf4j.internal.DefaultUserDataLoggerConfig;

import java.util.Map;
import java.util.function.BiPredicate;
import java.util.function.BooleanSupplier;
import javax.annotation.Nullable;

import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_HEADER_LIST_SIZE;
import static io.servicetalk.http.netty.H2KeepAlivePolicies.DISABLE_KEEP_ALIVE;
import static java.util.Objects.requireNonNull;

Expand All @@ -35,15 +37,15 @@
* @see HttpProtocolConfigs#h2()
*/
public final class H2ProtocolConfigBuilder {

private static final BiPredicate<CharSequence, CharSequence> DEFAULT_SENSITIVITY_DETECTOR = (name, value) -> false;

private Map<Character, Integer> h2Settings = newDefaultSettingsBuilder().build();
private HttpHeadersFactory headersFactory = H2HeadersFactory.INSTANCE;
private BiPredicate<CharSequence, CharSequence> headersSensitivityDetector = DEFAULT_SENSITIVITY_DETECTOR;
@Nullable
private UserDataLoggerConfig frameLoggerConfig;
@Nullable
private KeepAlivePolicy keepAlivePolicy;
private int flowControlQuantum = defaultFlowControlQuantum();

H2ProtocolConfigBuilder() {
}
Expand Down Expand Up @@ -103,33 +105,79 @@ public H2ProtocolConfigBuilder keepAlivePolicy(final KeepAlivePolicy policy) {
return this;
}

/**
* Sets the initial <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.1">HTTP/2 Setting</a> to for
* each h2 connection.
* @param settings the initial settings to for each h2 connection.
* @return {@code this}
* @see Http2SettingsBuilder
*/
public H2ProtocolConfigBuilder initialSettings(Map<Character, Integer> settings) {
this.h2Settings = requireNonNull(settings);
return this;
}

/**
* Provide a hint on the number of bytes that the flow controller will attempt to give to a stream for each
* allocation (assuming the stream has this much eligible data).
* @param flowControlQuantum a hint on the number of bytes that the flow controller will attempt to give to a
* stream for each allocation (assuming the stream has this much eligible data).
* @return {@code this}
*/
public H2ProtocolConfigBuilder flowControlQuantum(int flowControlQuantum) {
if (flowControlQuantum <= 0) {
throw new IllegalArgumentException("flowControlQuantum " + flowControlQuantum + " (expected >0)");
}
this.flowControlQuantum = flowControlQuantum;
return this;
}

/**
* Builds {@link H2ProtocolConfig}.
*
* @return {@link H2ProtocolConfig}
*/
public H2ProtocolConfig build() {
return new DefaultH2ProtocolConfig(headersFactory, headersSensitivityDetector, frameLoggerConfig,
keepAlivePolicy);
return new DefaultH2ProtocolConfig(h2Settings, headersFactory, headersSensitivityDetector, frameLoggerConfig,
keepAlivePolicy, flowControlQuantum);
}

private static final class DefaultH2ProtocolConfig implements H2ProtocolConfig {
static Http2SettingsBuilder newDefaultSettingsBuilder() {
return new Http2SettingsBuilder()
.initialWindowSize(1048576) // 1mb default window size
.maxHeaderListSize((int) DEFAULT_HEADER_LIST_SIZE);
}

/**
* Default allocation quantum to use for the remote flow controller.
* @return Default allocation quantum to use for the remote flow controller.
*/
private static int defaultFlowControlQuantum() {
return 1024 * 16;
}

private static final class DefaultH2ProtocolConfig implements H2ProtocolConfig {
private final Map<Character, Integer> h2Settings;
private final HttpHeadersFactory headersFactory;
private final BiPredicate<CharSequence, CharSequence> headersSensitivityDetector;
@Nullable
private final UserDataLoggerConfig frameLoggerConfig;
@Nullable
private final KeepAlivePolicy keepAlivePolicy;
private final int flowControlQuantum;

DefaultH2ProtocolConfig(final HttpHeadersFactory headersFactory,
DefaultH2ProtocolConfig(final Map<Character, Integer> h2Settings,
final HttpHeadersFactory headersFactory,
final BiPredicate<CharSequence, CharSequence> headersSensitivityDetector,
@Nullable final UserDataLoggerConfig frameLoggerConfig,
@Nullable final KeepAlivePolicy keepAlivePolicy) {
@Nullable final KeepAlivePolicy keepAlivePolicy,
final int flowControlQuantum) {
this.h2Settings = h2Settings;
this.headersFactory = headersFactory;
this.headersSensitivityDetector = headersSensitivityDetector;
this.frameLoggerConfig = frameLoggerConfig;
this.keepAlivePolicy = keepAlivePolicy;
this.flowControlQuantum = flowControlQuantum;
}

@Override
Expand All @@ -154,16 +202,27 @@ public KeepAlivePolicy keepAlivePolicy() {
return keepAlivePolicy;
}

@Override
public Map<Character, Integer> initialSettings() {
return h2Settings;
}

@Override
public int flowControlQuantum() {
return flowControlQuantum;
}

@Override
public String toString() {
return getClass().getSimpleName() +
"{alpnId=" + alpnId() +
", headersFactory=" + headersFactory +
", headersSensitivityDetector=" + (headersSensitivityDetector == DEFAULT_SENSITIVITY_DETECTOR ?
"DEFAULT_SENSITIVITY_DETECTOR" : headersSensitivityDetector.toString()) +
"DEFAULT_SENSITIVITY_DETECTOR" : headersSensitivityDetector.toString()) +
", frameLoggerConfig=" + frameLoggerConfig +
", keepAlivePolicy=" + keepAlivePolicy +
'}';
", flowControlQuantum=" + flowControlQuantum +
", h2Settings=" + h2Settings + '}';
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package io.servicetalk.http.netty;

import io.servicetalk.http.api.Http2Settings;
import io.servicetalk.logging.api.UserDataLoggerConfig;
import io.servicetalk.transport.netty.internal.ChannelInitializer;

Expand All @@ -23,6 +24,7 @@
import io.netty.handler.codec.http2.Http2MultiplexHandler;
import io.netty.handler.codec.http2.Http2StreamChannel;

import java.util.Map;
import java.util.function.BiPredicate;
import javax.annotation.Nullable;

Expand All @@ -41,7 +43,8 @@ final class H2ServerParentChannelInitializer implements ChannelInitializer {

@Override
public void init(final Channel channel) {
final Http2FrameCodecBuilder multiplexCodecBuilder = new OptimizedHttp2FrameCodecBuilder(true)
final Http2FrameCodecBuilder multiplexCodecBuilder =
new OptimizedHttp2FrameCodecBuilder(true, config.flowControlQuantum())
// We do not want close to trigger graceful closure (go away), instead when user triggers a graceful
// close, we do the appropriate go away handling.
.decoupleCloseAndGoAway(true)
Expand All @@ -55,9 +58,14 @@ public void init(final Channel channel) {
config.headersSensitivityDetector();
multiplexCodecBuilder.headerSensitivityDetector(headersSensitivityDetector::test);

final Map<Character, Integer> h2Settings = config.initialSettings();
if (!h2Settings.isEmpty()) {
multiplexCodecBuilder.initialSettings(toNettySettings(h2Settings));
}

initFrameLogger(multiplexCodecBuilder, config.frameLoggerConfig());

// TODO(scott): more configuration. header validation, settings stream, etc...
// TODO(scott): more configuration. header validation, etc...

channel.pipeline().addLast(multiplexCodecBuilder.build(), new Http2MultiplexHandler(streamChannelInitializer));
}
Expand All @@ -70,4 +78,34 @@ static void initFrameLogger(final Http2FrameCodecBuilder multiplexCodecBuilder,
frameLoggerConfig.logLevel()), frameLoggerConfig.logUserData()));
}
}

static io.netty.handler.codec.http2.Http2Settings toNettySettings(Map<Character, Integer> h2Settings) {
io.netty.handler.codec.http2.Http2Settings nettySettings = new io.netty.handler.codec.http2.Http2Settings();
h2Settings.forEach((identifier, value) -> {
switch (identifier) {
case Http2Settings.HEADER_TABLE_SIZE:
nettySettings.headerTableSize(value);
break;
case Http2Settings.ENABLE_PUSH:
nettySettings.pushEnabled(value != 0);
break;
case Http2Settings.MAX_CONCURRENT_STREAMS:
nettySettings.maxConcurrentStreams(value);
break;
case Http2Settings.INITIAL_WINDOW_SIZE:
nettySettings.initialWindowSize(value);
break;
case Http2Settings.MAX_FRAME_SIZE:
nettySettings.maxFrameSize(value);
break;
case Http2Settings.MAX_HEADER_LIST_SIZE:
nettySettings.maxHeaderListSize(value);
break;
default:
nettySettings.put(identifier, Long.valueOf(value));
break;
}
});
return nettySettings;
}
}
Loading

0 comments on commit ac3ba07

Please sign in to comment.