event: Reduce potential for lock contention while executing dispatcher post callbacks.#14289
Conversation
…r post callbacks. Signed-off-by: Antonio Vicente <avd@google.com>
jmarantz
left a comment
There was a problem hiding this comment.
this looks great; just a couple of nits. Did we wind up finding any evidence of contention?
| if (post_callbacks_.empty()) { | ||
| return; | ||
| } | ||
| callbacks = std::move(post_callbacks_); |
There was a problem hiding this comment.
I didn't think std::move defined clearly what state post_callbacks_ would be in after this statement. So I was thinking maybe swap would be better. WDYT?
There was a problem hiding this comment.
post_callbacks_ is a list, so I think this is well defined. I could add post_callbacks_.clear(); after the move.
There was a problem hiding this comment.
ok that seems like it would de-risk it. From https://en.cppreference.com/w/cpp/utility/move
Unless otherwise specified, all standard library objects that have been moved from are placed in a valid but unspecified state. That is, only the functions without preconditions, such as the assignment operator, can be safely used on the object after it was moved from: ...
str.clear(); // OK, clear() has no preconditions
is that better than swap? I'm fine either way.
I did not see anything about std::list's move semantics in https://en.cppreference.com/w/cpp/container/list
There was a problem hiding this comment.
std::swap is defined as 3 move assignments. Added the clear and a comment.
There was a problem hiding this comment.
Instead of the clear(), I think it would be better to ASSERT(post_callbacks_.empty()). Then if something gets messed up and it somehow became a copy instead of a move (due to c++-shenanigans or something) we'll notice quickly, instead of callbacks not getting run.
There was a problem hiding this comment.
Just jumping in here... what about:
std::list<std::function<void()>> callbacks = [this]() {
Thread::LockGuard lock(post_lock_);
return std::exchange(post_callbacks_, {});
}();or some variation using std::exchange. The pattern of using exchange is mentioned in reference to an event Dispatcher class in an article on fluentcpp.com.
There was a problem hiding this comment.
I went through the article. I think the most relevant section is the "Why not just move?". The issue of expressing the "empty after move" constraint is addressed here by means of the ASSERT(post_callbacks_,empty());
| } | ||
| // It is important that the execution and deletion of the callback happen while post_lock_ is not | ||
| // held. Either the invocation or destructor of the callback can call post() on this dispatcher. | ||
| while (!callbacks.empty()) { |
There was a problem hiding this comment.
is there some reason not to write this as:
for (auto& callback : callbacks) {
callback();
}
There was a problem hiding this comment.
Differences in deletion order for the callback objects.
There was a problem hiding this comment.
good point. add that to the comment?
There was a problem hiding this comment.
Added a comment and test.
I also wanted to test yield behavior, but it turns out apparently ordering of schedulable callback scheduling is not deterministic so the yield behavior is not testable.
Signed-off-by: Antonio Vicente <avd@google.com>
Signed-off-by: Antonio Vicente <avd@google.com>
jmarantz
left a comment
There was a problem hiding this comment.
@envoyproxy/senior-maintainers ptal
…ocking Signed-off-by: Antonio Vicente <avd@google.com>
ggreenway
left a comment
There was a problem hiding this comment.
I like the change; much better to not lock/unlock the mutex for each callback separately.
| if (post_callbacks_.empty()) { | ||
| return; | ||
| } | ||
| callbacks = std::move(post_callbacks_); |
There was a problem hiding this comment.
Instead of the clear(), I think it would be better to ASSERT(post_callbacks_.empty()). Then if something gets messed up and it somehow became a copy instead of a move (due to c++-shenanigans or something) we'll notice quickly, instead of callbacks not getting run.
| // callbacks execute. Callbacks added after this transfer will re-arm post_cb_ and will execute | ||
| // later in the event loop. | ||
| Thread::LockGuard lock(post_lock_); | ||
| if (post_callbacks_.empty()) { |
There was a problem hiding this comment.
I think this case can be deleted; the function behaves correctly without it AFAICT.
There was a problem hiding this comment.
Done.
I was thinking of it as a micro optimization but didn't realize that most post_callbacks_ is usually non-empty when we get here.
|
/wait |
Signed-off-by: Antonio Vicente <avd@google.com>
lizan
left a comment
There was a problem hiding this comment.
This changed the behavior that if a posted callback post another callback, it will be in next event cycle instead of the current one. Is that intended?
| // It is important that the execution and deletion of the callback happen while post_lock_ is not | ||
| // held. Either the invocation or destructor of the callback can call post() on this dispatcher. | ||
| while (!callbacks.empty()) { | ||
| auto& callback = callbacks.front(); |
There was a problem hiding this comment.
can be in oneline: callbacks.front().callback()?
There was a problem hiding this comment.
I think the one line version would be:
callbacks.front()();
or
callbacks.front().operator();
| while (!callbacks.empty()) { | ||
| auto& callback = callbacks.front(); | ||
| callback(); | ||
| // pop the front so that the destructor of the callback that just executed runs before the next |
antoniovicente
left a comment
There was a problem hiding this comment.
This changed the behavior that if a posted callback post another callback, it will be in next event cycle instead of the current one. Is that intended?
Yes, there is a change in post loop behavior. The loop yields after the group of callbacks present is executed. Post callbacks added by the first set of post callbacks execute in the same event loop iteration (thanks to post_cb_->scheduleCallbackCurrentIteration()) but after other loop events have a chance to execute.
This change in behavior is desirable. I wanted to add a test to cover this behavior but I ran into an issue with ordering of post callbacks vs schedulable callbacks. I thought order was deterministic but apparently it isn't. The yield behavior will be testable as a consequence of #14293
| // It is important that the execution and deletion of the callback happen while post_lock_ is not | ||
| // held. Either the invocation or destructor of the callback can call post() on this dispatcher. | ||
| while (!callbacks.empty()) { | ||
| auto& callback = callbacks.front(); |
There was a problem hiding this comment.
I think the one line version would be:
callbacks.front()();
or
callbacks.front().operator();
| while (!callbacks.empty()) { | ||
| auto& callback = callbacks.front(); | ||
| callback(); | ||
| // pop the front so that the destructor of the callback that just executed runs before the next |
…ocking Signed-off-by: Antonio Vicente <avd@google.com>
Signed-off-by: Antonio Vicente <avd@google.com>
lizan
left a comment
There was a problem hiding this comment.
Just wanted to double-check the behavior change.
No strong preference on the nit, up to you.
| // It is important that the execution and deletion of the callback happen while post_lock_ is not | ||
| // held. Either the invocation or destructor of the callback can call post() on this dispatcher. | ||
| while (!callbacks.empty()) { | ||
| auto& callback = callbacks.front(); |
* master: buffer: Optimize the layout of Slices in Buffer::OwnedImpl by removing subclassing and storing slice info directly in the SliceDeque (envoyproxy#14282) gRPC client to be used by ext_proc filter (envoyproxy#14283) http2: Add integration tests for PRIORITY frame flood mitigation for upstream servers (envoyproxy#14328) event: touch watchdog before execution of each post callback and before deferred deletion (envoyproxy#14339) stale: more allowed ops (envoyproxy#14345) stale: more changes (envoyproxy#14344) test: TODO fixup making enable_half_close private envoyproxy#14330) event: Reduce potential for lock contention while executing dispatcher post callbacks. (envoyproxy#14289) stale: fix config (envoyproxy#14337) metrics service sink: generalize the sink and grpc streamer for external use (envoyproxy#13919) wasm: update V8 to v8.8.278.8. (envoyproxy#14298) repo: switch to actions based stale bot (envoyproxy#14335) buffer: Use WatermarkFactory to create most WatermarkBuffer instances (envoyproxy#14256) Signed-off-by: Michael Puncel <mpuncel@squareup.com>
Risk Level: low, minor code refactor
Testing: n/a, minimal functional changes expected.
Docs Changes:
Release Notes:
Platform Specific Features:
[Optional Runtime guard:]
[Optional Fixes #Issue]
[Optional Deprecated:]