Skip to content

Commit becade0

Browse files
committed
add per-stream idle timeout and suppress CancelledKeyException on cancel
; Handle cancelled SelectionKey in interestOps access
1 parent 26993ee commit becade0

6 files changed

Lines changed: 241 additions & 461 deletions

File tree

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

Lines changed: 6 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -26,44 +26,29 @@
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

3333
/**
34-
* {@link java.net.SocketTimeoutException} raised by the HTTP/2 stream
35-
* multiplexer when a per-stream timeout elapses.
34+
* {@link java.net.SocketTimeoutException} raised when an HTTP/2 stream exceeds its configured timeout.
3635
* <p>
37-
* This exception is used for timeouts that are scoped to a single HTTP/2
38-
* stream rather than the underlying TCP connection, for example:
39-
* </p>
40-
* <ul>
41-
* <li>an idle timeout where no activity has been observed on the stream, or</li>
42-
* <li>a lifetime timeout where the total age of the stream exceeds
43-
* the configured limit.</li>
44-
* </ul>
45-
* <p>
46-
* The {@link #isIdleTimeout()} flag can be used to distinguish whether
47-
* the timeout was triggered by idleness or by the overall stream lifetime.
48-
* The affected stream id and the timeout value are exposed via
49-
* {@link #getStreamId()} and {@link #getTimeout()} respectively.
36+
* This timeout is scoped to a single stream and is independent of the underlying connection socket timeout.
5037
* </p>
5138
*
52-
* @since 5.4
39+
* @since 5.5
5340
*/
54-
public class H2StreamTimeoutException extends SocketTimeoutException {
41+
public class H2StreamTimeoutException extends InterruptedIOException {
5542

5643
private static final long serialVersionUID = 1L;
5744

5845
private final int streamId;
5946
private final Timeout timeout;
60-
private final boolean idleTimeout;
6147

62-
public H2StreamTimeoutException(final String message, final int streamId, final Timeout timeout, final boolean idleTimeout) {
48+
public H2StreamTimeoutException(final String message, final int streamId, final Timeout timeout) {
6349
super(message);
6450
this.streamId = streamId;
6551
this.timeout = timeout;
66-
this.idleTimeout = idleTimeout;
6752
}
6853

6954
public int getStreamId() {
@@ -74,14 +59,4 @@ public Timeout getTimeout() {
7459
return timeout;
7560
}
7661

77-
/**
78-
* Indicates whether this timeout was triggered by idle time (no activity)
79-
* rather than by stream lifetime.
80-
*
81-
* @return {@code true} if this is an idle timeout, {@code false} if it is a lifetime timeout.
82-
*/
83-
public boolean isIdleTimeout() {
84-
return idleTimeout;
85-
}
86-
8762
}

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

Lines changed: 23 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,11 @@ enum SettingsHandshake { READY, TRANSMITTED, ACKED }
144144
private final Map<Integer, PriorityValue> priorities = new ConcurrentHashMap<>();
145145
private volatile boolean peerNoRfc7540Priorities;
146146

147+
148+
private static final long STREAM_TIMEOUT_GRANULARITY_MILLIS = 1000;
149+
private long lastStreamTimeoutCheckMillis;
150+
151+
147152
AbstractH2StreamMultiplexer(
148153
final ProtocolIOSession ioSession,
149154
final FrameFactory frameFactory,
@@ -439,10 +444,6 @@ public final void onInput(final ByteBuffer src) throws HttpException, IOExceptio
439444
for (;;) {
440445
final RawFrame frame = inputBuffer.read(src, ioSession);
441446
if (frame != null) {
442-
if (connState.compareTo(ConnectionHandshake.SHUTDOWN) < 0) {
443-
checkStreamTimeouts(System.nanoTime());
444-
}
445-
446447
if (streamListener != null) {
447448
streamListener.onFrameInput(this, frame.getStreamId(), frame);
448449
}
@@ -460,7 +461,7 @@ public final void onInput(final ByteBuffer src) throws HttpException, IOExceptio
460461
}
461462
}
462463
if (connState.compareTo(ConnectionHandshake.SHUTDOWN) < 0) {
463-
checkStreamTimeouts(System.nanoTime());
464+
validateStreamTimeouts();
464465
}
465466
}
466467
}
@@ -541,7 +542,7 @@ public final void onOutput() throws HttpException, IOException {
541542
}
542543

543544
if (connState.compareTo(ConnectionHandshake.SHUTDOWN) < 0) {
544-
checkStreamTimeouts(System.nanoTime());
545+
validateStreamTimeouts();
545546
}
546547

547548
if (connState.compareTo(ConnectionHandshake.GRACEFUL_SHUTDOWN) == 0) {
@@ -655,7 +656,6 @@ private void executeRequest(final RequestExecutionCommand requestExecutionComman
655656
requestExecutionCommand.getExchangeHandler(),
656657
requestExecutionCommand.getPushHandlerFactory(),
657658
requestExecutionCommand.getContext()));
658-
initializeStreamTimeouts(stream);
659659

660660
if (streamListener != null) {
661661
final int initInputWindow = stream.getInputWindow().get();
@@ -774,12 +774,10 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio
774774
final H2StreamChannel channel = createChannel(streamId);
775775
if (connState.compareTo(ConnectionHandshake.ACTIVE) <= 0) {
776776
stream = streams.createActive(channel, incomingRequest(channel));
777-
initializeStreamTimeouts(stream);
778777
streams.resetIfExceedsMaxConcurrentLimit(stream, localConfig.getMaxConcurrentStreams());
779778
} else {
780779
channel.localReset(H2Error.REFUSED_STREAM);
781780
stream = streams.createActive(channel, NoopH2StreamHandler.INSTANCE);
782-
initializeStreamTimeouts(stream);
783781
}
784782
} else if (stream.isLocalClosed() && stream.isRemoteClosed()) {
785783
throw new H2ConnectionException(H2Error.STREAM_CLOSED, "Stream closed");
@@ -970,7 +968,6 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio
970968
channel.localReset(H2Error.REFUSED_STREAM);
971969
promisedStream = streams.createActive(channel, NoopH2StreamHandler.INSTANCE);
972970
}
973-
initializeStreamTimeouts(promisedStream);
974971
try {
975972
consumePushPromiseFrame(frame, payload, promisedStream);
976973
} catch (final H2StreamResetException ex) {
@@ -1376,16 +1373,8 @@ H2StreamChannel createChannel(final int streamId) {
13761373
return new H2StreamChannelImpl(streamId, initInputWinSize, initOutputWinSize);
13771374
}
13781375

1379-
private void initializeStreamTimeouts(final H2Stream stream) {
1380-
final Timeout socketTimeout = ioSession.getSocketTimeout();
1381-
if (socketTimeout != null && socketTimeout.isEnabled()) {
1382-
stream.setIdleTimeout(socketTimeout);
1383-
}
1384-
}
1385-
13861376
H2Stream createStream(final H2StreamChannel channel, final H2StreamHandler streamHandler) {
13871377
final H2Stream stream = streams.createActive(channel, streamHandler);
1388-
initializeStreamTimeouts(stream);
13891378
return stream;
13901379
}
13911380

@@ -1489,7 +1478,6 @@ public void push(final List<Header> headers, final AsyncPushProducer pushProduce
14891478
final int promisedStreamId = streams.generateStreamId();
14901479
final H2StreamChannel channel = createChannel(promisedStreamId);
14911480
final H2Stream stream = streams.createReserved(channel, outgoingPushPromise(channel, pushProducer));
1492-
initializeStreamTimeouts(stream);
14931481

14941482
commitPushPromise(id, promisedStreamId, headers);
14951483
stream.markRemoteClosed();
@@ -1614,42 +1602,28 @@ private void checkStreamTimeouts(final long nowNanos) throws IOException {
16141602
}
16151603

16161604
final Timeout idleTimeout = stream.getIdleTimeout();
1617-
final Timeout lifetimeTimeout = stream.getLifetimeTimeout();
1618-
if ((idleTimeout == null || !idleTimeout.isEnabled())
1619-
&& (lifetimeTimeout == null || !lifetimeTimeout.isEnabled())) {
1605+
if (idleTimeout == null || !idleTimeout.isEnabled()) {
16201606
continue;
16211607
}
16221608

1623-
final long created = stream.getCreatedNanos();
16241609
final long last = stream.getLastActivityNanos();
1625-
1626-
if (idleTimeout != null && idleTimeout.isEnabled()) {
1627-
final long idleNanos = idleTimeout.toNanoseconds();
1628-
if (idleNanos > 0 && nowNanos - last > idleNanos) {
1629-
final int streamId = stream.getId();
1630-
final H2StreamTimeoutException ex = new H2StreamTimeoutException(
1631-
"HTTP/2 stream idle timeout (" + idleTimeout + ")",
1632-
streamId,
1633-
idleTimeout,
1634-
true);
1635-
stream.localReset(ex, H2Error.CANCEL);
1636-
// Once reset due to idle timeout, we do not care about lifetime anymore
1637-
continue;
1638-
}
1610+
final long idleNanos = idleTimeout.toNanoseconds();
1611+
if (idleNanos > 0 && nowNanos - last > idleNanos) {
1612+
final int streamId = stream.getId();
1613+
final H2StreamTimeoutException ex = new H2StreamTimeoutException(
1614+
"HTTP/2 stream idle timeout (" + idleTimeout + ")",
1615+
streamId,
1616+
idleTimeout);
1617+
stream.localReset(ex, H2Error.CANCEL);
16391618
}
1619+
}
1620+
}
16401621

1641-
if (lifetimeTimeout != null && lifetimeTimeout.isEnabled()) {
1642-
final long lifeNanos = lifetimeTimeout.toNanoseconds();
1643-
if (lifeNanos > 0 && nowNanos - created > lifeNanos) {
1644-
final int streamId = stream.getId();
1645-
final H2StreamTimeoutException ex = new H2StreamTimeoutException(
1646-
"HTTP/2 stream lifetime timeout (" + lifetimeTimeout + ")",
1647-
streamId,
1648-
lifetimeTimeout,
1649-
false);
1650-
stream.localReset(ex, H2Error.CANCEL);
1651-
}
1652-
}
1622+
private void validateStreamTimeouts() throws IOException {
1623+
final long nowMillis = System.currentTimeMillis();
1624+
if ((nowMillis - lastStreamTimeoutCheckMillis) >= STREAM_TIMEOUT_GRANULARITY_MILLIS) {
1625+
lastStreamTimeoutCheckMillis = nowMillis;
1626+
checkStreamTimeouts(System.nanoTime());
16531627
}
16541628
}
16551629

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

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,9 @@ class H2Stream implements StreamControl {
6161
private volatile boolean reserved;
6262
private volatile boolean remoteClosed;
6363

64-
private volatile long createdNanos;
6564
private volatile long lastActivityNanos;
6665

6766
private volatile Timeout idleTimeout;
68-
private volatile Timeout lifetimeTimeout;
6967

7068
H2Stream(final H2StreamChannel channel, final H2StreamHandler handler, final Consumer<State> stateChangeCallback) {
7169
this.channel = channel;
@@ -75,7 +73,6 @@ class H2Stream implements StreamControl {
7573
this.transitionRef = new AtomicReference<>(State.RESERVED);
7674
this.released = new AtomicBoolean();
7775
this.cancelled = new AtomicBoolean();
78-
this.createdNanos = 0L;
7976
this.lastActivityNanos = 0L;
8077
}
8178

@@ -91,7 +88,7 @@ public State getState() {
9188

9289
@Override
9390
public void setTimeout(final Timeout timeout) {
94-
// not supported
91+
this.idleTimeout = timeout;
9592
}
9693

9794
boolean isReserved() {
@@ -112,7 +109,7 @@ private void triggerClosed() {
112109

113110
void activate() {
114111
reserved = false;
115-
markCreatedAndActive();
112+
touch();
116113
triggerOpen();
117114
}
118115

@@ -219,16 +216,13 @@ boolean isOutputReady() {
219216

220217
void produceOutput() throws HttpException, IOException {
221218
try {
222-
touch();
223-
224219
handler.produceOutput();
225220
} catch (final ProtocolException ex) {
226221
localReset(ex, H2Error.PROTOCOL_ERROR);
227222
}
228223
}
229224

230225
void produceInputCapacityUpdate() throws IOException {
231-
touch();
232226
handler.updateInputCapacity();
233227
}
234228

@@ -326,20 +320,10 @@ public String toString() {
326320
return buf.toString();
327321
}
328322

329-
private void markCreatedAndActive() {
330-
final long now = System.nanoTime();
331-
this.createdNanos = now;
332-
this.lastActivityNanos = now;
333-
}
334-
335323
private void touch() {
336324
this.lastActivityNanos = System.nanoTime();
337325
}
338326

339-
long getCreatedNanos() {
340-
return createdNanos;
341-
}
342-
343327
long getLastActivityNanos() {
344328
return lastActivityNanos;
345329
}
@@ -348,15 +332,4 @@ Timeout getIdleTimeout() {
348332
return idleTimeout;
349333
}
350334

351-
void setIdleTimeout(final Timeout idleTimeout) {
352-
this.idleTimeout = idleTimeout;
353-
}
354-
355-
Timeout getLifetimeTimeout() {
356-
return lifetimeTimeout;
357-
}
358-
359-
void setLifetimeTimeout(final Timeout lifetimeTimeout) {
360-
this.lifetimeTimeout = lifetimeTimeout;
361-
}
362335
}

0 commit comments

Comments
 (0)