Skip to content
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

http: CodecClient handle TCP end_stream #2743

Closed
wants to merge 6 commits into from

Conversation

lizan
Copy link
Member

@lizan lizan commented Mar 6, 2018

Signed-off-by: Lizan Zhou zlizan@google.com

Description:
Http::CodecClient should know if the connection is remote closed before onEvent.

Risk Level: Low

Testing:
unit test, integration tests

Docs Changes:
N/A

Release Notes:
N/A

Fixes #2715

Signed-off-by: Lizan Zhou <zlizan@google.com>
@@ -96,7 +96,8 @@ void CodecClient::onReset(ActiveRequest& request, StreamResetReason reason) {
deleteRequest(request);
}

void CodecClient::onData(Buffer::Instance& data) {
void CodecClient::onData(Buffer::Instance& data, bool end_stream) {
remote_closed_ |= end_stream;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely the correct fix, but AFAIK we don't use half close semantics for any HTTP code, so I don't think this will actually solve your problem in practice. @ggreenway?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, Network::ReadFilter::onData() should never pass end_stream==true on the http code. If you're seeing that, there's a bug somewhere, probably in ConnectionImpl.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do some test with real env that reproduced #2715, though I believe this at least make this if statement true, so it won't assign next pending request in HTTP1 conn pool.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/envoyproxy/envoy/blob/master/source/common/network/connection_impl.cc#L435

We could consider I guess setting end_stream to true in this case, even for not half-closed connections. That might allow you to win the race in more cases, but I think that is a more involved change.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can ConnectionImpl::onRead() result in a new request being sent out on the upstream connection (if we read the end of a response)? If not, then it shouldn't matter; the RemoteClose is raised right after onRead().

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ggreenway that's exactly why we're seeing 503s in #2715 (the code is here). Let me take more look on how I can fix this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did this exact case ever work better? Given that onData and onEvent were always separate calls, there was a choice of either calling onEvent(RemoteClose) first, which would mean an error on the response (because we didn't process all the response data yet), or calling onData() first and getting this error.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For HTTP/1.1, I don't think this ever worked any better, because upstream is supposed to set Connection: close as I keep saying into the void. For HTTP/1 this used to work OK because of https://github.com/envoyproxy/envoy/pull/2743/files#diff-181fe1d44460531b055c9b316ca2cd69R64.

I don't have any objection per say to attempting to handle this better by potentially setting end_stream = true in this case, but again as I've said a ton of times, this is not going to work out in your favor 100% of the time due to certain local/remote interleavings.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the late response (was sick last week).

I ended up by making onData set end_stream = true even half-close is not enabled. This new parameter is not used widely yet so it should be safe. Letting a TCP read filter know if the byte is the last one (before connection close) is useful even it doesn't handle half-close semantics.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whether it's a regression fix or just a fix is somewhat irrelevant no ? envoy should handle "unexpectedly" closed connection without attributing the error to the next request?

result.action_ = PostIoAction::Close;
}

read_end_stream_ |= result.end_stream_read_;
if (result.bytes_processed_ != 0 || result.end_stream_read_) {
if (result.bytes_processed_ != 0 || (enable_half_close_ && result.end_stream_read_)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If end_stream is delivered without any data, and you want this to be delivered, I think you want to revert this line. Otherwise end_stream==true will only be set if it arrives with some data (or enable_half_close_ is true).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

end_stream==true will only be set if it arrives with some data (or enable_half_close_ is true).

This is intentional, since we have converted half-close to full-close above, raising onData(0-length buffer, true) for non-half-close read filter is not needed since it will be destroyed right after onReadReady.

Copy link
Contributor

@ggreenway ggreenway left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add some test coverage to connection_impl for the new end_stream behavior.

@@ -81,8 +81,7 @@ class ReadFilter {
/**
* Called when data is read on the connection.
* @param data supplies the read data which may be modified.
* @param end_stream supplies whether this is the last byte on the connection. This will only
* be set if the connection has half-close semantics enabled.
* @param end_stream supplies whether this is the last byte on the connection.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make this comment a bit more clear? end_stream may not be set on the last byte in the connection, if the FIN arrives after the last byte on a split packet.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Signed-off-by: Lizan Zhou <zlizan@google.com>
@@ -81,8 +81,9 @@ class ReadFilter {
/**
* Called when data is read on the connection.
* @param data supplies the read data which may be modified.
* @param end_stream supplies whether this is the last byte on the connection. This will only
* be set if the connection has half-close semantics enabled.
* @param end_stream supplies whether this is the last byte on the connection. It is possible that
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dislike the interface that's being created here. It's confusing to reason about, because we're saying that end_stream may or may not be delivered, depending on how the bytes are broken into packets.

I think we should guarantee delivery of end_stream, so that it's simple to reason about the interface.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, but if we don't do this I really have no idea how we are going to "fix" the issue because of we supply an empty onData with end_stream as true, the original issue will still repro.

I still really think we should just not "fix" this at all...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I should make a separate PR for onRead interface change since the discussion is separate.

@ggreenway not only depending on how the bytes are broken into packets, connection may be closed for several reasons. We cannot guarantee delivery of end_stream, instead, Envoy makes it best effort to set end_stream when onData is called, even if half-close is not enabled. I can revert source/common/network/connection_impl.cc line 439 but it still does not guarantee anything.

@mattklein123 I partly agree that for the original issue the upstream is responsible. While Envoy can make the best effort to let read filter know when it read end_stream (ReadFilter change), and HTTP conn pool can use that, no?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lizan I think we should always deliver end_stream if we can, ie if the connection is shutdown cleanly.

I think we revert line 439, and in the http handling, if you get onData(empty, true) you can ignore it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mattklein123 I partly agree that for the original issue the upstream is responsible. While Envoy can make the best effort to let read filter know when it read end_stream (ReadFilter change), and HTTP conn pool can use that, no?

Yes, but when we are spending this much time talking about this change, and the change is becoming increasingly risky, it seems not worth it to me.

@lizan I think we should always deliver end_stream if we can, ie if the connection is shutdown cleanly. I think we revert line 439, and in the http handling, if you get onData(empty, true) you can ignore it.

Yes I think this should work. It will "fix" the problem when we get FIN inline with the final data, but still always deliver an end_stream with zero data before closing the connection in the half close case. Though I really do worry that there are going to be other filters that won't correctly handle this.

One other option would be to add some type of function to connection such as sawRemoteClose() (similar to what we have on codec client), and allow codecClient to call this. Then we can keep the half close / end_stream logic as it currently is.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lizan I think we should always deliver end_stream if we can, ie if the connection is shutdown cleanly. I think we revert line 439, and in the http handling, if you get onData(empty, true) you can ignore it.

Please see #2841, OTOH I think it might be better not deliver onData(0-length data, true) even half-close is enabled, and let filters rely on onEvent(RemoteClose) for that case. Since they are same and deliver onData doesn't let us win the race since no data is there.

One other option would be to add some type of function to connection such as sawRemoteClose() (similar to what we have on codec client), and allow codecClient to call this. Then we can keep the half close / end_stream logic as it currently is.

I thought this once though I don't want to add another place in addition to onData and onEvent handle remote close. It should be simpler.

@mattklein123
Copy link
Member

@lizan I'm going to close this for now while we sort out the primary API changes. Once that lands, let's reopen and revisit this change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

HTTP1 conneciton pool attach pending request to half-closed connection
5 participants