Skip to content

Commit 0839a20

Browse files
authored
Fixes #11259 - HTTP/2 connection not closed after idle timeout when TCP congested. (#11267)
Now upon the second idle timeout, the connection is forcibly closed. Fixed also similar problem in HTTP/3. Signed-off-by: Simone Bordet <[email protected]>
1 parent 090287d commit 0839a20

File tree

9 files changed

+385
-66
lines changed

9 files changed

+385
-66
lines changed

jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Session.java

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1909,6 +1909,7 @@ private boolean onIdleTimeout()
19091909
{
19101910
String reason = "idle_timeout";
19111911
boolean notify = false;
1912+
boolean terminate = false;
19121913
boolean sendGoAway = false;
19131914
GoAwayFrame goAwayFrame = null;
19141915
Throwable cause = null;
@@ -1923,10 +1924,9 @@ private boolean onIdleTimeout()
19231924
return false;
19241925
notify = true;
19251926
}
1926-
1927-
// Timed out while waiting for closing events, fail all the streams.
19281927
case LOCALLY_CLOSED ->
19291928
{
1929+
// Timed out while waiting for closing events, fail all the streams.
19301930
if (goAwaySent.isGraceful())
19311931
{
19321932
goAwaySent = newGoAwayFrame(ErrorCode.NO_ERROR.code, reason);
@@ -1935,7 +1935,7 @@ private boolean onIdleTimeout()
19351935
goAwayFrame = goAwaySent;
19361936
closed = CloseState.CLOSING;
19371937
zeroStreamsAction = null;
1938-
failure = cause = new TimeoutException("Session idle timeout expired");
1938+
failure = cause = newTimeoutException();
19391939
}
19401940
case REMOTELY_CLOSED ->
19411941
{
@@ -1944,17 +1944,21 @@ private boolean onIdleTimeout()
19441944
goAwayFrame = goAwaySent;
19451945
closed = CloseState.CLOSING;
19461946
zeroStreamsAction = null;
1947-
failure = cause = new TimeoutException("Session idle timeout expired");
1948-
}
1949-
default ->
1950-
{
1951-
if (LOG.isDebugEnabled())
1952-
LOG.debug("Already closed, ignored idle timeout for {}", HTTP2Session.this);
1953-
return false;
1947+
failure = cause = newTimeoutException();
19541948
}
1949+
default -> terminate = true;
19551950
}
19561951
}
19571952

1953+
if (terminate)
1954+
{
1955+
if (LOG.isDebugEnabled())
1956+
LOG.debug("Already closed, ignored idle timeout for {}", HTTP2Session.this);
1957+
// Writes may be TCP congested, so termination never happened.
1958+
flusher.abort(newTimeoutException());
1959+
return false;
1960+
}
1961+
19581962
if (notify)
19591963
{
19601964
boolean confirmed = notifyIdleTimeout(HTTP2Session.this);
@@ -1973,6 +1977,11 @@ private boolean onIdleTimeout()
19731977
return false;
19741978
}
19751979

