Fix HTTP/2 cancellation during stream creation#9447
Conversation
|
The failing It failed in I checked the exact CI merge ref locally:
Could someone rerun the failed |
| close(ErrorCode.NO_ERROR, ErrorCode.CANCEL, null) | ||
| } | ||
|
|
||
| internal fun cancel() { |
There was a problem hiding this comment.
This method feels awkward given we have other close methods, it's really socket.cancel, but it feels like codec and connection are too coupled if we need this hammer from codec to connection.
| if (stream != null) { | ||
| stream.closeLater(ErrorCode.CANCEL) | ||
| } else if (streamCreationInFlight) { | ||
| http2Connection.cancel() |
There was a problem hiding this comment.
Rather than this new http2Connection cancel, can we use Carrier?
carrier.noNewExchanges()
carrier.cancel()
Summary
Fix HTTP/2 call cancellation when a call is blocked creating a new stream and flushing request headers.
When
Http2ExchangeCodec.writeRequestHeaders()callsHttp2Connection.newStream(), the new stream is not assigned to the codec untilnewStream()returns. If the underlying HTTP/2 writer blocks during that operation,Http2ExchangeCodec.cancel()currently only setscanceled = true; there is no stream reference yet, so it cannot sendRST_STREAMor interrupt the blocked writer.This means an overall
callTimeoutcan fire,RealCall.cancel()can reach the codec, but the thread doing the HTTP/2 write can remain blocked in socket I/O.This patch tracks when stream creation is in flight. If cancellation happens in that window before a stream is available to close, the codec cancels the underlying HTTP/2 connection socket to interrupt the blocked writer. Once a stream exists, cancellation continues to use the narrower stream-level
closeLater(CANCEL)behavior.Fixes #8014
Verification
Added
Http2ConnectionTest.cancelHttp2ExchangeInterruptsBlockedStreamCreation, which uses a fakeBufferedSocketwhose sink blocks onflush().The regression exercises the existing
Http2ExchangeCodec.cancel()path:codec.cancel()does not unblockwriteRequestHeaders(), and the test fails because the writer thread does not complete within the timeout.codec.cancel()interrupts the blocked stream creation and the test completes with anIOException.Commands run:
Both pass with the fix applied.
Debugging Notes
Static analysis showed the cancellation gap in the
Http2ExchangeCodec.writeRequestHeaders()->Http2Connection.newStream()path: the stream can exist insideHttp2Connection, while the codec still hasstream == nulluntil header write/flush returns.We also verified the repro/fix behavior using the local JDWP debugging workflow with
debug-bridge/java-debug-mcp:https://github.com/ittaigolde/java-debug-mcp