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: delaying attach pending requests #2871

Merged
merged 16 commits into from
Apr 24, 2018

Conversation

lizan
Copy link
Member

@lizan lizan commented Mar 22, 2018

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

Signed-off-by: Lizan Zhou <zlizan@google.com>
Copy link
Member

@mattklein123 mattklein123 left a 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(); });
Copy link
Member

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.

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

} else {
void ConnPoolImpl::onUpstreamReady() {
upstream_ready_posted_ = false;
while (!pending_requests_.empty() && !ready_clients_.empty()) {
Copy link
Member

@mattklein123 mattklein123 Mar 22, 2018

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).

Copy link
Member Author

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?

Copy link
Member

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).

Copy link
Member Author

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).

lizan added 3 commits March 23, 2018 01:17
Signed-off-by: Lizan Zhou <zlizan@google.com>
Signed-off-by: Lizan Zhou <zlizan@google.com>
Copy link
Member

@mattklein123 mattklein123 left a 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_(); }));
Copy link
Member

@mattklein123 mattklein123 Mar 23, 2018

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.

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.

@zuercher
Copy link
Member

Going to unassign myself as I'll be unavailable next week. @alyssawilk can assign another non-senior reviewwe on Monday as necessary.

@zuercher zuercher removed their assignment Mar 23, 2018
@alyssawilk
Copy link
Contributor

@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!

@mattklein123
Copy link
Member

@lizan can you check tests?

Signed-off-by: Lizan Zhou <zlizan@google.com>
@lizan
Copy link
Member Author

lizan commented Mar 28, 2018

Yep, I'm trying to reproduce them locally.

@lizan
Copy link
Member Author

lizan commented Mar 28, 2018

Hmm, I cannot reproduce them outside CircleCI, (on my macOS, GCE with docker or without docker)... Will continue to investigate, does anyone have idea?

@mattklein123
Copy link
Member

@lizan I'm guessing the test failures are spurious. Can you just push a dummy commit?

@lizan
Copy link
Member Author

lizan commented Mar 28, 2018

It seems consistent in CircleCI, I've pushed many merge commits which triggers CI run.

@mattklein123
Copy link
Member

@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?

@lizan
Copy link
Member Author

lizan commented Mar 28, 2018

Yeah that's my suspect too. I had runs_per_test=100 with no luck, will try more...

@lizan
Copy link
Member Author

lizan commented Mar 28, 2018

The latest ASAN/TSAN failure is JVM OOO java.io.IOException: Cannot allocate memory, so for sure they are irrelevant (CircleCI failure)

@lizan lizan force-pushed the http1_conn_pool_fix branch 3 times, most recently from ed84a4e to 76d7827 Compare March 29, 2018 04:07
@lizan
Copy link
Member Author

lizan commented Mar 29, 2018

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.

@mattklein123
Copy link
Member

@lizan friendly ping. What's the status of this one?

@lizan
Copy link
Member Author

lizan commented Apr 11, 2018

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.

@lizan lizan force-pushed the http1_conn_pool_fix branch from cdfa577 to 49e831e Compare April 21, 2018 00:54
@lizan
Copy link
Member Author

lizan commented Apr 21, 2018

@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!

Copy link
Member

@mattklein123 mattklein123 left a 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.

@@ -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_;

Copy link
Member

Choose a reason for hiding this comment

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

nit: del newline

@alyssawilk alyssawilk self-assigned this Apr 23, 2018
Copy link
Contributor

@alyssawilk alyssawilk left a 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.
Copy link
Contributor

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);
Copy link
Contributor

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()) {
Copy link
Contributor

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?

Copy link
Member Author

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();
Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

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.

lizan added 5 commits April 23, 2018 15:52
Signed-off-by: Lizan Zhou <zlizan@google.com>
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
Copy link
Contributor

Choose a reason for hiding this comment

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

delay -> delayed

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

// Response 1.
upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "200"}}, false);
upstream_request_->encodeData(512, true);
fake_upstream_connection_->close();
Copy link
Contributor

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.

alyssawilk
alyssawilk previously approved these changes Apr 24, 2018
Copy link
Contributor

@alyssawilk alyssawilk left a 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.

@alyssawilk alyssawilk merged commit ee6f148 into envoyproxy:master Apr 24, 2018
ramaraochavali pushed a commit to ramaraochavali/envoy that referenced this pull request May 3, 2018
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>
@lizan lizan deleted the http1_conn_pool_fix branch March 8, 2019 00:04
// 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_);
Copy link
Contributor

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.

Copy link
Member Author

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

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.

5 participants