Skip to content

Commit 603fc86

Browse files
ikhoonyzfeng2020
andauthored
Ensure HTTP/2 flow control to work at the stream level (#6266)
Motivation: Since `InboundTrafficController` operates at the TCP level, it isn't aligned with HTTP/2 flow control. As a result, a stream may receive more than its stream window size allows and interfering with other streams from receiving data. Related: #6253 Modifications: - Integrated `InboundTrafficController` with HTTP/2 `Http2LocalFlowController` - Invokes `Http2LocalFlowController.consumeBytes()` when steam data is consumed to send a `WINDOW_UPDATE` frame. - Introduced `{ServerBuilder,ClientFactoryBuilder}.http2StreamWindowUpdateRatio` to customize the threshold to send WINDOW_UPDATE frames. - Fixed `client.Http2ResponseDecoder` and `server.Http2RequestDecoder` to defer reporting the consumed data length. - The length is not reported via `InboundTrafficController` when the data is consumed in userland. Result: - Fixed a bug where HTTP/2 flow control did not work properly, and stream-level windowing was ignored. - Closes #6253 --------- Co-authored-by: Yizhou Feng <yizhou.feng@databricks.com>
1 parent 478bba9 commit 603fc86

28 files changed

Lines changed: 639 additions & 58 deletions

core/src/main/java/com/linecorp/armeria/client/AbstractHttpRequestHandler.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ final boolean tryInitialize() {
187187
}
188188

189189
this.session = session;
190+
originalRes.setId(id);
190191
responseWrapper = responseDecoder.addResponse(this, id, originalRes, ctx, ch.eventLoop());
191192

192193
if (timeoutMillis > 0) {

core/src/main/java/com/linecorp/armeria/client/ClientFactoryBuilder.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
import io.netty.channel.ChannelOption;
7777
import io.netty.channel.EventLoop;
7878
import io.netty.channel.EventLoopGroup;
79+
import io.netty.handler.codec.http2.DefaultHttp2LocalFlowController;
7980
import io.netty.handler.codec.http2.Http2CodecUtil;
8081
import io.netty.handler.ssl.SslContextBuilder;
8182
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
@@ -634,6 +635,26 @@ public ClientFactoryBuilder http2InitialStreamWindowSize(int http2InitialStreamW
634635
return this;
635636
}
636637

638+
/**
639+
* Sets the threshold ratio of the HTTP/2 stream flow-control window at which a
640+
* <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.9">WINDOW_UPDATE</a> frame will be sent.
641+
* When the size of the flow-control window drops below the specified ratio (relative to the initial window
642+
* size), a {@code WINDOW_UPDATE} frame is triggered to replenish the window.
643+
*
644+
* <p>The default value is {@value DefaultHttp2LocalFlowController#DEFAULT_WINDOW_UPDATE_RATIO}.
645+
* The value must be greater than 0 and less than 1.0.
646+
*
647+
* <p>Note: Do not change this value unless you know what you are doing.
648+
*/
649+
@UnstableApi
650+
public ClientFactoryBuilder http2StreamWindowUpdateRatio(float http2StreamWindowUpdateRatio) {
651+
checkArgument(http2StreamWindowUpdateRatio > 0 && http2StreamWindowUpdateRatio < 1.0f,
652+
"http2StreamWindowUpdateRatio: %s (expected: > 0 and < 1.0)",
653+
http2StreamWindowUpdateRatio);
654+
option(ClientFactoryOptions.HTTP2_STREAM_WINDOW_UPDATE_RATIO, http2StreamWindowUpdateRatio);
655+
return this;
656+
}
657+
637658
/**
638659
* Sets the
639660
* <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">SETTINGS_MAX_FRAME_SIZE</a>

core/src/main/java/com/linecorp/armeria/client/ClientFactoryOptions.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,17 @@ public final class ClientFactoryOptions
160160
ClientFactoryOption.define("HTTP2_INITIAL_STREAM_WINDOW_SIZE",
161161
Flags.defaultHttp2InitialStreamWindowSize());
162162

163+
/**
164+
* The threshold ratio of the HTTP/2 stream flow-control window at which a
165+
* <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.9">WINDOW_UPDATE</a> frame will be sent.
166+
* When the size of the flow-control window drops below the specified ratio (relative to the initial window
167+
* size), a {@code WINDOW_UPDATE} frame is triggered to replenish the window.
168+
*/
169+
@UnstableApi
170+
public static final ClientFactoryOption<Float> HTTP2_STREAM_WINDOW_UPDATE_RATIO =
171+
ClientFactoryOption.define("HTTP2_STREAM_WINDOW_UPDATE_RATIO",
172+
Flags.defaultHttp2StreamWindowUpdateRatio());
173+
163174
/**
164175
* The <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">SETTINGS_MAX_FRAME_SIZE</a>
165176
* that indicates the size of the largest frame payload that this client is willing to receive.
@@ -504,6 +515,17 @@ public int http2InitialStreamWindowSize() {
504515
return get(HTTP2_INITIAL_STREAM_WINDOW_SIZE);
505516
}
506517

518+
/**
519+
* Returns the threshold ratio of the HTTP/2 stream flow-control window at which a
520+
* <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.9">WINDOW_UPDATE</a> frame will be sent.
521+
* When the size of the flow-control window drops below the specified ratio (relative to the initial window
522+
* size), a {@code WINDOW_UPDATE} frame is triggered to replenish the window.
523+
*/
524+
@UnstableApi
525+
public float http2StreamWindowUpdateRatio() {
526+
return get(HTTP2_STREAM_WINDOW_UPDATE_RATIO);
527+
}
528+
507529
/**
508530
* Returns the <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">SETTINGS_MAX_FRAME_SIZE</a>
509531
* that indicates the size of the largest frame payload that this client is willing to receive.

core/src/main/java/com/linecorp/armeria/client/Http2ClientConnectionHandler.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,10 @@ final class Http2ClientConnectionHandler extends AbstractHttp2ConnectionHandler
4545
super(decoder, encoder, initialSettings,
4646
newKeepAliveHandler(encoder, channel, clientFactory, protocol));
4747

48-
responseDecoder = new Http2ResponseDecoder(channel, encoder(), clientFactory, keepAliveHandler());
48+
responseDecoder = new Http2ResponseDecoder(channel, encoder(), decoder, clientFactory,
49+
keepAliveHandler());
4950
connection().addListener(responseDecoder);
50-
decoder().frameListener(responseDecoder);
51+
decoder.frameListener(responseDecoder);
5152
}
5253

5354
private static KeepAliveHandler newKeepAliveHandler(

core/src/main/java/com/linecorp/armeria/client/Http2ResponseDecoder.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import io.netty.channel.ChannelHandlerContext;
4545
import io.netty.channel.EventLoop;
4646
import io.netty.handler.codec.http2.Http2Connection;
47+
import io.netty.handler.codec.http2.Http2ConnectionDecoder;
4748
import io.netty.handler.codec.http2.Http2ConnectionEncoder;
4849
import io.netty.handler.codec.http2.Http2Error;
4950
import io.netty.handler.codec.http2.Http2Exception;
@@ -63,10 +64,12 @@ final class Http2ResponseDecoder extends AbstractHttpResponseDecoder implements
6364
private final Http2GoAwayHandler goAwayHandler;
6465
private final KeepAliveHandler keepAliveHandler;
6566

66-
Http2ResponseDecoder(Channel channel, Http2ConnectionEncoder encoder, HttpClientFactory clientFactory,
67+
Http2ResponseDecoder(Channel channel, Http2ConnectionEncoder encoder, Http2ConnectionDecoder decoder,
68+
HttpClientFactory clientFactory,
6769
KeepAliveHandler keepAliveHandler) {
6870
super(channel,
69-
InboundTrafficController.ofHttp2(channel, clientFactory.http2InitialConnectionWindowSize()));
71+
InboundTrafficController.ofHttp2(channel, decoder,
72+
clientFactory.http2InitialConnectionWindowSize()));
7073
conn = encoder.connection();
7174
this.encoder = encoder;
7275
assert keepAliveHandler instanceof Http2ClientKeepAliveHandler ||
@@ -284,8 +287,8 @@ public int onDataRead(
284287
res.close();
285288
}
286289

287-
// All bytes have been processed.
288-
return dataLength + padding;
290+
// The data length will be reported to InboundTrafficController upon consumption for flow control.
291+
return padding;
289292
}
290293

291294
/**

core/src/main/java/com/linecorp/armeria/client/HttpClientFactory.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ private static void setupTlsMetrics(List<X509Certificate> certificates, MeterReg
111111
private final AddressResolverGroup<InetSocketAddress> addressResolverGroup;
112112
private final int http2InitialConnectionWindowSize;
113113
private final int http2InitialStreamWindowSize;
114+
private final float http2StreamWindowUpdateRatio;
114115
private final int http2MaxFrameSize;
115116
private final long http2MaxHeaderListSize;
116117
private final int http1MaxInitialLineLength;
@@ -208,6 +209,7 @@ private static void setupTlsMetrics(List<X509Certificate> certificates, MeterReg
208209

209210
http2InitialConnectionWindowSize = options.http2InitialConnectionWindowSize();
210211
http2InitialStreamWindowSize = options.http2InitialStreamWindowSize();
212+
http2StreamWindowUpdateRatio = options.http2StreamWindowUpdateRatio();
211213
http2MaxFrameSize = options.http2MaxFrameSize();
212214
http2MaxHeaderListSize = options.http2MaxHeaderListSize();
213215
pingIntervalMillis = options.pingIntervalMillis();
@@ -265,6 +267,10 @@ int http2InitialStreamWindowSize() {
265267
return http2InitialStreamWindowSize;
266268
}
267269

270+
float http2StreamWindowUpdateRatio() {
271+
return http2StreamWindowUpdateRatio;
272+
}
273+
268274
int http2MaxFrameSize() {
269275
return http2MaxFrameSize;
270276
}

core/src/main/java/com/linecorp/armeria/client/HttpClientPipelineConfigurator.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
import io.netty.handler.codec.http2.DefaultHttp2ConnectionEncoder;
9696
import io.netty.handler.codec.http2.DefaultHttp2FrameReader;
9797
import io.netty.handler.codec.http2.DefaultHttp2FrameWriter;
98+
import io.netty.handler.codec.http2.DefaultHttp2LocalFlowController;
9899
import io.netty.handler.codec.http2.Http2ClientUpgradeCodec;
99100
import io.netty.handler.codec.http2.Http2CodecUtil;
100101
import io.netty.handler.codec.http2.Http2Connection;
@@ -539,7 +540,9 @@ public void channelActive(ChannelHandlerContext ctx) throws Exception {
539540
final Http2ResponseDecoder responseDecoder = this.responseDecoder;
540541
final DecodedHttpResponse res = new DecodedHttpResponse(ctx.channel().eventLoop());
541542

543+
final int id = 0;
542544
res.init(responseDecoder.inboundTrafficController());
545+
res.setId(id);
543546
res.subscribe(new Subscriber<HttpObject>() {
544547

545548
private boolean notified;
@@ -580,7 +583,7 @@ public void onComplete() {}
580583
System.nanoTime(), SystemInfo.currentTimeMicros());
581584

582585
// NB: No need to set the response timeout because we have session creation timeout.
583-
responseDecoder.addResponse(null, 0, res, reqCtx, ctx.channel().eventLoop());
586+
responseDecoder.addResponse(null, id, res, reqCtx, ctx.channel().eventLoop());
584587
ctx.fireChannelActive();
585588
}
586589

@@ -781,6 +784,10 @@ private Http2ConnectionDecoder decoder(Http2Connection connection, Http2Connecti
781784
/* validateHeaders */ false, clientFactory.http2MaxHeaderListSize());
782785
Http2FrameReader reader = new DefaultHttp2FrameReader(headersDecoder);
783786
reader = new Http2InboundFrameLogger(reader, frameLogger);
787+
final DefaultHttp2LocalFlowController flowController =
788+
new DefaultHttp2LocalFlowController(connection, clientFactory.http2StreamWindowUpdateRatio(),
789+
false);
790+
connection.local().flowController(flowController);
784791
return new DefaultHttp2ConnectionDecoder(connection, encoder, reader);
785792
}
786793

