-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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: delaying attach pending requests #2871
Conversation
Signed-off-by: Lizan Zhou <zlizan@google.com>
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.
Thanks a ton for iterating on this into a solution that everyone is happy with. Overall looks good, just a few comments. Thank you.
|
||
if (!pending_requests_.empty() && !upstream_ready_posted_) { | ||
upstream_ready_posted_ = true; | ||
dispatcher_.post([this]() { onUpstreamReady(); }); |
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.
Instead of using post()
, I would allocate a new "timer" and just enable it with a 0ms timeout here. The reason for this is that when the conn pool is destroyed (as might happen in certain cases), the wakeup is disabled. This pattern is used in a bunch of places for similar reasons.
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.
Done
} else { | ||
void ConnPoolImpl::onUpstreamReady() { | ||
upstream_ready_posted_ = false; | ||
while (!pending_requests_.empty() && !ready_clients_.empty()) { |
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 believe that because of https://github.com/lizan/envoy/blob/11426feab2df2feb5b0cceb0e2b9876840a65c5a/source/common/http/http1/conn_pool.cc#L163, there is no chance of starvation here. I.e., if you lose a ready client we will create new clients if we need them. Please make sure to add a test case for this exact case (if you didn't already).
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.
Do you mean pending_requests_
should be always smaller than ready_clients_
? I think the line only make sure the the sum of ready_clients_
and busy_clients_
is larger than pending_requests_
so we should check both of them here, no?
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 mean that when connections die, we should make a new connection if the number of existing ready + busy connections is not sufficient. I think the code already does that. I just want to make sure we have test coverage of this case. Does the integration test you have cover it? (Basically I think the code you have is correct. I just want to make sure we have good coverage).
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.
OK, the new integration test I added is testing that behavior (and what would fail before this PR because of #2715).
Signed-off-by: Lizan Zhou <zlizan@google.com>
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.
LGTM, thanks. One test comment.
mock_dispatcher_(dispatcher), mock_upstream_ready_timer_(upstream_ready_timer) { | ||
ON_CALL(*mock_upstream_ready_timer_, enableTimer(std::chrono::milliseconds(0))) | ||
.WillByDefault(Invoke( | ||
[&](const std::chrono::milliseconds&) { mock_upstream_ready_timer_->callback_(); })); |
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 fact that this gets invoked inline is not an accurate view of how the real code will work. I would not add this ON_CALL, and explicitly fire the ready even when needed outside of the immediate call stack. I realize this is tedious, but given how important it is that this code is correct and how we would like it to track the real code as much as possible, I think it's worth it.
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.
Done.
Going to unassign myself as I'll be unavailable next week. @alyssawilk can assign another non-senior reviewwe on Monday as necessary. |
@mattklein123 is assigned and he can cover both senior and cross-company. Matt, I think you're most familiar with this code anyway, but let me know if you want a second pair of eyes on this one and I can take a look as well! |
Signed-off-by: Lizan Zhou <zlizan@google.com>
@lizan can you check tests? |
Signed-off-by: Lizan Zhou <zlizan@google.com>
Yep, I'm trying to reproduce them locally. |
Hmm, I cannot reproduce them outside CircleCI, (on my macOS, GCE with docker or without docker)... Will continue to investigate, does anyone have idea? |
@lizan I'm guessing the test failures are spurious. Can you just push a dummy commit? |
It seems consistent in CircleCI, I've pushed many merge commits which triggers CI run. |
@lizan there must be some timing issue here. I haven't looked but given that ASAN/TSAN failed, maybe that's a clue? Or try running in a loop on your machine? |
Yeah that's my suspect too. I had runs_per_test=100 with no luck, will try more... |
The latest ASAN/TSAN failure is JVM OOO |
ed84a4e
to
76d7827
Compare
oops, enable CircleCI in my fork resulted the CI result from there (with less resource, hence OOM) being reported here... Will try to add some dummy commit to debug the CI, feel free to ignore the notifications. |
@lizan friendly ping. What's the status of this one? |
I'm still trying to fix the test but had no luck yet. It never reproduced on my local machine and only once on Istio circle CI, it seems only fails when 1st time running in a machine or something weird. I'll get back to this later this week. |
Signed-off-by: Lizan Zhou <zlizan@google.com>
cdfa577
to
49e831e
Compare
@mattklein123 I changed the logic a bit (not using timer for onConnected event) and seems it fixed the tests and it also perform better. PTAL again. Thanks! |
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.
LGTM, thanks for the follow up here. Would like @alyssawilk to reconfirm that this works for her.
source/common/http/http1/conn_pool.h
Outdated
@@ -134,6 +134,9 @@ class ConnPoolImpl : Logger::Loggable<Logger::Id::pool>, public ConnectionPool:: | |||
std::list<DrainedCb> drained_callbacks_; | |||
Upstream::ResourcePriority priority_; | |||
const Network::ConnectionSocket::OptionsSharedPtr socket_options_; | |||
|
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.
nit: del newline
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 definitely like this approach, thanks for getting this working @lizan. A few nits, one concern about the test, and I would love a test for "stray data" killing off the connection (which I think may remove a latent race anyway)
client.stream_wrapper_.reset(); | ||
if (pending_requests_.empty()) { | ||
if (pending_requests_.empty() || delay) { | ||
// There is nothing to service so just move the connection into the ready list. |
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.
Update comment please
@@ -209,25 +215,44 @@ void ConnPoolImpl::onResponseComplete(ActiveClient& client) { | |||
host_->cluster().stats().upstream_cx_max_requests_.inc(); | |||
onDownstreamReset(client); | |||
} else { | |||
processIdleClient(client); | |||
processIdleClient(client, 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.
Can we comment why, perhaps with a link to the original issue to make it clear why we are doing this?
|
||
void ConnPoolImpl::onUpstreamReady() { | ||
upstream_ready_enabled_ = false; | ||
while (!pending_requests_.empty() && !ready_clients_.empty()) { |
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.
sanity check: this is simply deferring work we used to do in the last dispatcher loop to the next loop, so there's no corner case we'll end up batching together too much work and triggering the watchdog, right?
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 chance of batching together is when multiple upstream connections are completing responses at same time (same epoll callback), I don't think there will will be too much work to trigger watchdog.
// Response 1. | ||
upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "200"}}, false); | ||
upstream_request_->encodeData(512, true); | ||
fake_upstream_connection_->close(); |
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.
Sorry, I may not have had sufficient caffeine, but how are we making sure that the test Envoy is receiving the FIN before assigning the next request? If there were a context switch between encodeData and the close() couldn't Envoy read the whole request and reassign the upstream before the close() occurs? We may need to unit test this rather than integration test. Alternately we could do a raw tcp connection, send the response and stray data in one write call. I think that'd guarantee no race and I'd hope any activity on the delayed-use connection would cause it to be removed.
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 dispatcher is not running until next wait* call, so in the right next line waitForEndSream
the dispatcher runs, the connection read 512 bytes and FIN in same event. So the request is not being scheduled before close() occurs.
Also added a unit test in conn_pool_test too.
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.
Yeah, I'd like to think this will be fine, we've just had a spate of macos integration flakes due to slightly different network semantics. We can just keep an eye out for test flakes and remove this if it causes problems now that we have a unit test. @zuercher just so it's on his radar.
Signed-off-by: Lizan Zhou <zlizan@google.com>
Signed-off-by: Lizan Zhou <zlizan@google.com>
@@ -236,7 +239,8 @@ void ConnPoolImpl::onUpstreamReady() { | |||
void ConnPoolImpl::processIdleClient(ActiveClient& client, bool delay) { | |||
client.stream_wrapper_.reset(); | |||
if (pending_requests_.empty() || delay) { | |||
// There is nothing to service so just move the connection into the ready list. | |||
// There is nothing to service or delay processing is requested, so just move the connection |
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.
delay -> delayed
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.
Done
// Response 1. | ||
upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "200"}}, false); | ||
upstream_request_->encodeData(512, true); | ||
fake_upstream_connection_->close(); |
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.
Yeah, I'd like to think this will be fine, we've just had a spate of macos integration flakes due to slightly different network semantics. We can just keep an eye out for test flakes and remove this if it causes problems now that we have a unit test. @zuercher just so it's on his radar.
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.
Looks great! Let's fix that one last typo and I'll merge it in.
Attach pending upstream requests in next event after onResponseComplete. Risk Level: Medium Testing: unit test, integration test Docs Changes: N/A Release Notes: N/A Fixes envoyproxy#2715 Signed-off-by: Lizan Zhou <zlizan@google.com> Signed-off-by: Rama <rama.rao@salesforce.com>
// There is nothing to service so just move the connection into the ready list. | ||
if (pending_requests_.empty() || delay) { | ||
// There is nothing to service or delayed processing is requested, so just move the connection | ||
// into the ready list. | ||
ENVOY_CONN_LOG(debug, "moving to ready", *client.codec_client_); | ||
client.moveBetweenLists(busy_clients_, ready_clients_); |
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.
@lizan Too early to mark the client ready in this branch. Expecting anther read attempt to see if there is EOF
. This client could be immediately used by another down stream request.
At the next cycle, dispatcher always handle write attempt first, even there is EOF in the read buffer, that is a 503 for this upstream request.
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.
At the end of the upstream read event when we reach EOF the client will be removed so this is safe. https://github.com/envoyproxy/envoy/blob/v1.10.0/source/common/network/connection_impl.cc#L486
Signed-off-by: Lizan Zhou zlizan@google.com
Description:
Another approach to #2715, attach pending request in next event after onResponseComplete.
Risk Level: Low
Testing: unit test, integration test
Docs Changes: N/A
Release Notes: N/A
Fixes #2715