1980+
private TimeoutException newTimeoutException()
1981+
{
1982+
return new TimeoutException("Session idle timeout expired");
1983+
}
1984+
19761985
private void onSessionFailure(int error, String reason, Callback callback)
19771986
{
19781987
GoAwayFrame goAwayFrame;
@@ -2036,7 +2045,7 @@ private void onWriteFailure(Throwable x)
20362045

20372046
private void sendGoAwayAndTerminate(GoAwayFrame frame, GoAwayFrame eventFrame)
20382047
{
2039-
sendGoAway(frame, Callback.from(Callback.NOOP, () -> terminate(eventFrame)));
2048+
sendGoAway(frame, Callback.from(() -> terminate(eventFrame)));
20402049
}
20412050

20422051
private void sendGoAway(GoAwayFrame frame, Callback callback)

jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/IdleTimeoutTest.java

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@
1313

1414
package org.eclipse.jetty.http2.tests;
1515

16+
import java.net.InetSocketAddress;
1617
import java.nio.ByteBuffer;
18+
import java.nio.channels.SelectionKey;
19+
import java.nio.channels.SocketChannel;
20+
import java.time.Duration;
1721
import java.util.concurrent.CountDownLatch;
1822
import java.util.concurrent.TimeUnit;
1923
import java.util.concurrent.TimeoutException;
@@ -33,8 +37,11 @@
3337
import org.eclipse.jetty.http2.frames.GoAwayFrame;
3438
import org.eclipse.jetty.http2.frames.HeadersFrame;
3539
import org.eclipse.jetty.http2.frames.ResetFrame;
40+
import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
3641
import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
3742
import org.eclipse.jetty.io.Content;
43+
import org.eclipse.jetty.io.ManagedSelector;
44+
import org.eclipse.jetty.io.SocketChannelEndPoint;
3845
import org.eclipse.jetty.server.Handler;
3946
import org.eclipse.jetty.server.HttpConfiguration;
4047
import org.eclipse.jetty.server.Request;
@@ -50,6 +57,7 @@
5057

5158
import static org.awaitility.Awaitility.await;
5259
import static org.hamcrest.MatcherAssert.assertThat;
60+
import static org.hamcrest.Matchers.is;
5361
import static org.junit.jupiter.api.Assertions.assertEquals;
5462
import static org.junit.jupiter.api.Assertions.assertFalse;
5563
import static org.junit.jupiter.api.Assertions.assertNull;
@@ -747,10 +755,10 @@ public void onReset(Stream stream, ResetFrame frame, Callback callback)
747755
await().atMost(5, TimeUnit.SECONDS).until(() -> ((HTTP2Session)client).updateSendWindow(0), Matchers.greaterThan(0));
748756

749757
// Wait for the server to finish serving requests.
750-
await().atMost(5, TimeUnit.SECONDS).until(handled::get, Matchers.is(0));
751-
assertThat(requests.get(), Matchers.is(count - 1));
758+
await().atMost(5, TimeUnit.SECONDS).until(handled::get, is(0));
759+
assertThat(requests.get(), is(count - 1));
752760

753-
await().atMost(5, TimeUnit.SECONDS).until(responses::get, Matchers.is(count - 1));
761+
await().atMost(5, TimeUnit.SECONDS).until(responses::get, is(count - 1));
754762
}
755763

756764
@Test
@@ -837,6 +845,53 @@ public void onHeaders(Stream stream, HeadersFrame frame)
837845
assertTrue(responseLatch.await(5, TimeUnit.SECONDS));
838846
}
839847