core/src/main/java/com/linecorp/armeria/common/DefaultFlagsProvider.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import io.micrometer.core.instrument.MeterRegistry;
3737
import io.micrometer.core.instrument.Metrics;
3838
import io.micrometer.core.instrument.distribution.DistributionStatisticConfig;
39+
import io.netty.handler.codec.http2.DefaultHttp2LocalFlowController;
3940

4041
/**
4142
* Implementation of {@link FlagsProvider} which provides default values to {@link Flags}.
@@ -81,6 +82,8 @@ final class DefaultFlagsProvider implements FlagsProvider {
8182
static final long DEFAULT_CLIENT_HTTP2_GRACEFUL_SHUTDOWN_TIMEOUT_MILLIS = 1000;
8283
static final int DEFAULT_HTTP2_INITIAL_CONNECTION_WINDOW_SIZE = 1024 * 1024; // 1MiB
8384
static final int DEFAULT_HTTP2_INITIAL_STREAM_WINDOW_SIZE = 1024 * 1024; // 1MiB
85+
static final float DEFAULT_HTTP2_STREAM_WINDOW_UPDATE_RATIO =
86+
DefaultHttp2LocalFlowController.DEFAULT_WINDOW_UPDATE_RATIO; // 0.5f
8487
static final int DEFAULT_HTTP2_MAX_FRAME_SIZE = 16384; // From HTTP/2 specification
8588

8689
// Can't use 0xFFFFFFFFL because some implementations use a signed 32-bit integer to store HTTP/2 SETTINGS
@@ -330,6 +333,11 @@ public Integer defaultHttp2InitialStreamWindowSize() {
330333
return DEFAULT_HTTP2_INITIAL_STREAM_WINDOW_SIZE;
331334
}
332335

336+
@Override
337+
public Float defaultHttp2StreamWindowUpdateRatio() {
338+
return DEFAULT_HTTP2_STREAM_WINDOW_UPDATE_RATIO;
339+
}
340+
333341
@Override
334342
public Integer defaultHttp2MaxFrameSize() {
335343
return DEFAULT_HTTP2_MAX_FRAME_SIZE;

core/src/main/java/com/linecorp/armeria/common/Flags.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,10 @@ private static boolean validateTransportType(TransportType transportType, String
297297
getValue(FlagsProvider::defaultHttp2InitialStreamWindowSize,
298298
"defaultHttp2InitialStreamWindowSize", value -> value > 0);
299299

300+
private static final float DEFAULT_HTTP2_STREAM_WINDOW_UPDATE_RATIO =
301+
getValue(FlagsProvider::defaultHttp2StreamWindowUpdateRatio,
302+
"defaultHttp2InitialStreamWindowSize", value -> value > 0 && value <= 1.0f);
303+
300304
private static final int DEFAULT_HTTP2_MAX_FRAME_SIZE =
301305
getValue(FlagsProvider::defaultHttp2MaxFrameSize, "defaultHttp2MaxFrameSize",
302306
value -> value >= Http2CodecUtil.MAX_FRAME_SIZE_LOWER_BOUND &&
@@ -1112,6 +1116,23 @@ public static int defaultHttp2InitialStreamWindowSize() {
11121116
return DEFAULT_HTTP2_INITIAL_STREAM_WINDOW_SIZE;
11131117
}
11141118

1119+
/**
1120+
* Returns the default value of the {@link ServerBuilder#http2StreamWindowUpdateRatio(float)} and
1121+
* {@link ClientFactoryBuilder#http2StreamWindowUpdateRatio(float)}.
1122+
* Note that this flag has no effect if a user specified the value explicitly via
1123+
* {@link ServerBuilder#http2StreamWindowUpdateRatio(float)} or
1124+
* {@link ClientFactoryBuilder#http2StreamWindowUpdateRatio(float)}.
1125+
*
1126+
* <p>The default value of this flag is
1127+
* {@value DefaultFlagsProvider#DEFAULT_HTTP2_STREAM_WINDOW_UPDATE_RATIO}.
1128+
* Specify the {@code -Dcom.linecorp.armeria.defaultHttp2StreamWindowUpdateRatio=<float>} JVM option
1129+
* to override the default value.
1130+
*/
1131+
@UnstableApi
1132+
public static Float defaultHttp2StreamWindowUpdateRatio() {
1133+
return DEFAULT_HTTP2_STREAM_WINDOW_UPDATE_RATIO;
1134+
}
1135+
11151136
/**
11161137
* Returns the default value of the {@link ServerBuilder#http2MaxFrameSize(int)} and
11171138
* {@link ClientFactoryBuilder#http2MaxFrameSize(int)} option.

core/src/main/java/com/linecorp/armeria/common/FlagsProvider.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,23 @@ default Integer defaultHttp2InitialStreamWindowSize() {
702702
return null;
703703
}
704704

705+
/**
706+
* Returns the default value of the {@link ServerBuilder#http2StreamWindowUpdateRatio(float)} and
707+
* {@link ClientFactoryBuilder#http2StreamWindowUpdateRatio(float)}.
708+
* Note that this flag has no effect if a user specified the value explicitly via
709+
* {@link ServerBuilder#http2StreamWindowUpdateRatio(float)} or
710+
* {@link ClientFactoryBuilder#http2StreamWindowUpdateRatio(float)}.
711+
*
712+
* <p>The default value of this flag is
713+
* {@value DefaultFlagsProvider#DEFAULT_HTTP2_STREAM_WINDOW_UPDATE_RATIO}.
714+
* Specify the {@code -Dcom.linecorp.armeria.defaultHttp2StreamWindowUpdateRatio=<float>} JVM option
715+
* to override the default value.
716+
*/
717+
@Nullable
718+
default Float defaultHttp2StreamWindowUpdateRatio() {
719+
return null;
720+
}
721+
705722
/**
706723
* Returns the default value of the {@link ServerBuilder#http2MaxFrameSize(int)} and
707724
* {@link ClientFactoryBuilder#http2MaxFrameSize(int)} option.

0 commit comments

Comments
 (0)