Skip to content

Commit 3caade4

Browse files
authored
When metrics are enabled and responseTimeout is configured, ensure the correct order for ChannelHandlers (#3090)
- Place ResponseTimeoutHandler before HttpMetricsHandler so that is an error happened it can be recorded - Add test to verify that errors with establishing a connection are recorded Fixes #3060
1 parent 7f47b89 commit 3caade4

File tree

3 files changed

+58
-4
lines changed

3 files changed

+58
-4
lines changed

reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientConfig.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -574,8 +574,20 @@ static void addStreamHandlers(
574574
}
575575

576576
if (responseTimeoutMillis > -1) {
577-
Connection.from(ch).addHandlerFirst(NettyPipeline.ResponseTimeoutHandler,
578-
new ReadTimeoutHandler(responseTimeoutMillis, TimeUnit.MILLISECONDS));
577+
Connection conn = Connection.from(ch);
578+
if (ch.pipeline().get(NettyPipeline.HttpMetricsHandler) != null) {
579+
if (ch.pipeline().get(NettyPipeline.ResponseTimeoutHandler) == null) {
580+
ch.pipeline().addBefore(NettyPipeline.HttpMetricsHandler, NettyPipeline.ResponseTimeoutHandler,
581+
new ReadTimeoutHandler(responseTimeoutMillis, TimeUnit.MILLISECONDS));
582+
if (conn.isPersistent()) {
583+
conn.onTerminate().subscribe(null, null, () -> conn.removeHandler(NettyPipeline.ResponseTimeoutHandler));
584+
}
585+
}
586+
}
587+
else {
588+
conn.addHandlerFirst(NettyPipeline.ResponseTimeoutHandler,
589+
new ReadTimeoutHandler(responseTimeoutMillis, TimeUnit.MILLISECONDS));
590+
}
579591
}
580592

581593
if (log.isDebugEnabled()) {

reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientOperations.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -639,8 +639,19 @@ else if (markSentBody()) {
639639
}
640640
listener().onStateChange(this, HttpClientState.REQUEST_SENT);
641641
if (responseTimeout != null) {
642-
addHandlerFirst(NettyPipeline.ResponseTimeoutHandler,
643-
new ReadTimeoutHandler(responseTimeout.toMillis(), TimeUnit.MILLISECONDS));
642+
if (channel().pipeline().get(NettyPipeline.HttpMetricsHandler) != null) {
643+
if (channel().pipeline().get(NettyPipeline.ResponseTimeoutHandler) == null) {
644+
channel().pipeline().addBefore(NettyPipeline.HttpMetricsHandler, NettyPipeline.ResponseTimeoutHandler,
645+
new ReadTimeoutHandler(responseTimeout.toMillis(), TimeUnit.MILLISECONDS));
646+
if (isPersistent()) {
647+
onTerminate().subscribe(null, null, () -> removeHandler(NettyPipeline.ResponseTimeoutHandler));
648+
}
649+
}
650+
}
651+
else {
652+
addHandlerFirst(NettyPipeline.ResponseTimeoutHandler,
653+
new ReadTimeoutHandler(responseTimeout.toMillis(), TimeUnit.MILLISECONDS));
654+
}
644655
}
645656
channel().read();
646657
if (channel().parent() != null) {

reactor-netty-http/src/test/java/reactor/netty/http/HttpMetricsHandlerTests.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import io.netty.channel.ChannelDuplexHandler;
2424
import io.netty.channel.ChannelHandlerContext;
2525
import io.netty.channel.ChannelInboundHandlerAdapter;
26+
import io.netty.channel.ChannelOption;
2627
import io.netty.channel.ChannelOutboundHandlerAdapter;
2728
import io.netty.channel.ChannelPipeline;
2829
import io.netty.channel.ChannelPromise;
@@ -98,6 +99,7 @@
9899
import static reactor.netty.Metrics.DATA_RECEIVED_TIME;
99100
import static reactor.netty.Metrics.DATA_SENT;
100101
import static reactor.netty.Metrics.DATA_SENT_TIME;
102+
import static reactor.netty.Metrics.ERROR;
101103
import static reactor.netty.Metrics.ERRORS;
102104
import static reactor.netty.Metrics.HTTP_CLIENT_PREFIX;
103105
import static reactor.netty.Metrics.HTTP_SERVER_PREFIX;
@@ -730,12 +732,16 @@ void testServerConnectionsMicrometerConnectionClose(HttpProtocol[] serverProtoco
730732
assertThat(ServerCloseHandler.INSTANCE.awaitClientClosedOnServer()).as("awaitClientClosedOnServer timeout").isTrue();
731733
assertGauge(registry, SERVER_CONNECTIONS_TOTAL, URI, HTTP, LOCAL_ADDRESS, address).hasValueEqualTo(0);
732734
assertGauge(registry, SERVER_CONNECTIONS_ACTIVE, URI, HTTP, LOCAL_ADDRESS, address).hasValueEqualTo(0);
735+
// https://github.com/reactor/reactor-netty/issues/3060
736+
assertCounter(registry, CLIENT_ERRORS, REMOTE_ADDRESS, address, URI, "/6").hasCountGreaterThanOrEqualTo(1);
733737
}
734738
else {
735739
// make sure the client stream is closed on the server side before checking server metrics
736740
assertThat(StreamCloseHandler.INSTANCE.awaitClientClosedOnServer()).as("awaitClientClosedOnServer timeout").isTrue();
737741
assertGauge(registry, SERVER_CONNECTIONS_TOTAL, URI, HTTP, LOCAL_ADDRESS, address).hasValueEqualTo(1);
738742
assertGauge(registry, SERVER_STREAMS_ACTIVE, URI, HTTP, LOCAL_ADDRESS, address).hasValueEqualTo(0);
743+
// https://github.com/reactor/reactor-netty/issues/3060
744+
assertCounter(registry, CLIENT_ERRORS, REMOTE_ADDRESS, address, URI, "/6").hasCountGreaterThanOrEqualTo(1);
739745
// in case of H2, the tearDown method will ensure client socket is closed on the server side
740746
}
741747
}
@@ -828,13 +834,17 @@ void testServerConnectionsRecorderConnectionClose(HttpProtocol[] serverProtocols
828834
assertThat(ServerRecorder.INSTANCE.onActiveConnectionsAmount.get()).isEqualTo(0);
829835
assertThat(ServerRecorder.INSTANCE.onActiveConnectionsLocalAddr.get()).isEqualTo(address);
830836
assertThat(ServerRecorder.INSTANCE.onInactiveConnectionsLocalAddr.get()).isEqualTo(address);
837+
// https://github.com/reactor/reactor-netty/issues/3060
838+
assertCounter(registry, CLIENT_ERRORS, REMOTE_ADDRESS, address, URI, "/7").hasCountGreaterThanOrEqualTo(1);
831839
}
832840
else {
833841
assertThat(StreamCloseHandler.INSTANCE.awaitClientClosedOnServer()).as("awaitClientClosedOnServer timeout").isTrue();
834842
assertThat(ServerRecorder.INSTANCE.onServerConnectionsAmount.get()).isEqualTo(1);
835843
assertThat(ServerRecorder.INSTANCE.onActiveConnectionsAmount.get()).isEqualTo(0);
836844
assertThat(ServerRecorder.INSTANCE.onActiveConnectionsLocalAddr.get()).isEqualTo(address);
837845
assertThat(ServerRecorder.INSTANCE.onInactiveConnectionsLocalAddr.get()).isEqualTo(address);
846+
// https://github.com/reactor/reactor-netty/issues/3060
847+
assertCounter(registry, CLIENT_ERRORS, REMOTE_ADDRESS, address, URI, "/7").hasCountGreaterThanOrEqualTo(1);
838848
// in case of H2, the tearDown method will ensure client socket is closed on the server side
839849
}
840850
}
@@ -967,6 +977,27 @@ void testIssue2956(boolean isCustomRecorder, boolean isHttp2) throws Exception {
967977
}
968978
}
969979

