Mark an HTTP/2 connection instead of looking it up - #2320
Open
pavel-ptashyts wants to merge 1 commit into
Open
Conversation
isHttp2 asked the pipeline for the multiplex handler by name, and the write path asks isHttp2 of every request: once to decide whether to store the per-request future on the channel, once to route the write. A pipeline lookup walks the handlers comparing names, and an HTTP/1.1 connection, having no such handler, is walked to the end to say no, which is the common case for anyone not using HTTP/2. In one profile of a client with HTTP/2 disabled, DefaultChannelPipeline.context0 took 83 CPU samples on that account alone. The multiplex handler is installed in exactly one place, so a channel attribute is set beside it and isHttp2 reads that: a binary search over integer keys in a small array rather than a walk with a string compare per handler. hasAttr rather than attr().get(), which would add an entry to the attribute map of every HTTP/1.1 channel just to find none. Not a config check. Reading isHttp2Enabled first would be cheaper still, but the two can disagree: NettyConnectListener upgrades on the ALPN result alone, and a caller who supplies an SslContext or an SslEngineFactory of their own controls what ALPN advertises whatever the config says - which the WebSocket guard beside it already accounts for. A request would then be written as HTTP/1.1 onto an HTTP/2 pipeline. The attribute costs nothing extra and cannot disagree, since it is set where the handler is. The attribute cannot go stale either: nothing removes the multiplex handler from a pipeline, so there is no downgrade for the two to differ across. ChannelManagerHttp2MarkerTest pins them together, so a later change to the upgrade cannot set one without the other. Removing the attribute fails that test and 46 of the 52 in BasicHttp2Test, the write path having routed HTTP/2 connections down the HTTP/1.1 branch. Claude Code on behalf of Pavel Ptashyts Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
ChannelManager.isHttp2(channel)asks the pipeline for the multiplex handler by name:The write path asks it of every request, twice: once in
sendRequestWithOpenChannelto decidewhether to store the per-request future on the channel, once in
writeRequestto route thewrite.
DefaultChannelPipeline.get(String)walks the handler chain comparing names, and anHTTP/1.1 connection - which has no such handler - is walked to the end to answer no. That is the
common case for anyone who has not turned HTTP/2 on.
In a profile of a client running with
setHttp2Enabled(false),DefaultChannelPipeline.context0accounted for 83 CPU samples on this alone.Change
The multiplex handler is installed in exactly one place,
upgradePipelineToHttp2, so a channelattribute is set beside it and
isHttp2reads that instead:A binary search over integer keys in a small array, rather than a walk with a string compare per
handler.
hasAttrrather thanattr(...).get(): the latter would add an entry to the attributemap of every HTTP/1.1 channel just to find nothing in it.
Behaviour is unchanged in every configuration, including the ones a config check would get wrong
Why not check the config instead
Reading
config.isHttp2Enabled()first would be cheaper still, and it was the first thing tried.It is not safe: the two can disagree.
NettyConnectListenerupgrades the pipeline on the ALPN result alone, not on the config:And ALPN can select
h2with the flag off.DefaultSslEngineFactoryadvertisesh2only whenisHttp2Enabled(), but it leaves a caller-suppliedSslContextalone(
config.getSslContext() != null || !config.isHttp2Enabled()), and a caller-suppliedSslEngineFactoryis free to advertise whatever it likes - which the WebSocket guard beside theupgrade already accounts for in as many words: "this guard is the backstop for a custom
SslEngineFactory that still advertises h2".
upgradePipelineToHttp2AfterProxyConnectis gated onALPN the same way.
With
http2Enabled(false)and such a context, a config check would route a genuine HTTP/2connection down the HTTP/1.1 branch and write an HTTP/1.1 request onto an HTTP/2 pipeline. The
attribute costs nothing more than the config read would have saved, and cannot disagree with the
handler, being set where the handler is.
If HTTP/2 negotiated against
http2Enabled(false)is considered unsupported, a configshort-circuit could be layered on top of this - but that is a behaviour decision rather than a
micro-optimisation, so it is not made here.
Can the attribute go stale
No. Nothing removes
HTTP2_MULTIPLEXfrom a pipeline - the only reference to it besides thelookup is the
addLastin the upgrade - so there is no downgrade for the two to diverge across.Stream child channels carry neither the handler nor the attribute, so
isHttp2answers no forthem exactly as it did before.
Tests
ChannelManagerHttp2MarkerTestpins the attribute to the handler: either both say HTTP/2 orneither does, so a later change to the upgrade cannot set one and forget the other. Three cases -
a connection never upgraded, one upgraded, and a stream channel.
Checked by mutation rather than assumption: dropping the attribute assignment fails that test and
46 of the 52 in
BasicHttp2Test, the write path having routed HTTP/2 connections down theHTTP/1.1 branch.
Verification
mvnw clean verify- BUILD SUCCESS, 1488 tests, 0 failures, 0 errors, 26 skipped. Error Prone,NullAway and Revapi all clean, with no revapi entries:
isHttp2keeps its signature and theattribute key is private.
Caveat on the testing gate:
AGENTS.mdrequires the build to run on JDK 11 and no JDK 11 isinstalled on this machine, so it was run on JDK 17 (also in the CI matrix). The JDK 11 legs of
CI on this PR are the real gate.
Claude Code on behalf of @pavel-ptashyts
🤖 Generated with Claude Code