-
Notifications
You must be signed in to change notification settings - Fork 5.5k
listener manager: implement graceful draining using a local drain man… #1219
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
Changes from 2 commits
c1d58c1
ac45a6d
079675b
5961da5
44abbb8
2c4cfd8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,20 +8,17 @@ namespace Envoy { | |
| namespace Server { | ||
|
|
||
| /** | ||
| * Handles connection draining. An instance is generally shared across the entire server. | ||
| * Handles connection draining. This concept is used globally during hot restart / server draining | ||
| * as well as on individual listeners when they are being dynamically removed. | ||
| */ | ||
| class DrainManager : public Network::DrainDecision { | ||
| public: | ||
| /** | ||
| * @return TRUE if the manager is currently draining connections. | ||
| * Invoked to begin the drain procedure. (Making drain close operations more likely). | ||
| * @param completion supplies the completion that will be called when the drain sequence is | ||
| * finished. | ||
| */ | ||
| virtual bool draining() PURE; | ||
|
|
||
| /** | ||
| * Invoked in the secondary process to begin the drain procedure. (Making drain close operations | ||
| * more likely). | ||
| */ | ||
| virtual void startDrainSequence() PURE; | ||
| virtual void startDrainSequence(std::function<void()> completion) PURE; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we drain globally on each LDS update? This seems reasonable given the current semantics (each LDS refresh is an update of the complete set of listeners), but won't this also preclude the possibility of skipping the drain on listeners that aren't being modified (i.e. they have the same config hash)?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, I see, we create a new drain manager per listener as well as the singleton per-server. Makes sense (could be called out in some comment as well).
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK will find a comment to add to. |
||
|
|
||
| /** | ||
| * Invoked in the newly launched primary process to begin the parent shutdown sequence. At the end | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ | |
| #include "common/ssl/context_config_impl.h" | ||
|
|
||
| #include "server/configuration_impl.h" // TODO(mattklein123): Remove post 1.4.0 | ||
| #include "server/drain_manager_impl.h" | ||
|
|
||
| namespace Envoy { | ||
| namespace Server { | ||
|
|
@@ -87,6 +88,10 @@ ProdListenerComponentFactory::createListenSocket(Network::Address::InstanceConst | |
| } | ||
| } | ||
|
|
||
| DrainManagerPtr ProdListenerComponentFactory::createDrainManager() { | ||
| return DrainManagerPtr{new DrainManagerImpl(server_)}; | ||
| } | ||
|
|
||
| ListenerImpl::ListenerImpl(const Json::Object& json, ListenerManagerImpl& parent, | ||
| const std::string& name, bool workers_started, uint64_t hash) | ||
| : Json::Validator(json, Json::Schema::LISTENER_SCHEMA), parent_(parent), | ||
|
|
@@ -98,7 +103,8 @@ ListenerImpl::ListenerImpl(const Json::Object& json, ListenerManagerImpl& parent | |
| per_connection_buffer_limit_bytes_( | ||
| json.getInteger("per_connection_buffer_limit_bytes", 1024 * 1024)), | ||
| listener_tag_(parent_.factory_.nextListenerTag()), name_(name), | ||
| workers_started_(workers_started), hash_(hash) { | ||
| workers_started_(workers_started), hash_(hash), | ||
| local_drain_manager_(parent.factory_.createDrainManager()) { | ||
|
|
||
| // ':' is a reserved char in statsd. Do the translation here to avoid costly inline translations | ||
| // later. | ||
|
|
@@ -130,6 +136,10 @@ bool ListenerImpl::createFilterChain(Network::Connection& connection) { | |
| return Configuration::FilterChainUtility::buildFilterChain(connection, filter_factories_); | ||
| } | ||
|
|
||
| bool ListenerImpl::drainClose() const { | ||
| return local_drain_manager_->drainClose() || parent_.server_.drainManager().drainClose(); | ||
| } | ||
|
|
||
| void ListenerImpl::infoLog(const std::string& message) { | ||
| ENVOY_LOG(info, "{}: name={}, hash={}, address={}", message, name_, hash_, address_->asString()); | ||
| } | ||
|
|
@@ -261,25 +271,38 @@ bool ListenerManagerImpl::addOrUpdateListener(const Json::Object& json) { | |
| } | ||
|
|
||
| void ListenerManagerImpl::drainListener(ListenerImplPtr&& listener) { | ||
| // TODO(mattklein123): Actually implement timed draining in a follow up. Currently we just | ||
| // correctly synchronize removal across all workers. | ||
| // First add the listener to the draining list. Thist must be done under the lock since it | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: s/Thirst/This/ Does the timed draining TODO no longer apply?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It no longer applies. The drain manager handles the timer and then removal via the completion callback. I will add more comments. |
||
| // can race with remove completions. | ||
| std::list<DrainingListener>::iterator draining_it; | ||
| { | ||
| std::lock_guard<std::mutex> guard(draining_listeners_lock_); | ||
| draining_it = draining_listeners_.emplace(draining_listeners_.begin(), std::move(listener), | ||
| workers_.size()); | ||
| } | ||
|
|
||
| draining_it->listener_->infoLog("removing listener"); | ||
| // Tell all workers to stop accepting new connections on this listener. | ||
| draining_it->listener_->infoLog("draining listener"); | ||
| for (const auto& worker : workers_) { | ||
| worker->removeListener(*draining_it->listener_, [this, draining_it]() -> void { | ||
| std::lock_guard<std::mutex> guard(draining_listeners_lock_); | ||
| if (--draining_it->workers_pending_removal_ == 0) { | ||
| draining_it->listener_->infoLog("listener removal complete"); | ||
| draining_listeners_.erase(draining_it); | ||
| } | ||
| }); | ||
| worker->stopListener(*draining_it->listener_); | ||
| } | ||
|
|
||
| // The following sets up 2 level lambda. The first completes when the listener's drain manager | ||
| // has completed draining at whatever the server configured drain times are. Once that is | ||
| // done we tell the workers to remove the listener. The 2nd lambda acquires the lock and | ||
| // determines when we can remove the listener from the draining list. This makes sure that we | ||
| // don't destroy the listener while filters might still be using its context (stats, etc.). | ||
| draining_it->listener_->localDrainManager().startDrainSequence([this, draining_it]() -> void { | ||
| draining_it->listener_->infoLog("removing listener"); | ||
| for (const auto& worker : workers_) { | ||
| worker->removeListener(*draining_it->listener_, [this, draining_it]() -> void { | ||
| std::lock_guard<std::mutex> guard(draining_listeners_lock_); | ||
| if (--draining_it->workers_pending_removal_ == 0) { | ||
| draining_it->listener_->infoLog("listener removal complete"); | ||
| draining_listeners_.erase(draining_it); | ||
| } | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| ListenerManagerImpl::ListenerList::iterator | ||
|
|
@@ -306,6 +329,12 @@ std::vector<std::reference_wrapper<Listener>> ListenerManagerImpl::listeners() { | |
| } | ||
|
|
||
| void ListenerManagerImpl::onListenerWarmed(ListenerImpl& listener) { | ||
| // The warmed listener should be added first so that the worker will accept new connections | ||
| // when it stops listening on the old listener. | ||
| for (const auto& worker : workers_) { | ||
| worker->addListener(listener); | ||
| } | ||
|
|
||
| auto existing_active_listener = getListenerByName(active_listeners_, listener.name()); | ||
| auto existing_warming_listener = getListenerByName(warming_listeners_, listener.name()); | ||
| (*existing_warming_listener)->infoLog("warm complete. updating active listener"); | ||
|
|
@@ -317,10 +346,6 @@ void ListenerManagerImpl::onListenerWarmed(ListenerImpl& listener) { | |
| } | ||
|
|
||
| warming_listeners_.erase(existing_warming_listener); | ||
|
|
||
| for (const auto& worker : workers_) { | ||
| worker->addListener(listener); | ||
| } | ||
| } | ||
|
|
||
| uint64_t ListenerManagerImpl::numConnections() { | ||
|
|
||
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 like this is optional (could be unassigned std::function) from the code below. Might be useful to call that out here.