980+
@ParameterizedTest
981+
@MethodSource("httpCompatibleProtocols")
982+
void testIssue3060ConnectTimeoutException(HttpProtocol[] serverProtocols, HttpProtocol[] clientProtocols,
983+
@Nullable ProtocolSslContextSpec serverCtx, @Nullable ProtocolSslContextSpec clientCtx) throws Exception {
984+
CountDownLatch latch = new CountDownLatch(1);
985+
customizeClientOptions(httpClient, clientCtx, clientProtocols)
986+
.remoteAddress(() -> new InetSocketAddress("1.1.1.1", 11111))
987+
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10)
988+
.doOnChannelInit((o, c, address) -> c.closeFuture().addListener(f -> latch.countDown()))
989+
.post()
990+
.uri("/1")
991+
.send(ByteBufFlux.fromString(Mono.just("hello")))
992+
.responseContent()
993+
.subscribe();
994+
995+
assertThat(latch.await(30, TimeUnit.SECONDS)).as("latch await").isTrue();
996+
997+
String[] summaryTags = new String[]{REMOTE_ADDRESS, "1.1.1.1:11111", STATUS, ERROR};
998+
assertTimer(registry, CLIENT_CONNECT_TIME, summaryTags).hasCountEqualTo(1);
999+
}
1000+
9701001
static Stream<Arguments> combinationsIssue2956() {
9711002
return Stream.of(
9721003
// isCustomRecorder, isHttp2

0 commit comments

Comments
 (0)