-
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
QUIC hot restart part 6 - child instance pauses listening until parent is drained #31130
Conversation
2d2e97b
to
a156650
Compare
a156650
to
f37e653
Compare
Signed-off-by: Raven Black <ravenblack@dropbox.com>
Signed-off-by: Raven Black <ravenblack@dropbox.com>
Signed-off-by: Raven Black <ravenblack@dropbox.com>
Signed-off-by: Raven Black <ravenblack@dropbox.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.
I think this is a reasonable approach overall. But I think you'll need a way to un-register for these callbacks, in the case that the quic listener is removed via LDS after the child has started, but before the parent has finished draining.
I'd also like to get at least one more opinion on this, maybe oneof @wbpcode @soulxu @alyssawilk @yanavlasov.
@@ -34,20 +34,49 @@ UdpListenerImpl::UdpListenerImpl(Event::Dispatcher& dispatcher, SocketSharedPtr | |||
: BaseListenerImpl(dispatcher, std::move(socket)), cb_(cb), time_source_(time_source), | |||
// Default prefer_gro to false for downstream server traffic. | |||
config_(config, false) { | |||
parent_drained_callback_registry_ = | |||
dynamic_cast<const UdpListenSocket&>(*socket_).parentDrainedCallbackRegistry(); |
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.
Why is a dynamic_cast needed? Can the types be changed so that this isn't needed?
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.
Couldn't upgrade the types because it cascaded back up through multiple layers to eventually interfaces which, in order to be able to use the specified type, caused a circular dependency.
So instead I put a virtual function on the base Socket type, which is yuck, but works cleanly and doesn't cause any kind of dependency cascade.
envoy/network/listen_socket.h
Outdated
* @param callback the function to call when the listener matching address is | ||
* drained on the parent instance. | ||
*/ | ||
virtual void registerParentDrainedCallback(std::string address, |
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.
Why is the address a string instead an address shared_ptr?
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.
Now a (const reference to) an address shared_ptr. It was an ill-considered simplification (it still ends up as a string in the end because we can't make a map keyed on address objects) - stringing it early reduces dependencies and makes it simpler to test, but also reduces how much it makes sense.
Signed-off-by: Raven Black <ravenblack@dropbox.com>
That's handled as-is by the callback self-aborting, rather than by unregistering. I don't think unregistering can work reliably because the destructor may not be called on the thread that owns the registry (it could work but would require a network of locks and the behavior would become much less clear if destruction and unpausing are trying to be concurrent - having the callback abort on-thread if the destructor has happened means the "race" occurs exclusively in the worker thread, so there can be no attempted concurrency between destruction and notification). |
Or we only register the thread worker as the callbacks, then we can use the existing disableListeners/enableListeners method (maybe add a new flag to disable all the udp listener) of ConnectionHandler to pause/unpause the listener. envoy/source/common/listener_manager/connection_handler_impl.h Lines 56 to 57 in d797293
|
The problem with using the existing functions is there are already calls to those functions which could potentially make something racily do the wrong thing, e.g. if a listener starts up and is disabled by a call to disableListeners for hot restart purposes, then is disabled for some other reason, then hot restart draining completes and calls enableListeners, now the socket would be listening despite the other caller of disableListeners expecting it not to be. Tracking it separately ensures that the socket is only reading if both things want it to be. So my rationale for separating out this paused state is that it can play well with other enable/disable operations with no risk of conflict. Another way to achieve this would be to add a "disabledness" counter so that two disables must be matched to two enables, but knowing whether that would be safe or not would require digging a lot more into existing code to ensure it doesn't currently expect one enable to be capable of overriding two prior disables. |
Signed-off-by: Raven Black <ravenblack@dropbox.com>
Signed-off-by: Raven Black <ravenblack@dropbox.com>
Signed-off-by: Raven Black <ravenblack@dropbox.com>
Signed-off-by: Raven Black <ravenblack@dropbox.com>
/coverage |
Coverage for this Pull Request will be rendered here: https://storage.googleapis.com/envoy-pr/31130/coverage/index.html The coverage results are (re-)rendered each time the CI |
Signed-off-by: Raven Black <ravenblack@dropbox.com>
Signed-off-by: Raven Black <ravenblack@dropbox.com>
Signed-off-by: Raven Black <ravenblack@dropbox.com>
Signed-off-by: Raven Black <ravenblack@dropbox.com>
/retest |
Signed-off-by: Raven Black <ravenblack@dropbox.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.
Looks good overall!
/wait
socket_->connectionInfoProvider().localAddress(), | ||
[this, &dispatcher, alive = std::weak_ptr<void>(destruction_checker_)]() { | ||
dispatcher.post([this, alive = std::move(alive)]() { | ||
if (alive.lock()) { |
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 think to be not-racy this needs to be
auto ptr = alive.lock();
if (ptr != nullptr) {
That will guarantee that the pointer can't be deleted while unpause()
is running.
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.
It's safe because the destruction happens on the same thread, but changing as suggested because you're right that it otherwise merits a comment explaining this, and it's easier to just make it not matter. :)
void HotRestartingChild::allDrainsImplicitlyComplete() { | ||
for (auto& drain_action : on_drained_actions_) { | ||
// call the callback. | ||
std::move(drain_action.second)(); |
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.
What does the std::move
do here? Is it needed?
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, this is required for calling an AnyInvocable
to do with it being a "call once" type.
Signed-off-by: Raven Black <ravenblack@dropbox.com>
Signed-off-by: Raven Black <ravenblack@dropbox.com>
Signed-off-by: Raven Black <ravenblack@dropbox.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
/retest |
…il parent is drained (envoyproxy#31130)" This reverts commit f7352b3. Signed-off-by: Ryan Northey <ryan@synca.io>
Commit Message: QUIC hot restart part 6 - child instance pauses listening until parent is drained
Additional Description: Make the child instance not read from the UDP socket until the parent instance stops reading. From the prior changes, the parent instance forwards UDP packets it doesn't recognize to the child instance via a side-channel, so not reading doesn't mean not responding - it just means not intercepting packets which may be intended for the parent instance.
In this change there is no drain optimization - if the parent has no more live connections we will still wait and forward packets until the drain time completes. I think it is most likely not worth optimizing for that, since the eventual goal is to migrate the QUIC-packet-sorting behavior onto eBPF at which point, for systems that care about performance, all of this manual forwarding support will be unused anyway.
I've tried to keep entirely of the performance hot path, so the change only touches constructors and enable/disable operations and stays entirely out of the read path.
It's awkward because the point at which we detect that the socket is being forwarded (the creation of the
UdpListenSocket
) theUdpListener
andActiveQuicListener
don't exist, but those are where we need to disable or enable reading. Also awkward is the potential race - the unpause callback can potentially arrive after the listener was deleted, and must go to the right thread, so there's a bit of a tangle ensuring that that's the case - for this we still call the callback, but abort if the listener was deleted by the time the dispatcher tries to perform the action.Finally, there's a potential race of disable/enable and this paused state, so I've added enough state to the
UdpListener
that the sequence start_paused->disable->unpause does not accidentally end with the listener enabled.Risk Level: Some risk, this is touching quite a bit of core code. The touches should mostly not do anything except to QUIC listeners, so the risk isn't as big as it looks.
Testing: Unit tests provide coverage; I intend to augment hotrestart_handoff test to make sure this is fully doing what it needs to do for QUIC listeners.
Docs Changes:
Release Notes:
Platform Specific Features: