netty5/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2ConnectionHandlerBuilder.java
Scott Mitchell b4e3c12b8e
Http2ConnectionHandler to allow decoupling close(..) from GOAWAY graceful close (#9094)
Motivation:
Http2ConnectionHandler#close(..) always runs the GOAWAY and graceful close
logic. This coupling means that a user would have to override
Http2ConnectionHandler#close(..) to modify the behavior, and the
Http2FrameCodec and Http2MultiplexCodec are not extendable so you cannot
override at this layer. Ideally we can totally decouple the close(..) of the
transport and the GOAWAY graceful closure process completely, but to preserve
backwards compatibility we can add an opt-out option to decouple where the
application is responsible for sending a GOAWAY with error code equal to
NO_ERROR as described in https://tools.ietf.org/html/rfc7540#section-6.8 in
order to initiate graceful close.

Modifications:
- Http2ConnectionHandler supports an additional boolean constructor argument to
opt out of close(..) going through the graceful close path.
- Http2FrameCodecBuilder and Http2MultiplexCodec expose
 gracefulShutdownTimeoutMillis but do not hook them up properly. Since these
are already exposed we should hook them up and make sure the timeout is applied
properly.
- Http2ConnectionHandler's goAway(..) method from Http2LifecycleManager should
initiate the graceful closure process after writing a GOAWAY frame if the error
code is NO_ERROR. This means that writing a Http2GoAwayFrame from
Http2FrameCodec will initiate graceful close.

Result:
Http2ConnectionHandler#close(..) can now be decoupled from the graceful close
process, and immediately close the underlying transport if desired.
2019-04-28 17:48:04 -07:00

525 lines
21 KiB
Java

/*
* Copyright 2015 The Netty Project
*
* The Netty Project licenses this file to you 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.netty.handler.codec.http2;
import io.netty.channel.Channel;
import io.netty.handler.codec.http2.Http2HeadersEncoder.SensitivityDetector;
import io.netty.util.internal.UnstableApi;
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_HEADER_LIST_SIZE;
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_INITIAL_HUFFMAN_DECODE_CAPACITY;
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_MAX_RESERVED_STREAMS;
import static io.netty.handler.codec.http2.Http2PromisedRequestVerifier.ALWAYS_VERIFY;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import static io.netty.util.internal.ObjectUtil.checkPositive;
import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
/**
* Abstract base class which defines commonly used features required to build {@link Http2ConnectionHandler} instances.
*
* <h3>Three ways to build a {@link Http2ConnectionHandler}</h3>
* <h4>Let the builder create a {@link Http2ConnectionHandler}</h4>
* Simply call all the necessary setter methods, and then use {@link #build()} to build a new
* {@link Http2ConnectionHandler}. Setting the following properties are prohibited because they are used for
* other ways of building a {@link Http2ConnectionHandler}.
* conflicts with this option:
* <ul>
* <li>{@link #connection(Http2Connection)}</li>
* <li>{@link #codec(Http2ConnectionDecoder, Http2ConnectionEncoder)}</li>
* </ul>
*
*
* <h4>Let the builder use the {@link Http2ConnectionHandler} you specified</h4>
* Call {@link #connection(Http2Connection)} to tell the builder that you want to build the handler from the
* {@link Http2Connection} you specified. Setting the following properties are prohibited and thus will trigger
* an {@link IllegalStateException} because they conflict with this option.
* <ul>
* <li>{@link #server(boolean)}</li>
* <li>{@link #codec(Http2ConnectionDecoder, Http2ConnectionEncoder)}</li>
* </ul>
*
* <h4>Let the builder use the {@link Http2ConnectionDecoder} and {@link Http2ConnectionEncoder} you specified</h4>
* Call {@link #codec(Http2ConnectionDecoder, Http2ConnectionEncoder)} to tell the builder that you want to built the
* handler from the {@link Http2ConnectionDecoder} and {@link Http2ConnectionEncoder} you specified. Setting the
* following properties are prohibited and thus will trigger an {@link IllegalStateException} because they conflict
* with this option:
* <ul>
* <li>{@link #server(boolean)}</li>
* <li>{@link #connection(Http2Connection)}</li>
* <li>{@link #frameLogger(Http2FrameLogger)}</li>
* <li>{@link #headerSensitivityDetector(SensitivityDetector)}</li>
* <li>{@link #encoderEnforceMaxConcurrentStreams(boolean)}</li>
* <li>{@link #encoderIgnoreMaxHeaderListSize(boolean)}</li>
* <li>{@link #initialHuffmanDecodeCapacity(int)}</li>
* </ul>
*
* <h3>Exposing necessary methods in a subclass</h3>
* {@link #build()} method and all property access methods are {@code protected}. Choose the methods to expose to the
* users of your builder implementation and make them {@code public}.
*
* @param <T> The type of handler created by this builder.
* @param <B> The concrete type of this builder.
*/
@UnstableApi
public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2ConnectionHandler,
B extends AbstractHttp2ConnectionHandlerBuilder<T, B>> {
private static final SensitivityDetector DEFAULT_HEADER_SENSITIVITY_DETECTOR = Http2HeadersEncoder.NEVER_SENSITIVE;
// The properties that can always be set.
private Http2Settings initialSettings = Http2Settings.defaultSettings();
private Http2FrameListener frameListener;
private long gracefulShutdownTimeoutMillis = Http2CodecUtil.DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT_MILLIS;
private boolean decoupleCloseAndGoAway;
// The property that will prohibit connection() and codec() if set by server(),
// because this property is used only when this builder creates a Http2Connection.
private Boolean isServer;
private Integer maxReservedStreams;
// The property that will prohibit server() and codec() if set by connection().
private Http2Connection connection;
// The properties that will prohibit server() and connection() if set by codec().
private Http2ConnectionDecoder decoder;
private Http2ConnectionEncoder encoder;
// The properties that are:
// * mutually exclusive against codec() and
// * OK to use with server() and connection()
private Boolean validateHeaders;
private Http2FrameLogger frameLogger;
private SensitivityDetector headerSensitivityDetector;
private Boolean encoderEnforceMaxConcurrentStreams;
private Boolean encoderIgnoreMaxHeaderListSize;
private int initialHuffmanDecodeCapacity = DEFAULT_INITIAL_HUFFMAN_DECODE_CAPACITY;
private Http2PromisedRequestVerifier promisedRequestVerifier = ALWAYS_VERIFY;
private boolean autoAckSettingsFrame = true;
/**
* Sets the {@link Http2Settings} to use for the initial connection settings exchange.
*/
protected Http2Settings initialSettings() {
return initialSettings;
}
/**
* Sets the {@link Http2Settings} to use for the initial connection settings exchange.
*/
protected B initialSettings(Http2Settings settings) {
initialSettings = checkNotNull(settings, "settings");
return self();
}
/**
* Returns the listener of inbound frames.
*
* @return {@link Http2FrameListener} if set, or {@code null} if not set.
*/
protected Http2FrameListener frameListener() {
return frameListener;
}
/**
* Sets the listener of inbound frames.
* This listener will only be set if the decoder's listener is {@code null}.
*/
protected B frameListener(Http2FrameListener frameListener) {
this.frameListener = checkNotNull(frameListener, "frameListener");
return self();
}
/**
* Returns the graceful shutdown timeout of the {@link Http2Connection} in milliseconds. Returns -1 if the
* timeout is indefinite.
*/
protected long gracefulShutdownTimeoutMillis() {
return gracefulShutdownTimeoutMillis;
}
/**
* Sets the graceful shutdown timeout of the {@link Http2Connection} in milliseconds.
*/
protected B gracefulShutdownTimeoutMillis(long gracefulShutdownTimeoutMillis) {
if (gracefulShutdownTimeoutMillis < -1) {
throw new IllegalArgumentException("gracefulShutdownTimeoutMillis: " + gracefulShutdownTimeoutMillis +
" (expected: -1 for indefinite or >= 0)");
}
this.gracefulShutdownTimeoutMillis = gracefulShutdownTimeoutMillis;
return self();
}
/**
* Returns if {@link #build()} will to create a {@link Http2Connection} in server mode ({@code true})
* or client mode ({@code false}).
*/
protected boolean isServer() {
return isServer != null ? isServer : true;
}
/**
* Sets if {@link #build()} will to create a {@link Http2Connection} in server mode ({@code true})
* or client mode ({@code false}).
*/
protected B server(boolean isServer) {
enforceConstraint("server", "connection", connection);
enforceConstraint("server", "codec", decoder);
enforceConstraint("server", "codec", encoder);
this.isServer = isServer;
return self();
}
/**
* Get the maximum number of streams which can be in the reserved state at any given time.
* <p>
* By default this value will be ignored on the server for local endpoint. This is because the RFC provides
* no way to explicitly communicate a limit to how many states can be in the reserved state, and instead relies
* on the peer to send RST_STREAM frames when they will be rejected.
*/
protected int maxReservedStreams() {
return maxReservedStreams != null ? maxReservedStreams : DEFAULT_MAX_RESERVED_STREAMS;
}
/**
* Set the maximum number of streams which can be in the reserved state at any given time.
*/
protected B maxReservedStreams(int maxReservedStreams) {
enforceConstraint("server", "connection", connection);
enforceConstraint("server", "codec", decoder);
enforceConstraint("server", "codec", encoder);
this.maxReservedStreams = checkPositiveOrZero(maxReservedStreams, "maxReservedStreams");
return self();
}
/**
* Returns the {@link Http2Connection} to use.
*
* @return {@link Http2Connection} if set, or {@code null} if not set.
*/
protected Http2Connection connection() {
return connection;
}
/**
* Sets the {@link Http2Connection} to use.
*/
protected B connection(Http2Connection connection) {
enforceConstraint("connection", "maxReservedStreams", maxReservedStreams);
enforceConstraint("connection", "server", isServer);
enforceConstraint("connection", "codec", decoder);
enforceConstraint("connection", "codec", encoder);
this.connection = checkNotNull(connection, "connection");
return self();
}
/**
* Returns the {@link Http2ConnectionDecoder} to use.
*
* @return {@link Http2ConnectionDecoder} if set, or {@code null} if not set.
*/
protected Http2ConnectionDecoder decoder() {
return decoder;
}
/**
* Returns the {@link Http2ConnectionEncoder} to use.
*
* @return {@link Http2ConnectionEncoder} if set, or {@code null} if not set.
*/
protected Http2ConnectionEncoder encoder() {
return encoder;
}
/**
* Sets the {@link Http2ConnectionDecoder} and {@link Http2ConnectionEncoder} to use.
*/
protected B codec(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder) {
enforceConstraint("codec", "server", isServer);
enforceConstraint("codec", "maxReservedStreams", maxReservedStreams);
enforceConstraint("codec", "connection", connection);
enforceConstraint("codec", "frameLogger", frameLogger);
enforceConstraint("codec", "validateHeaders", validateHeaders);
enforceConstraint("codec", "headerSensitivityDetector", headerSensitivityDetector);
enforceConstraint("codec", "encoderEnforceMaxConcurrentStreams", encoderEnforceMaxConcurrentStreams);
checkNotNull(decoder, "decoder");
checkNotNull(encoder, "encoder");
if (decoder.connection() != encoder.connection()) {
throw new IllegalArgumentException("The specified encoder and decoder have different connections.");
}
this.decoder = decoder;
this.encoder = encoder;
return self();
}
/**
* Returns if HTTP headers should be validated according to
* <a href="https://tools.ietf.org/html/rfc7540#section-8.1.2.6">RFC 7540, 8.1.2.6</a>.
*/
protected boolean isValidateHeaders() {
return validateHeaders != null ? validateHeaders : true;
}
/**
* Sets if HTTP headers should be validated according to
* <a href="https://tools.ietf.org/html/rfc7540#section-8.1.2.6">RFC 7540, 8.1.2.6</a>.
*/
protected B validateHeaders(boolean validateHeaders) {
enforceNonCodecConstraints("validateHeaders");
this.validateHeaders = validateHeaders;
return self();
}
/**
* Returns the logger that is used for the encoder and decoder.
*
* @return {@link Http2FrameLogger} if set, or {@code null} if not set.
*/
protected Http2FrameLogger frameLogger() {
return frameLogger;
}
/**
* Sets the logger that is used for the encoder and decoder.
*/
protected B frameLogger(Http2FrameLogger frameLogger) {
enforceNonCodecConstraints("frameLogger");
this.frameLogger = checkNotNull(frameLogger, "frameLogger");
return self();
}
/**
* Returns if the encoder should queue frames if the maximum number of concurrent streams
* would otherwise be exceeded.
*/
protected boolean encoderEnforceMaxConcurrentStreams() {
return encoderEnforceMaxConcurrentStreams != null ? encoderEnforceMaxConcurrentStreams : false;
}
/**
* Sets if the encoder should queue frames if the maximum number of concurrent streams
* would otherwise be exceeded.
*/
protected B encoderEnforceMaxConcurrentStreams(boolean encoderEnforceMaxConcurrentStreams) {
enforceNonCodecConstraints("encoderEnforceMaxConcurrentStreams");
this.encoderEnforceMaxConcurrentStreams = encoderEnforceMaxConcurrentStreams;
return self();
}
/**
* Returns the {@link SensitivityDetector} to use.
*/
protected SensitivityDetector headerSensitivityDetector() {
return headerSensitivityDetector != null ? headerSensitivityDetector : DEFAULT_HEADER_SENSITIVITY_DETECTOR;
}
/**
* Sets the {@link SensitivityDetector} to use.
*/
protected B headerSensitivityDetector(SensitivityDetector headerSensitivityDetector) {
enforceNonCodecConstraints("headerSensitivityDetector");
this.headerSensitivityDetector = checkNotNull(headerSensitivityDetector, "headerSensitivityDetector");
return self();
}
/**
* Sets if the <a href="https://tools.ietf.org/html/rfc7540#section-6.5.2">SETTINGS_MAX_HEADER_LIST_SIZE</a>
* should be ignored when encoding headers.
* @param ignoreMaxHeaderListSize {@code true} to ignore
* <a href="https://tools.ietf.org/html/rfc7540#section-6.5.2">SETTINGS_MAX_HEADER_LIST_SIZE</a>.
* @return this.
*/
protected B encoderIgnoreMaxHeaderListSize(boolean ignoreMaxHeaderListSize) {
enforceNonCodecConstraints("encoderIgnoreMaxHeaderListSize");
this.encoderIgnoreMaxHeaderListSize = ignoreMaxHeaderListSize;
return self();
}
/**
* Sets the initial size of an intermediate buffer used during HPACK huffman decoding.
* @param initialHuffmanDecodeCapacity initial size of an intermediate buffer used during HPACK huffman decoding.
* @return this.
*/
protected B initialHuffmanDecodeCapacity(int initialHuffmanDecodeCapacity) {
enforceNonCodecConstraints("initialHuffmanDecodeCapacity");
this.initialHuffmanDecodeCapacity = checkPositive(initialHuffmanDecodeCapacity, "initialHuffmanDecodeCapacity");
return self();
}
/**
* Set the {@link Http2PromisedRequestVerifier} to use.
* @return this.
*/
protected B promisedRequestVerifier(Http2PromisedRequestVerifier promisedRequestVerifier) {
enforceNonCodecConstraints("promisedRequestVerifier");
this.promisedRequestVerifier = checkNotNull(promisedRequestVerifier, "promisedRequestVerifier");
return self();
}
/**
* Get the {@link Http2PromisedRequestVerifier} to use.
* @return the {@link Http2PromisedRequestVerifier} to use.
*/
protected Http2PromisedRequestVerifier promisedRequestVerifier() {
return promisedRequestVerifier;
}
/**
* Determine if settings frame should automatically be acknowledged and applied.
* @return this.
*/
protected B autoAckSettingsFrame(boolean autoAckSettings) {
enforceNonCodecConstraints("autoAckSettingsFrame");
this.autoAckSettingsFrame = autoAckSettings;
return self();
}
/**
* Determine if the SETTINGS frames should be automatically acknowledged and applied.
* @return {@code true} if the SETTINGS frames should be automatically acknowledged and applied.
*/
protected boolean isAutoAckSettingsFrame() {
return autoAckSettingsFrame;
}
/**
* Determine if the {@link Channel#close()} should be coupled with goaway and graceful close.
* @param decoupleCloseAndGoAway {@code true} to make {@link Channel#close()} directly close the underlying
* transport, and not attempt graceful closure via GOAWAY.
* @return {@code this}.
*/
protected B decoupleCloseAndGoAway(boolean decoupleCloseAndGoAway) {
this.decoupleCloseAndGoAway = decoupleCloseAndGoAway;
return self();
}
/**
* Determine if the {@link Channel#close()} should be coupled with goaway and graceful close.
*/
protected boolean decoupleCloseAndGoAway() {
return decoupleCloseAndGoAway;
}
/**
* Create a new {@link Http2ConnectionHandler}.
*/
protected T build() {
if (encoder != null) {
assert decoder != null;
return buildFromCodec(decoder, encoder);
}
Http2Connection connection = this.connection;
if (connection == null) {
connection = new DefaultHttp2Connection(isServer(), maxReservedStreams());
}
return buildFromConnection(connection);
}
private T buildFromConnection(Http2Connection connection) {
Long maxHeaderListSize = initialSettings.maxHeaderListSize();
Http2FrameReader reader = new DefaultHttp2FrameReader(new DefaultHttp2HeadersDecoder(isValidateHeaders(),
maxHeaderListSize == null ? DEFAULT_HEADER_LIST_SIZE : maxHeaderListSize,
initialHuffmanDecodeCapacity));
Http2FrameWriter writer = encoderIgnoreMaxHeaderListSize == null ?
new DefaultHttp2FrameWriter(headerSensitivityDetector()) :
new DefaultHttp2FrameWriter(headerSensitivityDetector(), encoderIgnoreMaxHeaderListSize);
if (frameLogger != null) {
reader = new Http2InboundFrameLogger(reader, frameLogger);
writer = new Http2OutboundFrameLogger(writer, frameLogger);
}
Http2ConnectionEncoder encoder = new DefaultHttp2ConnectionEncoder(connection, writer);
boolean encoderEnforceMaxConcurrentStreams = encoderEnforceMaxConcurrentStreams();
if (encoderEnforceMaxConcurrentStreams) {
if (connection.isServer()) {
encoder.close();
reader.close();
throw new IllegalArgumentException(
"encoderEnforceMaxConcurrentStreams: " + encoderEnforceMaxConcurrentStreams +
" not supported for server");
}
encoder = new StreamBufferingEncoder(encoder);
}
DefaultHttp2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, reader,
promisedRequestVerifier(), isAutoAckSettingsFrame());
return buildFromCodec(decoder, encoder);
}
private T buildFromCodec(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder) {
final T handler;
try {
// Call the abstract build method
handler = build(decoder, encoder, initialSettings);
} catch (Throwable t) {
encoder.close();
decoder.close();
throw new IllegalStateException("failed to build a Http2ConnectionHandler", t);
}
// Setup post build options
handler.gracefulShutdownTimeoutMillis(gracefulShutdownTimeoutMillis);
if (handler.decoder().frameListener() == null) {
handler.decoder().frameListener(frameListener);
}
return handler;
}
/**
* Implement this method to create a new {@link Http2ConnectionHandler} or its subtype instance.
* <p>
* The return of this method will be subject to the following:
* <ul>
* <li>{@link #frameListener(Http2FrameListener)} will be set if not already set in the decoder</li>
* <li>{@link #gracefulShutdownTimeoutMillis(long)} will always be set</li>
* </ul>
*/
protected abstract T build(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder,
Http2Settings initialSettings) throws Exception;
/**
* Returns {@code this}.
*/
@SuppressWarnings("unchecked")
protected final B self() {
return (B) this;
}
private void enforceNonCodecConstraints(String rejected) {
enforceConstraint(rejected, "server/connection", decoder);
enforceConstraint(rejected, "server/connection", encoder);
}
private static void enforceConstraint(String methodName, String rejectorName, Object value) {
if (value != null) {
throw new IllegalStateException(
methodName + "() cannot be called because " + rejectorName + "() has been called already.");
}
}
}