848+
@Test
849+
public void testIdleTimeoutWhenCongested() throws Exception
850+
{
851+
long idleTimeout = 1000;
852+
HTTP2CServerConnectionFactory h2c = new HTTP2CServerConnectionFactory(new HttpConfiguration());
853+
prepareServer(h2c);
854+
server.removeConnector(connector);
855+
connector = new ServerConnector(server, 1, 1, h2c)
856+
{
857+
@Override
858+
protected SocketChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key)
859+
{
860+
SocketChannelEndPoint endpoint = new SocketChannelEndPoint(channel, selectSet, key, getScheduler())
861+
{
862+
@Override
863+
public boolean flush(ByteBuffer... buffers)
864+
{
865+
// Fake TCP congestion.
866+
return false;
867+
}
868+
869+
@Override
870+
protected void onIncompleteFlush()
871+
{
872+
// Do nothing here to avoid spin loop,
873+
// since the network is actually writable,
874+
// as we are only faking TCP congestion.
875+
}
876+
};
877+
endpoint.setIdleTimeout(getIdleTimeout());
878+
return endpoint;
879+
}
880+
};
881+
connector.setIdleTimeout(idleTimeout);
882+
server.addConnector(connector);
883+
server.start();
884+
885+
prepareClient();
886+
httpClient.start();
887+
888+
InetSocketAddress address = new InetSocketAddress("localhost", connector.getLocalPort());
889+
// The connect() will complete exceptionally.
890+
http2Client.connect(address, new Session.Listener() {});
891+
892+
await().atMost(Duration.ofMillis(5 * idleTimeout)).until(() -> connector.getConnectedEndPoints().size(), is(0));
893+
}
894+
840895
private void sleep(long value)
841896
{
842897
try

jetty-core/jetty-http3/jetty-http3-common/src/main/java/org/eclipse/jetty/http3/HTTP3Session.java

Lines changed: 23 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,7 @@ private CompletableFuture<Void> goAway(GoAwayFrame frame)
218218
long error = HTTP3ErrorCode.REQUEST_CANCELLED_ERROR.code();
219219
String reason = "go_away";
220220
failStreams(stream -> true, error, reason, true, new ClosedChannelException());
221-
terminate();
222-
outwardDisconnect(error, reason);
221+
terminateAndDisconnect(error, reason);
223222
}
224223
return CompletableFuture.completedFuture(null);
225224
}
@@ -489,18 +488,12 @@ public void onGoAway(GoAwayFrame frame)
489488
goAwaySent = newGoAwayFrame(false);
490489
GoAwayFrame goAwayFrame = goAwaySent;
491490
zeroStreamsAction = () -> writeControlFrame(goAwayFrame, Callback.from(() ->
492-
{
493-
terminate();
494-
outwardDisconnect(HTTP3ErrorCode.NO_ERROR.code(), "go_away");
495-
}));
491+
terminateAndDisconnect(HTTP3ErrorCode.NO_ERROR.code(), "go_away")
492+
));
496493
}
497494
else
498495
{
499-
zeroStreamsAction = () ->
500-
{
501-
terminate();
502-
outwardDisconnect(HTTP3ErrorCode.NO_ERROR.code(), "go_away");
503-
};
496+
zeroStreamsAction = () -> terminateAndDisconnect(HTTP3ErrorCode.NO_ERROR.code(), "go_away");
504497
failStreams = true;
505498
}
506499
}
@@ -561,34 +554,24 @@ public void onGoAway(GoAwayFrame frame)
561554
public boolean onIdleTimeout()
562555
{
563556
boolean notify = false;
557+
boolean terminate = false;
564558
try (AutoLock ignored = lock.lock())
565559
{
566560
switch (closeState)
567561
{
568-
case NOT_CLOSED:
569-
{
570-
notify = true;
571-
break;
572-
}
573-
case LOCALLY_CLOSED:
574-
case REMOTELY_CLOSED:
575-
{
576-
break;
577-
}
578-
case CLOSING:
579-
case CLOSED:
580-
{
581-
if (LOG.isDebugEnabled())
582-
LOG.debug("already closed, ignored idle timeout for {}", this);
583-
return false;
584-
}
585-
default:
586-
{
587-
throw new IllegalStateException();
588-
}
562+
case NOT_CLOSED -> notify = true;
563+
case CLOSING, CLOSED -> terminate = true;
589564
}
590565
}
591566

567+
if (terminate)
568+
{
569+
if (LOG.isDebugEnabled())
570+
LOG.debug("already closed, ignored idle timeout for {}", this);
571+
terminateAndDisconnect(HTTP3ErrorCode.NO_ERROR.code(), "idle_timeout");
572+
return false;
573+
}
574+
592575
boolean confirmed = true;
593576
if (notify)
594577
confirmed = notifyIdleTimeout();
@@ -645,18 +628,15 @@ public void inwardClose(long error, String reason)
645628
failStreams(stream -> true, error, reason, true, new IOException(reason));
646629

647630
if (goAwayFrame != null)
648-
{
649-
writeControlFrame(goAwayFrame, Callback.from(() ->
650-
{
651-
terminate();
652-
outwardDisconnect(error, reason);
653-
}));
654-
}
631+
writeControlFrame(goAwayFrame, Callback.from(() -> terminateAndDisconnect(error, reason)));
655632
else
656-
{
657-
terminate();
658-
outwardDisconnect(error, reason);
659-
}
633+
terminateAndDisconnect(error, reason);
634+
}
635+
636+
private void terminateAndDisconnect(long error, String reason)
637+
{
638+
terminate();
639+
outwardDisconnect(error, reason);
660640
}
661641

662642
/**

0 commit comments

Comments
 (0)