You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
We’ve been investigating an intermittent deadlock where one HTTP/2 stream starves the others under Armeria’s client. It appears that Armeria’s InboundTrafficController suspends inbound channel once its tracking value exceeds the connection-level high-water mark even it's consumed by one DecodedHttpResponse, and does not respect the per-stream window limits.
Server endpoint is serving with writing ~1MB data upon request arriving
Two GETs on the same connection issued by Armeria WebClient:
One is issued slightly earlier than the other
The HttpResponse from the client is adopted with res.toInputStream(Identity) to transform into InputStream, but the application layer doesn't read from the InputStream explicitly, e.g. the test doesn't read from the responses explicitly.
Observation
The first stream is able to receive 1MB data even at the conditions of
the application layer never reads from the InputStream explicitly and
the stream level window is 1KB
while the second stream couldn't make progress at all.
What we have confirmed
Netty H2 flow control doesn't kick in
Both H2 connection & per-stream windows remain open, shown in heap dumps as non-zero credit.
Armeria’s InboundTrafficController is suspended
it hits the high water mark.
What we think happens
When first request is sent from armeria client and handled by the server, the server keeps writing data to the stream to 1MB; during that time stream 2 hasn't arrived yet.
H2 level flow control doesn’t really take effect for first stream, as Armeria buffers the HttpData in the DecodedHttpResponse, and tells Netty and it has consumes all the bytes
The release of H2 flow control window is via onDataRead callback of Http2ResponseDecoder that implements Http2FrameListener.
InboundTrafficController operates at a connection level, so an unconsumed stream can block the channel and prevent another stream from reading. This form of backpressure may violate the HTTP/2 flow control specification.
A flow-control scheme ensures that streams on the same connection do not destructively interfere with each other.
It doesn't seem like fixing the problem requires significant effort. I already shared the direction of the change with the team and received their agreement.
I will open a PR within the next few working days.
Activity
trustin commentedon May 26, 2025
Thanks for the detailed report! I'm curious if the problem goes away if you remove the capping logic here.
Also, do you have a reproducer we can play with?
yzfeng2020 commentedon May 27, 2025
@trustin I pushed #6258 as a similar repro, ptal, thanks!
ikhoon commentedon May 28, 2025
InboundTrafficControlleroperates at a connection level, so an unconsumed stream can block the channel and prevent another stream from reading. This form of backpressure may violate the HTTP/2 flow control specification.To make flow control work properly in HTTP/2, I'm going to modify the code to use
Http2LocalFlowController.consumeBytes(Http2Stream,int)yzfeng2020 commentedon May 29, 2025
@ikhoon thanks, may I know what's the effort level for this? I am happy to help if the team doesn't have capacity for this issue.
ikhoon commentedon May 29, 2025
It doesn't seem like fixing the problem requires significant effort. I already shared the direction of the change with the team and received their agreement.
I will open a PR within the next few working days.
Ensure HTTP/2 flow control to work at the stream level
Ensure HTTP/2 flow control to work at the stream level
Ensure HTTP/2 flow control to work at the stream level
Ensure HTTP/2 flow control to work at the stream level (line#6266)