New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix too many flush #13244
base: 4.1
Are you sure you want to change the base?
fix too many flush #13244
Conversation
A unit test would be nice |
In the current version, @Test
public void closeStreamWithoutFlush() {
Http2StreamChannel childChannel3 = newInboundStream(3, true, new LastInboundHandler());
Http2StreamChannel childChannel5 = newInboundStream(5, true, new LastInboundHandler());
FlushSniffer flushSniffer = new FlushSniffer();
parentChannel.pipeline().addFirst(flushSniffer);
//1. write data and batch flush
byte[] dataBuf = "test".getBytes();
childChannel5.write(new DefaultHttp2DataFrame(Unpooled.wrappedBuffer(dataBuf), false));
childChannel3.write(new DefaultHttp2DataFrame(Unpooled.wrappedBuffer(dataBuf), false));
childChannel3.flush();
assertTrue(flushSniffer.checkFlush());
//2. write end header frame
Http2Headers headers = new DefaultHttp2Headers();
headers.set("test", "test");
childChannel3.write(new DefaultHttp2HeadersFrame(headers, true));
childChannel5.write(new DefaultHttp2HeadersFrame(headers, true));
assertFalse(flushSniffer.checkFlush()); // fails. write end frame will auto 'flush'
//batch flush
childChannel5.flush();
assertTrue(flushSniffer.checkFlush());
} |
@icodening hmm I am not convinced this change is "safe". If we not flush we could miss to update the connection related flow control. |
@icodening I did make some small changes which I think are more correct and safe. |
@bryce-anderson you might want to check this as well |
@@ -838,6 +842,9 @@ private void updateLocalWindowIfNeeded() { | |||
int bytes = flowControlledBytes; | |||
flowControlledBytes = 0; | |||
ChannelFuture future = write0(parentContext(), new DefaultHttp2WindowUpdateFrame(bytes).stream(stream)); | |||
|
|||
windowUpdateFrameWritten = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
io.netty.handler.codec.http2.Http2FrameCodec#consumeBytes
may return false
, but windowUpdateFrameWritten
is always true, this will cause unnecessary flush
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There might be some more improvements we can make but I would say we can do another PR for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
General question not related to this PR: if we write window_update frame to the parentContext()
can we rely on the parent channel to manage flushes instead of triggering flush from the stream channel?
@icodening one question... Why are you concerned by flushes at all ? If there is nothing to write it should be relative cheap. |
I want to The following code is from while (!cancelled && (frame = peek()) != null) {
int maxBytes = min(allocated, writableWindow());
if (maxBytes <= 0 && frame.size() > 0) {
// The frame still has data, but the amount of allocated bytes has been exhausted.
// Don't write needless empty frames.
break;
}
writeOccurred = true;
int initialFrameSize = frame.size();
try {
frame.write(ctx, max(0, maxBytes));
if (frame.size() == 0) {
// This frame has been fully written, remove this frame and notify it.
// Since we remove this frame first, we're guaranteed that its error
// method will not be called when we call cancel.
pendingWriteQueue.remove();
frame.writeComplete();
}
} finally {
// Decrement allocated by how much was actually written.
allocated -= initialFrameSize - frame.size();
}
} |
@@ -806,7 +808,9 @@ void doBeginRead() { | |||
} | |||
// We need to double check that there is nothing left to flush such as a | |||
// window update frame. | |||
flush(); | |||
if (windowUpdateFrameWritten) { | |||
flush(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if stream state is CLOSE
, it should not need to be flush
, right?
if (!Http2Stream.State.CLOSED.equals(stream.state())) {
flush();
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well I think it might still need to do to ensure connection window updates are flushed ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
correct, spec allows writing window_update frame after endStream=true
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@normanmaurer for ST this change won't make any difference because we manage flushes externally. But I can imagine that there is a risk to break users who rely on the current behavior (flush before closing the stream). Maybe it's worth adding an opt-in property to control this change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this checking of stream state is sufficient. The flow controller does at least profess to free any remaining bytes on stream closure and thus ignores any more stream flow window updates here.
@@ -838,6 +842,9 @@ private void updateLocalWindowIfNeeded() { | |||
int bytes = flowControlledBytes; | |||
flowControlledBytes = 0; | |||
ChannelFuture future = write0(parentContext(), new DefaultHttp2WindowUpdateFrame(bytes).stream(stream)); | |||
|
|||
windowUpdateFrameWritten = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
General question not related to this PR: if we write window_update frame to the parentContext()
can we rely on the parent channel to manage flushes instead of triggering flush from the stream channel?
@@ -806,7 +808,9 @@ void doBeginRead() { | |||
} | |||
// We need to double check that there is nothing left to flush such as a | |||
// window update frame. | |||
flush(); | |||
if (windowUpdateFrameWritten) { | |||
flush(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
correct, spec allows writing window_update frame after endStream=true
//do not auto flush, it will be 'false' | ||
assertFalse(flushSniffer.checkFlush()); | ||
//batch flush | ||
childChannel5.flush(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To clarify: the flush automatically invoked right now only if the stream is closed, meaning that both inbound and outbound side signaled endStream=true
. If the inbound side is still open, you won't see the flush.
From StreamChannel
point of view, it's not active anymore if the stream is closed. There will be channelInactive()
event sent as part of processing write(endStream=true)
(line 1388), and the closeFuture()
will be notified. There is nothing that users can write after that point.
@normanmaurer it sounds like it's incorrect to invoke childChannel5.flush()
here, because childChannel5.isActive()
returns false
at this point. However, there is no other easy way for users of StreamChannel
API to flush after response is written except using writeAndFlush
, which complicates usability. In ST, we also invoke flush()
after the last write(...)
because we have a separate logic to control flushes.
How does it work for the main connection Channel
(let's say HTTP/1.1)? If someone writes and closes the channel, I would expect Netty to flush everything before closing. Seems reasonable for StreamChannel
to behave consistently with real Channel
, at least by default.
Also, multiplexed streams run in parallel if there is > 1 in-flight requests. It complicates how "batch flush" can be implemented if writes from multiple streams intersect with each other.
I want to batch flush, when I call ctx.flush once, it will actually call flush multiple times.
@icodening taking the above description into account, can you clarify the exact flow of how you plan to "batch flush" on a closed stream?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Btw, have you considered adding FlushConsolidationHandler
between Http2ConnectionHandler
and Http2MultiplexHandler
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Btw, have you considered adding
FlushConsolidationHandler
betweenHttp2ConnectionHandler
andHttp2MultiplexHandler
?
I tried, this way can reduce flush
. thks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@icodening taking the above description into account, can you clarify the exact flow of how you plan to "batch flush" on a closed stream?
I will write the message to the queue
and schedule the eventloop to execute.
ctx.write
will be called when the number of messages is less than the threshold, and ctx.flush
will be called when the number of messages is more than the threshold.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@normanmaurer it sounds like it's incorrect to invoke childChannel5.flush() here, because childChannel5.isActive() returns false at this point. However, there is no other easy way for users of StreamChannel API to flush after response is written except using writeAndFlush, which complicates usability. In ST, we also invoke flush() after the last write(...) because we have a separate logic to control flushes.
It seems more reasonable to use the parent channel
flush
@@ -806,7 +808,9 @@ void doBeginRead() { | |||
} | |||
// We need to double check that there is nothing left to flush such as a | |||
// window update frame. | |||
flush(); | |||
if (windowUpdateFrameWritten) { | |||
flush(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@normanmaurer for ST this change won't make any difference because we manage flushes externally. But I can imagine that there is a risk to break users who rely on the current behavior (flush before closing the stream). Maybe it's worth adding an opt-in property to control this change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The modifications look safe to me, but could use a few more tests.
It's been a while since I've looked at this code, but it's not clear to me that it's necessary to flush the flow control frames, otherwise flow control for data buffered when a stream is closed would appear to be broken. Specifically, we only write an explicit flow control frame via updateLocalWindowIfNeeded()
, which is only called at the beginning of the HttpChannelUnsafe.beginRead()
. However, when a stream is closed it attempts to drain its inbound buffer but does so by going directly to Unsafe.doBeginRead()
, skipping the generation of flow control messages for anything that is still in the inboundBuffer
. Since those may be data frames and it appears we wouldn't send flow control for them on a stream close they must be accounted for elsewhere, which looks like in the sessions flow controller when the stream is removed here, which includes a flush.
That said, it's been a while so don't be surprised if this isn't an accurate assessment of things. 😅
Http2StreamChannel childChannel3 = newInboundStream(3, true, new LastInboundHandler()); | ||
Http2StreamChannel childChannel5 = newInboundStream(5, true, new LastInboundHandler()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be good to ensure that we do flush any window update frames. Can we add a third stream that tests that, or if you prefer, another test?
@@ -1037,6 +1044,7 @@ public void flush() { | |||
// We need to set this to false before we call flush0(...) as ChannelFutureListener may produce more data | |||
// that are explicit flushed. | |||
writeDoneAndNoFlush = false; | |||
windowUpdateFrameWritten = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are strong parallels with the writeDoneAndNoFlush
field. It would be good to align the names and make clear the distinction, which is only that writeDoneAndNoFlush
includes all frame types (including window updates) whereas windowUpdateFrameWritten
is only for window update frames.
@icodening Anyupdate on this? |
No updates yet |
Motivation:
fix too many flush.
when I call
ctx.write
to sendend frame
, it will be auto flush.Modification:
check
StreamChannel
state beforeflush
.When stream is closed, it may not need auto
flush
Result:
reduce the number of flush
Fixes #13232