Skip to content

Commit 084cab8

Browse files
committed
add per-stream idle timeout and suppress CancelledKeyException on cancel
1 parent ccf73f8 commit 084cab8

File tree

4 files changed

+26
-98
lines changed

4 files changed

+26
-98
lines changed

httpcore5-h2/src/main/java/org/apache/hc/core5/http2/H2StreamTimeoutException.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
*/
2727
package org.apache.hc.core5.http2;
2828

29-
import java.net.SocketTimeoutException;
29+
import java.io.InterruptedIOException;
3030

3131
import org.apache.hc.core5.util.Timeout;
3232

@@ -51,7 +51,7 @@
5151
*
5252
* @since 5.4
5353
*/
54-
public class H2StreamTimeoutException extends SocketTimeoutException {
54+
public class H2StreamTimeoutException extends InterruptedIOException {
5555

5656
private static final long serialVersionUID = 1L;
5757

httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/AbstractH2StreamMultiplexer.java

Lines changed: 13 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
import java.net.SocketAddress;
3131
import java.nio.BufferOverflowException;
3232
import java.nio.ByteBuffer;
33-
import java.nio.channels.CancelledKeyException;
3433
import java.nio.channels.SelectionKey;
3534
import java.nio.charset.StandardCharsets;
3635
import java.util.Deque;
@@ -272,12 +271,7 @@ private void commitFrameInternal(final RawFrame frame) throws IOException {
272271
} else {
273272
outputQueue.addLast(frame);
274273
}
275-
try {
276-
ioSession.setEvent(SelectionKey.OP_WRITE);
277-
} catch (final CancelledKeyException ex) {
278-
connState = ConnectionHandshake.SHUTDOWN;
279-
ioSession.close(CloseMode.IMMEDIATE);
280-
}
274+
ioSession.setEvent(SelectionKey.OP_WRITE);
281275
}
282276

283277
private void commitFrame(final RawFrame frame) throws IOException {
@@ -419,12 +413,7 @@ private void incrementInputCapacity(
419413

420414
void requestSessionOutput() {
421415
outputRequests.incrementAndGet();
422-
try {
423-
ioSession.setEvent(SelectionKey.OP_WRITE);
424-
} catch (final CancelledKeyException ex) {
425-
connState = ConnectionHandshake.SHUTDOWN;
426-
ioSession.close(CloseMode.IMMEDIATE);
427-
}
416+
ioSession.setEvent(SelectionKey.OP_WRITE);
428417
}
429418

430419
public final void onConnect() throws HttpException, IOException {
@@ -662,7 +651,6 @@ private void executeRequest(final RequestExecutionCommand requestExecutionComman
662651
requestExecutionCommand.getExchangeHandler(),
663652
requestExecutionCommand.getPushHandlerFactory(),
664653
requestExecutionCommand.getContext()));
665-
initializeStreamTimeouts(stream);
666654

667655
if (streamListener != null) {
668656
final int initInputWindow = stream.getInputWindow().get();
@@ -671,6 +659,7 @@ private void executeRequest(final RequestExecutionCommand requestExecutionComman
671659
streamListener.onOutputFlowControl(this, streamId, initOutputWindow, initOutputWindow);
672660
}
673661
requestExecutionCommand.initiated(stream);
662+
initializeStreamTimeouts(stream);
674663
if (stream.isOutputReady()) {
675664
stream.produceOutput();
676665
}
@@ -1624,42 +1613,21 @@ private void checkStreamTimeouts(final long nowNanos) throws IOException {
16241613
}
16251614

16261615
final Timeout idleTimeout = stream.getIdleTimeout();
1627-
final Timeout lifetimeTimeout = stream.getLifetimeTimeout();
1628-
if ((idleTimeout == null || !idleTimeout.isEnabled())
1629-
&& (lifetimeTimeout == null || !lifetimeTimeout.isEnabled())) {
1616+
if (idleTimeout == null || !idleTimeout.isEnabled()) {
16301617
continue;
16311618
}
16321619

1633-
final long created = stream.getCreatedNanos();
16341620
final long last = stream.getLastActivityNanos();
1635-
1636-
if (idleTimeout != null && idleTimeout.isEnabled()) {
1637-
final long idleNanos = idleTimeout.toNanoseconds();
1638-
if (idleNanos > 0 && nowNanos - last > idleNanos) {
1639-
final int streamId = stream.getId();
1640-
final H2StreamTimeoutException ex = new H2StreamTimeoutException(
1641-
"HTTP/2 stream idle timeout (" + idleTimeout + ")",
1642-
streamId,
1643-
idleTimeout,
1644-
true);
1645-
stream.localReset(ex, H2Error.CANCEL);
1646-
continue;
1647-
}
1648-
}
1649-
1650-
if (lifetimeTimeout != null && lifetimeTimeout.isEnabled()) {
1651-
final long lifeNanos = lifetimeTimeout.toNanoseconds();
1652-
if (lifeNanos > 0 && nowNanos - created > lifeNanos) {
1653-
final int streamId = stream.getId();
1654-
final H2StreamTimeoutException ex = new H2StreamTimeoutException(
1655-
"HTTP/2 stream lifetime timeout (" + lifetimeTimeout + ")",
1656-
streamId,
1657-
lifetimeTimeout,
1658-
false);
1659-
stream.localReset(ex, H2Error.CANCEL);
1660-
}
1621+
final long idleNanos = idleTimeout.toNanoseconds();
1622+
if (idleNanos > 0 && nowNanos - last > idleNanos) {
1623+
final int streamId = stream.getId();
1624+
final H2StreamTimeoutException ex = new H2StreamTimeoutException(
1625+
"HTTP/2 stream idle timeout (" + idleTimeout + ")",
1626+
streamId,
1627+
idleTimeout,
1628+
true);
1629+
stream.localReset(ex, H2Error.CANCEL);
16611630
}
16621631
}
16631632
}
1664-
16651633
}

httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/bootstrap/CancellableExecution.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
*/
2727
package org.apache.hc.core5.http2.impl.nio.bootstrap;
2828

29+
import java.nio.channels.CancelledKeyException;
2930
import java.util.concurrent.atomic.AtomicBoolean;
3031
import java.util.concurrent.atomic.AtomicReference;
3132

@@ -48,7 +49,11 @@ public void setDependency(final Cancellable cancellable) {
4849
if (cancelled.get()) {
4950
final Cancellable dependency = dependencyRef.getAndSet(null);
5051
if (dependency != null) {
51-
dependency.cancel();
52+
try {
53+
dependency.cancel();
54+
} catch (final CancelledKeyException ignore) {
55+
// Session already gone; cancellation is effectively complete.
56+
}
5257
}
5358
}
5459
}
@@ -63,7 +68,11 @@ public boolean cancel() {
6368
if (cancelled.compareAndSet(false, true)) {
6469
final Cancellable dependency = dependencyRef.getAndSet(null);
6570
if (dependency != null) {
66-
dependency.cancel();
71+
try {
72+
dependency.cancel();
73+
} catch (final CancelledKeyException ignore) {
74+
// Session already gone; treat as successfully cancelled.
75+
}
6776
}
6877
return true;
6978
}

httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestAbstractH2StreamMultiplexer.java

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1080,54 +1080,5 @@ void testStreamIdleTimeoutTriggersH2StreamTimeoutException() throws Exception {
10801080
Assertions.assertTrue(stream.isLocalClosed());
10811081
Assertions.assertTrue(stream.isClosed());
10821082
}
1083-
1084-
@Test
1085-
void testStreamLifetimeTimeoutTriggersH2StreamTimeoutException() throws Exception {
1086-
Mockito.when(protocolIOSession.getSocketTimeout()).thenReturn(Timeout.DISABLED);
1087-
1088-
Mockito.when(protocolIOSession.write(ArgumentMatchers.any(ByteBuffer.class)))
1089-
.thenAnswer(invocation -> {
1090-
final ByteBuffer buffer = invocation.getArgument(0, ByteBuffer.class);
1091-
final int remaining = buffer.remaining();
1092-
buffer.position(buffer.limit());
1093-
return remaining;
1094-
});
1095-
Mockito.doNothing().when(protocolIOSession).setEvent(ArgumentMatchers.anyInt());
1096-
Mockito.doNothing().when(protocolIOSession).clearEvent(ArgumentMatchers.anyInt());
1097-
1098-
final H2Config h2Config = H2Config.custom().build();
1099-
final AbstractH2StreamMultiplexer streamMultiplexer = new H2StreamMultiplexerImpl(
1100-
protocolIOSession,
1101-
FRAME_FACTORY,
1102-
StreamIdGenerator.ODD,
1103-
httpProcessor,
1104-
CharCodingConfig.DEFAULT,
1105-
h2Config,
1106-
h2StreamListener,
1107-
() -> streamHandler);
1108-
1109-
final H2StreamChannel channel = streamMultiplexer.createChannel(3);
1110-
final H2Stream stream = streamMultiplexer.createStream(channel, streamHandler);
1111-
stream.activate();
1112-
1113-
stream.setIdleTimeout(null);
1114-
stream.setLifetimeTimeout(Timeout.of(1, TimeUnit.NANOSECONDS));
1115-
1116-
streamMultiplexer.onOutput();
1117-
1118-
Mockito.verify(streamHandler).failed(exceptionCaptor.capture());
1119-
final Exception cause = exceptionCaptor.getValue();
1120-
Assertions.assertInstanceOf(H2StreamTimeoutException.class, cause);
1121-
1122-
final H2StreamTimeoutException timeoutEx = (H2StreamTimeoutException) cause;
1123-
Assertions.assertFalse(timeoutEx.isIdleTimeout(), "Expected lifetime timeout flag");
1124-
Assertions.assertEquals(3, timeoutEx.getStreamId(), "Unexpected stream id");
1125-
1126-
Assertions.assertTrue(stream.isLocalClosed());
1127-
Assertions.assertTrue(stream.isClosed());
1128-
}
1129-
1130-
1131-
11321083
}
11331084

0 commit comments

Comments
 (0)