Skip to content

[http] add http connection pool idle#5631

Closed
klarose wants to merge 6 commits intoenvoyproxy:masterfrom
klarose:add_conn_pool_Idle
Closed

[http] add http connection pool idle#5631
klarose wants to merge 6 commits intoenvoyproxy:masterfrom
klarose:add_conn_pool_Idle

Conversation

@klarose
Copy link
Copy Markdown
Contributor

@klarose klarose commented Jan 16, 2019

Description:
This adds the concept of a connection pool going "idle", implementing it for http1. A connection pool is idle if it has neither pending requests nor active requests. A user of a connection pool can ask it to
invoke a callback when the pool goes idle. A list of callbacks is maintained.

This is intended to be used to inform a mapping class when a connection pool is no longer used. The mapping class will use this information to help enforce a limit on the number of concurrent mapped connection pools. See #5337 (comment)

Risk Level: Low
Testing: Unit testing.
Docs Changes: None

We want to know when a connection pool is no longer processing requests.
This adds a callback mechanism so consumers may be informed when a
connection pool transitions into this state.

Signed-off-by: Kyle Larose <kyle@agilicus.com>
@klarose
Copy link
Copy Markdown
Contributor Author

klarose commented Jan 16, 2019

My next pull request will implement this for http2. I've left it out of this PR so I can get feedback on the overall approach early, and to keep the PR small.

My plan for http2 is to use the following logic to decide whether the pool is idle:

   (primary || primary->numActiveRequests() == 0) &&
   (draining || draining->numActiveRequests() == 0) &&
   pending_requests.empty()

After that I'll implement the mapping class, then hook it in.

@lizan lizan self-assigned this Jan 16, 2019
Comment thread source/common/http/http2/conn_pool.h Outdated
Comment thread source/common/http/http1/conn_pool.cc Outdated
Comment thread source/common/http/http1/conn_pool.h Outdated
std::list<ActiveClientPtr> ready_clients_;
std::list<ActiveClientPtr> busy_clients_;
std::list<DrainedCb> drained_callbacks_;
std::list<IdleCb> idle_callbacks_;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

it might be overkill, did you consider using Common::CallbackManager?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Makes sense. I replaced idle_callbacks_ with that. I considered replacing drained_callbacks_, but it has some logic dependent on whether the list is empty, and that wasn't exposed by common::CallbackManager. Can probably be added in another PR.

Returning the same decoder caused problems.

Signed-off-by: Kyle Larose <kyle@agilicus.com>
Signed-off-by: Kyle Larose <kyle@agilicus.com>
Signed-off-by: Kyle Larose <kyle@agilicus.com>
Signed-off-by: Kyle Larose <kyle@agilicus.com>
We were actually running a fucntion called from a parent class. Whoops.

Signed-off-by: Kyle Larose <kyle@agilicus.com>
Copy link
Copy Markdown
Member

@lizan lizan left a comment

Choose a reason for hiding this comment

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

LGTM modulo one nit.

// Http::ConnPoolImplBase
void checkForDrained() override;
void checkForIdle() override { /* TODO(klarose): implement */
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: NOT_IMPLEMENTED_GCOVR_EXCL_LINE

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is actually being invoked by the base class when a pending request is cancelled. I had this in originally, but it failed because this line panicked in a UT.

@lizan lizan added the waiting label Jan 17, 2019
@mattklein123 mattklein123 self-assigned this Jan 17, 2019
Copy link
Copy Markdown
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.

A question to get started. Thank you.

request.removeFromList(pending_requests_);
host_->cluster().stats().upstream_rq_cancelled_.inc();
checkForDrained();
checkForIdle();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is it possible to merge checkForDrained() and checkForIdle() into a single function? I see there is some boolean logic below, but I think it should be possbile and would make the code less fragile. In general FAICT drained and idle are basically the same thing, but the intention of the callback is the same? Could we just use a single callback system and a different registration callback for your purpose and not have this new callback type at all?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Are you thinking of a callback object with something like onDrained vs onIdle?

Then, the checkForX function would be along the lines of:

if(drainedCondition):
 for each callback:
   callback.onDrained
if (idleCondition):
  for each callback:
   callback.onIdle

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What I'm asking is whether this change is needed. AFAICT idle and drained are the same thing. Can't you just register for a drained callback in the place where you want to do something when it's idle?

Copy link
Copy Markdown
Contributor Author

@klarose klarose Jan 17, 2019

Choose a reason for hiding this comment

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

My concern is that adding a drained callback has side effects -- namely, when the pool goes idle, it will actively close all of its upstream connections. My vision here is for the upstreams to stay active until we need to actually free up connection pools for other hash key values.

But, in the short term, it's probably not a big deal. I had planned on allocating and freeing them as necessary in the first iteration. But, now that I think about it, a better approach may be to only register the drained callback when there is pressure from the constraints.

My original thought was along these lines:

ConnectionPool* assignNewPool(hash) {
  pool = getActivePool(hash)
  if pool { return pool; }

  pool = getIdlePool();
  if (!pool) { pool = allocateNewPoolIfPossible(); }
  if (!pool) {
    // out of resources
    return nullptr;
  }

  pool->addIdleCallback([&]() {this->poolIdle(hash, pool); });
  return pool;
}

void poolIdle(hash, pool) {
  moveFromActiveToIdle(hash, pool);
}

I was worried that the act of registering a drained callback would be problematic. But, maybe it's not. Here's an alternative:

ConnectionPool* assignNewPool(hash) {
  pool = getActivePool(hash)
  if pool { return pool; }

  pool = getIdlePool();
  if (!pool) { pool = allocateNewPoolIfPossible(); }
  if (!pool) {
    for each activePool {
      activePool->addDrainedCallback([&](){this->poolIdle(hash, pool);});
    }
     // at this point, any pools which are currently idle will have invoked poolIdle.
     return getIdlePool();
  }

  return pool;
}

void poolIdle(hash, pool) {
  moveFromActiveToIdle(hash, pool);
}

I think I'll park this idle concept for now, and try what I've just suggested.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

One thing I like about my "new" approach a bit better is that it could allow for some hysteresis on the draining. Start cleaning up pools if we cross a threshold, but before we run out, and stop cleaning them up when we cross below another. This would require that we add the ability to remove a drained callback, but we'll probably want that anyway.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I see what you are saying. I guess I would say two things:

  1. I would probably start simple and just use the drained callback for now, and we can optimize later if needed.
  2. if we do need to optimize, I think the implementation internally can actually share the same check idle/drained logic and just use a boolean to determine whether to close active connections or not. That would reduce the logic duplication.

WDYT?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed. I'll close this.

@klarose
Copy link
Copy Markdown
Contributor Author

klarose commented Jan 17, 2019

As discussed above, we're going to try an approach that does not require this.

@klarose klarose closed this Jan 17, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants